/* ** settingsmenu.cpp ** ** This file is part of mkxp. ** ** Copyright (C) 2014 Jonas Kulla <Nyocurio@gmail.com> ** ** mkxp is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 2 of the License, or ** (at your option) any later version. ** ** mkxp is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with mkxp. If not, see <http://www.gnu.org/licenses/>. */ #include "settingsmenu.h" #include <SDL.h> #include <SDL_video.h> #include <SDL_keyboard.h> #include "keybindings.h" #include "eventthread.h" #include "font.h" #include "input.h" #include "etc-internal.h" #include "util.h" #include "gl-fun.h" #include "bundledfont.h" #include "eventthread.h" #include "imgui/imgui.h" #include "imgui/imgui_impl_sdl.h" #include <algorithm> #include <assert.h> const Vec2i winSize(740, 400); const float fontSize = 16.0f; const ImVec4 colButton = ImColor(96,96,96); const ImVec4 colButtonHover = ImColor(51,51,51); const ImVec4 colBackground = ImColor(128,128,128); const uint8_t numCols = 3; const uint8_t numRows = 4; typedef SettingsMenuPrivate SMP; #define BTN_STRING(btn,desc) { Input:: btn, #desc } struct VButton { Input::ButtonCode code; const char *str; } static const vButtons[] = { BTN_STRING(Up,Up), BTN_STRING(Down,Down), BTN_STRING(L,L), BTN_STRING(Left,Left), BTN_STRING(Right,Right), BTN_STRING(R,W-Atk), BTN_STRING(A,Dismount), BTN_STRING(B,Cancel), BTN_STRING(C,Confirm), BTN_STRING(X,A-Atk), BTN_STRING(Y,S-Atk), BTN_STRING(Z,D-Atk) }; static elementsN(vButtons); /* Macros to read/write central config and check for changed values */ #define STORE_CONFIG(key) rtData.config. key = tempConfig. key; rtData.config.store(#key, rtData.config. key ) #define VALUE_CHANGED(key) (rtData.config. key != tempConfig. key) /* Holds configurables that can be modified in the settings menu /* until they get all written out the config file and applied */ struct Configurables { bool fullscreen; bool fixedAspectRatio; bool smoothScaling; bool vsync; int defScreenW; int defScreenH; bool frameSkip; bool solidFonts; Configurables() { } Configurables(Config &c) { fullscreen = c.fullscreen; fixedAspectRatio = c.fixedAspectRatio; smoothScaling = c.smoothScaling; vsync = c.vsync; defScreenW = c.defScreenW; defScreenH = c.defScreenH; frameSkip = c.frameSkip; solidFonts = c.solidFonts; } } static tempConfig; /* Human readable string representation */ std::string sourceDescString(const SourceDesc &src) { char buf[128]; char pos; switch (src.type) { case Invalid: return std::string(); case Key: { if (src.d.scan == SDL_SCANCODE_LSHIFT) return "Shift"; SDL_Keycode key = SDL_GetKeyFromScancode(src.d.scan); const char *str = SDL_GetKeyName(key); if (*str == '\0') return "Unknown key"; else return str; } case JButton: snprintf(buf, sizeof(buf), "JS %d", src.d.jb); return buf; case JHat: switch(src.d.jh.pos) { case SDL_HAT_UP: pos = 'U'; break; case SDL_HAT_DOWN: pos = 'D'; break; case SDL_HAT_LEFT: pos = 'L'; break; case SDL_HAT_RIGHT: pos = 'R'; break; default: pos = '-'; } snprintf(buf, sizeof(buf), "Hat %d:%c", src.d.jh.hat, pos); return buf; case JAxis: snprintf(buf, sizeof(buf), "Axis %d%c", src.d.ja.axis, src.d.ja.dir == Negative ? '-' : '+'); return buf; } assert(!"unreachable"); return ""; } struct BindingWidget { SMP *p; VButton vb; /* Source slots */ SourceDesc src[4]; /* Flag indicating whether a slot source is used * for multiple button targets (red indicator) */ bool dupFlag[4]; BindingWidget(int vbIndex, SMP *p) : vb(vButtons[vbIndex]), p(p) {} void appendBindings(BDescVec &d) const; void click(SourceDesc& desc); void displayWidget(uint32_t width, uint32_t height); }; enum State { Idle, AwaitingInput }; static bool resCheckbox[3]; struct SettingsMenuPrivate { State state; /* Necessary to decide which window gets to * process joystick events */ bool hasFocus; /* Tell the outer EventThread to destroy us */ bool destroyReq; /* Set to true if there are any duplicate bindings */ bool dupWarn; SDL_Window *window; SDL_GLContext glContext; uint32_t winID; enum tabs { CONTROLS, GRAPHICS }; enum tabs currentTab; RGSSThreadData &rtData; std::vector<BindingWidget> bWidgets; SourceDesc *captureDesc; const char *captureName; SettingsMenuPrivate(RGSSThreadData &rtData) : rtData(rtData) { } void setupBindingData(const BDescVec &d) { size_t slotI[vButtonsN] = { 0 }; for (size_t i = 0; i < bWidgets.size(); ++i) for (size_t j = 0; j < 4; ++j) bWidgets[i].src[j].type = Invalid; for (size_t i = 0; i < d.size(); ++i) { const BindingDesc &desc = d[i]; const Input::ButtonCode trg = desc.target; size_t j; for (j = 0; j < vButtonsN; ++j) if (bWidgets[j].vb.code == trg) break; assert(j < vButtonsN); size_t &slot = slotI[j]; BindingWidget &w = bWidgets[j]; if (slot == 4) continue; w.src[slot++] = desc.src; } } void updateDuplicateStatus() { for (size_t i = 0; i < bWidgets.size(); ++i) for (size_t j = 0; j < 4; ++j) bWidgets[i].dupFlag[j] = false; dupWarn = false; for (size_t i = 0; i < bWidgets.size(); ++i) { for (size_t j = 0; j < 4; ++j) { const SourceDesc &src = bWidgets[i].src[j]; if (src.type == Invalid) continue; for (size_t k = 0; k < bWidgets.size(); ++k) { if (k == i) continue; for (size_t l = 0; l < 4; ++l) { if (bWidgets[k].src[l] != src) continue; bWidgets[i].dupFlag[j] = true; bWidgets[k].dupFlag[l] = true; dupWarn = true; } } } } } void redraw() { ImGui_ImplSdl_NewFrame(window); { ImGui::SetNextWindowSize(ImVec2((float)winSize.x,(float)winSize.y)); ImGui::SetNextWindowPos(ImVec2(0, 0)); ImGuiWindowFlags WindowFlags = 0; WindowFlags |= ImGuiWindowFlags_NoTitleBar; WindowFlags |= ImGuiWindowFlags_NoResize; WindowFlags |= ImGuiWindowFlags_NoMove; WindowFlags |= ImGuiWindowFlags_NoScrollbar; WindowFlags |= ImGuiWindowFlags_NoCollapse; WindowFlags |= ImGuiWindowFlags_NoScrollWithMouse; WindowFlags |= ImGuiWindowFlags_NoSavedSettings; ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0); ImGui::PushStyleColor(ImGuiCol_Button, colButton); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, colButtonHover); ImGui::PushStyleColor(ImGuiCol_ButtonActive, colButtonHover); ImGui::PushStyleColor(ImGuiCol_WindowBg, colBackground); bool bTrue = true; ImGui::Begin("Container", &bTrue, WindowFlags); tabSelector("Controls", CONTROLS); ImGui::SameLine(); ImGui::Text("|"); ImGui::SameLine(); if(tabSelector("Graphics", GRAPHICS)) { tempConfig = Configurables(rtData.config); } ImGui::Separator(); switch(currentTab) { case CONTROLS: displayControllerTab(); break; case GRAPHICS: displayGraphicsTab(); break; } ImGui::End(); ImGui::PopStyleColor(4); ImGui::PopStyleVar(); } ImGui::Render(); SDL_GL_SwapWindow(window); } bool tabSelector(const char * tabName, tabs tabId) { if (ImGui::Selectable(tabName, currentTab == tabId, 0, ImGui::CalcTextSize(tabName)) && (state == Idle)) { currentTab = tabId; return true; } return false; } void displayControllerTab() { ImVec4 red = ImColor(255, 0, 0); if(state == AwaitingInput) { ImGui::Dummy(ImVec2(0, ImGui::GetWindowContentRegionMax().y/2 - fontSize/2)); ImGui::Text("Press key or joystick button for \"%s\"", captureName); } else { /* Header Text */ ImGui::Text("Use left click to bind a slot, right click to clear its binding"); if(dupWarn) { ImGui::TextColored(red, "Warning: Same physical key bound to multiple slots"); } /* Button Assignment Widgets */ uint32_t widgetWidth = (ImGui::GetWindowContentRegionMax().x-ImGui::GetStyle().WindowPadding.x) / numCols; uint32_t widgetHeight = 64; ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(2, 2)); ImGui::PushStyleColor(ImGuiCol_ChildWindowBg, ImColor(0, 0, 0)); ImGui::PushStyleColor(ImGuiCol_Button, colBackground); ImGui::BeginChild("Table", ImVec2(numCols*widgetWidth+8, numRows*widgetHeight+2)); ImGui::Spacing(); int i = 0; for(int y = 0; y < numRows; y++) { ImGui::Dummy(ImVec2(0, 0)); for(int x = 0; x < numCols; x++) { ImGui::SameLine(); bWidgets[i].displayWidget(widgetWidth, widgetHeight); i++; } } ImGui::EndChild(); ImGui::PopStyleColor(2); ImGui::PopStyleVar(); ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); /* Bottom Buttons */ ImVec2 btnDim = ImVec2(100, 24); if(ImGui::Button("Reset Default", btnDim)) { onResetToDefault(); } ImGui::SameLine(); ImGui::Dummy(ImVec2(ImGui::GetWindowContentRegionMax().x - ImGui::GetStyle().WindowPadding.x - 3*btnDim.x - 2*ImGui::GetStyle().ItemSpacing.x, btnDim.y)); ImGui::SameLine(); if(ImGui::Button("Cancel", btnDim)) { onCancel(); } ImGui::SameLine(); if(ImGui::Button("Store", btnDim)) { onAccept(); } } } static inline bool resolutionEqualsN(int* x, int* y, int n) { return ((x[0] == n*y[0]) && (x[1] == n*y[1])); } static inline bool TextCheckbox(const char* str_id, bool active, bool &hovered, const ImVec2 &size) { bool result; ImVec2 innerPadding = ImGui::GetStyle().FramePadding; ImVec2 innerSize = ImVec2(size.x-2*innerPadding.x, size.y-2*innerPadding.y); ImGuiID id = ImGui::GetID(str_id); /* Set button colors to match checkbox */ if(active) ImGui::PushStyleColor(ImGuiCol_Button, ImGui::GetStyle().Colors[ImGuiCol_CheckMark]); else ImGui::PushStyleColor(ImGuiCol_Button, ImColor(0, 0, 0, 0)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::GetStyle().Colors[ImGuiCol_Button]); ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImGui::GetStyle().Colors[ImGuiCol_CheckMark]); /* Was item hovered in the previous frame? */ if(hovered) ImGui::PushStyleColor(ImGuiCol_FrameBg, ImGui::GetStyle().Colors[ImGuiCol_FrameBgHovered]); ImGui::BeginChildFrame(id, size); if(hovered) ImGui::PopStyleColor(); hovered = ImGui::IsWindowHovered(); /* Align button properly in child window */ ImVec2 pos = ImGui::GetWindowPos(); pos.x += innerPadding.x; pos.y += innerPadding.y; ImGui::SetWindowPos(pos); result = ImGui::Button(str_id, innerSize); ImGui::EndChildFrame(); ImGui::PopStyleColor(3); return result; } static inline void TextCentered(const char* str_id, const ImVec2 &size) { ImGui::PushStyleColor(ImGuiCol_Button, ImGui::GetStyle().Colors[ImGuiCol_WindowBg]); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::GetStyle().Colors[ImGuiCol_WindowBg]); ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImGui::GetStyle().Colors[ImGuiCol_WindowBg]); ImGui::Button(str_id, size); ImGui::PopStyleColor(3); } void displayGraphicsTab() { if(ImGui::CollapsingHeader("Display Settings", 0, true, true)) { /* current resolution and native rendering resolution */ int *res = &tempConfig.defScreenW; int native[2] = {(rtData.config.rgssVersion == 1 ? 640 : 544), (rtData.config.rgssVersion == 1 ? 480 : 416)}; if(ImGui::InputInt2("Window Size", res)) { /* clamp to between 320x240 and 4K resolutions */ tempConfig.defScreenW = std::min(std::max(tempConfig.defScreenW, 320),4096); tempConfig.defScreenH = std::min(std::max(tempConfig.defScreenH, 240),2160); } if(TextCheckbox("1X native", resolutionEqualsN(res, native, 1), resCheckbox[0], ImVec2(80, 24))) { res[0] = native[0]; res[1] = native[1]; } ImGui::SameLine(); if(TextCheckbox("2X native", resolutionEqualsN(res, native, 2), resCheckbox[1], ImVec2(80, 24))) { res[0] = 2*native[0]; res[1] = 2*native[1]; } ImGui::SameLine(); if(TextCheckbox("3X native", resolutionEqualsN(res, native, 3), resCheckbox[2], ImVec2(80, 24))) { res[0] = 3*native[0]; res[1] = 3*native[1]; } ImGui::SameLine(); TextCentered("Recommended if no smooth upscaling.", ImVec2(0, 24)); ImGui::Checkbox("Start in fullscreen", &tempConfig.fullscreen); ImGui::SameLine(); ImGui::Checkbox("Keep aspect ratio", &tempConfig.fixedAspectRatio); } ImGui::Dummy(ImVec2(0, 48)); if(ImGui::CollapsingHeader("Quality Settings", 0, true, true)) { ImGui::Checkbox("Enable smooth upscaling", &tempConfig.smoothScaling); ImGui::Checkbox("Enable vertical sync", &tempConfig.vsync); ImGui::Checkbox("Skip frames when too slow", &tempConfig.frameSkip); ImGui::Checkbox("Fast font rendering", &tempConfig.solidFonts); } ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); /* Buttons */ ImVec2 btnDim = ImVec2(150, 24); ImGui::Dummy(ImVec2(ImGui::GetWindowContentRegionMax().x - ImGui::GetStyle().WindowPadding.x - 2*btnDim.x - 1*ImGui::GetStyle().ItemSpacing.x, btnDim.y)); ImGui::SameLine(); if(ImGui::Button("Discard Changes", btnDim)) { tempConfig = Configurables(rtData.config); } ImGui::SameLine(); if(ImGui::Button("Apply Changes", btnDim)) { bool refreshWindow = false; if(VALUE_CHANGED(defScreenW) || VALUE_CHANGED(defScreenH)) { STORE_CONFIG(defScreenW); STORE_CONFIG(defScreenH); refreshWindow = true; } if(VALUE_CHANGED(fullscreen)) { STORE_CONFIG(fullscreen); } if(VALUE_CHANGED(fixedAspectRatio)) { STORE_CONFIG(fixedAspectRatio); refreshWindow = true; } if(VALUE_CHANGED(smoothScaling)) { STORE_CONFIG(smoothScaling); } if(VALUE_CHANGED(vsync)) { STORE_CONFIG(vsync); } if(VALUE_CHANGED(frameSkip)) { STORE_CONFIG(frameSkip); } if(VALUE_CHANGED(solidFonts)) { STORE_CONFIG(solidFonts); } if(refreshWindow) SDL_SetWindowSize(rtData.window, tempConfig.defScreenW, tempConfig.defScreenH); } } bool onCaptureInputEvent(const SDL_Event &event) { assert(captureDesc); SourceDesc &desc = *captureDesc; switch (event.type) { case SDL_KEYDOWN: desc.type = Key; desc.d.scan = event.key.keysym.scancode; /* Special case aliases */ if (desc.d.scan == SDL_SCANCODE_RSHIFT) desc.d.scan = SDL_SCANCODE_LSHIFT; if (desc.d.scan == SDL_SCANCODE_KP_ENTER) desc.d.scan = SDL_SCANCODE_RETURN; break; case SDL_JOYBUTTONDOWN: desc.type = JButton; desc.d.jb = event.jbutton.button; break; case SDL_JOYHATMOTION: { int v = event.jhat.value; /* Only register if single directional input */ if (v != SDL_HAT_LEFT && v != SDL_HAT_RIGHT && v != SDL_HAT_UP && v != SDL_HAT_DOWN) return true; desc.type = JHat; desc.d.jh.hat = event.jhat.hat; desc.d.jh.pos = v; break; } case SDL_JOYAXISMOTION: { int v = event.jaxis.value; /* Only register if pushed halfway through */ if (v > -JAXIS_THRESHOLD && v < JAXIS_THRESHOLD) return true; desc.type = JAxis; desc.d.ja.axis = event.jaxis.axis; desc.d.ja.dir = v < 0 ? Negative : Positive; break; } default: return false; } captureDesc = 0; state = Idle; updateDuplicateStatus(); return true; } void onResetToDefault() { setupBindingData(genDefaultBindings(rtData.config, rtData.gamecontroller)); updateDuplicateStatus(); } void onAccept() { BDescVec binds; for (size_t i = 0; i < bWidgets.size(); ++i) bWidgets[i].appendBindings(binds); rtData.bindingUpdateMsg.post(binds); /* Store the key bindings to disk as well to prevent config loss */ storeBindings(binds, rtData.config); } void onCancel() { destroyReq = true; } }; void BindingWidget::click(SourceDesc &desc) { /* Check for right click */ if(ImGui::IsMouseClicked(1)) { desc.type = Invalid; p->updateDuplicateStatus(); return; } p->captureDesc = &desc; p->captureName = vb.str; p->state = AwaitingInput; } void BindingWidget::displayWidget(uint32_t width, uint32_t height) { ImVec2 buttonSize = ImVec2((width-6)/3, height/2-ImGui::GetStyle().ItemSpacing.x); ImGui::PushID(vb.code); /* Label for Widget */ ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::GetStyle().Colors[ImGuiCol_WindowBg]); ImGui::Button(vb.str, ImVec2(width/3, height-ImGui::GetStyle().ItemSpacing.x)); ImGui::PopStyleColor(); ImGui::SameLine(); /* Group of buttons */ ImGui::BeginGroup(); for(int i=0; i<4; i++) { if(dupFlag[i]) ImGui::PushStyleColor(ImGuiCol_Text, ImColor(255, 0, 0)); if(ImGui::Button(sourceDescString(src[i]).c_str(), buttonSize)) click(src[i]); if(dupFlag[i]) ImGui::PopStyleColor(); if(i%2 == 0) ImGui::SameLine(); } ImGui::EndGroup(); ImGui::PopID(); } void BindingWidget::appendBindings(BDescVec &d) const { for (size_t i = 0; i < 4; ++i) { if (src[i].type == Invalid) continue; BindingDesc desc; desc.src = src[i]; desc.target = vb.code; d.push_back(desc); } } SettingsMenu::SettingsMenu(RGSSThreadData &rtData) { p = new SettingsMenuPrivate(rtData); p->state = Idle; p->hasFocus = false; p->destroyReq = false; p->dupWarn = false; p->currentTab = SettingsMenuPrivate::CONTROLS; p->window = SDL_CreateWindow("Settings Menu", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, winSize.x, winSize.y, SDL_WINDOW_OPENGL|SDL_WINDOW_INPUT_FOCUS); p->winID = SDL_GetWindowID(p->window); p->glContext = SDL_GL_CreateContext(p->window); ImGui_ImplSdl_Init(p->window); /* ImGUI wants to own the memory with the TTF data, so requires a copy. */ void * liberation_copy = malloc(BNDL_F_L(BUNDLED_FONT)); memcpy(liberation_copy, BNDL_F_D(BUNDLED_FONT), BNDL_F_L(BUNDLED_FONT)); ImGuiIO& io = ImGui::GetIO(); ImFont* im_font = io.Fonts->AddFontFromMemoryTTF(liberation_copy, BNDL_F_L(BUNDLED_FONT), 16.0f); /* Generate Binding Widgets */ assert(numRows*numCols == vButtonsN); for (int i = 0; i < vButtonsN; i++) { BindingWidget w(i, p); p->bWidgets.push_back(w); } BDescVec binds; rtData.bindingUpdateMsg.get(binds); p->setupBindingData(binds); p->captureDesc = 0; p->captureName = 0; p->updateDuplicateStatus(); p->redraw(); } SettingsMenu::~SettingsMenu() { ImGui_ImplSdl_Shutdown(); SDL_GL_DeleteContext(p->glContext); SDL_DestroyWindow(p->window); delete p; } bool SettingsMenu::onEvent(const SDL_Event &event) { /* Check for a redraw event first */ if(event.type == EventThread::UPDATE_POPUP + EventThread::UsrIdStart) { if(p->hasFocus) p->redraw(); } /* Check whether this event is for us */ switch (event.type) { case SDL_WINDOWEVENT : case SDL_MOUSEBUTTONDOWN : case SDL_MOUSEBUTTONUP : case SDL_MOUSEMOTION : case SDL_KEYDOWN : case SDL_KEYUP : case SDL_TEXTINPUT : /* We can do this because windowID has the same * struct offset in all these event types */ if (event.window.windowID != p->winID) return false; break; case SDL_JOYBUTTONDOWN : case SDL_JOYBUTTONUP : case SDL_JOYHATMOTION : case SDL_JOYAXISMOTION : if (!p->hasFocus) return false; break; /* Don't try to handle something we don't understand */ default: return false; } /* Pass through event data to ImGUI */ ImGui_ImplSdl_ProcessEvent(event); /* Now process it.. */ switch (event.type) { /* Ignore these event */ case SDL_MOUSEBUTTONUP : case SDL_KEYUP : return true; case SDL_WINDOWEVENT : switch (event.window.event) { case SDL_WINDOWEVENT_SHOWN : // SDL is bugged and doesn't give us a first FOCUS_GAINED event case SDL_WINDOWEVENT_FOCUS_GAINED : p->hasFocus = true; break; case SDL_WINDOWEVENT_FOCUS_LOST : p->hasFocus = false; break; case SDL_WINDOWEVENT_EXPOSED : p->redraw(); break; case SDL_WINDOWEVENT_CLOSE: p->onCancel(); } return true; case SDL_MOUSEMOTION: //p->onMotion(event.motion); return true; case SDL_KEYDOWN: if (p->state != AwaitingInput) { if (event.key.keysym.sym == SDLK_RETURN) p->onAccept(); else if (event.key.keysym.sym == SDLK_ESCAPE) p->onCancel(); return true; } /* Don't let the user bind keys that trigger * mkxp functions */ switch(event.key.keysym.scancode) { case SDL_SCANCODE_F1: case SDL_SCANCODE_F2: case SDL_SCANCODE_F12: return true; default: break; } case SDL_JOYBUTTONDOWN: case SDL_JOYHATMOTION: case SDL_JOYAXISMOTION: if (p->state != AwaitingInput) return true; break; case SDL_MOUSEBUTTONDOWN: return true; default: return true; } if (p->state == AwaitingInput) return p->onCaptureInputEvent(event); return true; } void SettingsMenu::raise() { SDL_RaiseWindow(p->window); } bool SettingsMenu::destroyReq() const { return p->destroyReq; }