// SPDX-License-Identifier: MIT // // ES-DE // GuiInputConfig.cpp // // Input device configuration GUI (for keyboards, joysticks and gamepads). // #include "guis/GuiInputConfig.h" #include "InputManager.h" #include "Log.h" #include "components/ButtonComponent.h" #include "components/MenuComponent.h" #include "guis/GuiMsgBox.h" #define HOLD_TO_SKIP_MS 1000 struct InputConfigStructure { std::string name; bool skippable; std::string dispName; std::string icon; }; static const int inputCount = 24; static InputConfigStructure sGuiInputConfigList[inputCount]; GuiInputConfig::GuiInputConfig(InputConfig* target, bool reconfigureAll, const std::function& okCallback) : mBackground {":/graphics/frame.svg"} , mGrid {glm::ivec2 {1, 7}} , mTargetConfig {target} , mHoldingInput {false} { // Populate the configuration list with the text and icons applicable to the // configured controller type. populateConfigList(); LOG(LogInfo) << "Configuring device " << target->getDeviceId() << " (" << target->getDeviceName() << ")."; if (reconfigureAll) target->clear(); mConfiguringAll = reconfigureAll; mConfiguringRow = mConfiguringAll; addChild(&mBackground); addChild(&mGrid); // 0 is a spacer row. mGrid.setEntry(std::make_shared(), glm::ivec2 {0, 0}, false); mTitle = std::make_shared("CONFIGURING", Font::get(FONT_SIZE_LARGE), mMenuColorTitle, ALIGN_CENTER); mGrid.setEntry(mTitle, glm::ivec2 {0, 1}, false, true); std::stringstream ss; if (target->getDeviceId() == DEVICE_KEYBOARD) ss << "KEYBOARD"; else if (target->getDeviceId() == DEVICE_CEC) ss << "CEC"; else ss << "GAMEPAD " << (target->getDeviceId() + 1) << " (" << target->getDeviceName() << ")"; mSubtitle1 = std::make_shared(Utils::String::toUpper(ss.str()), Font::get(FONT_SIZE_MEDIUM), mMenuColorPrimary, ALIGN_CENTER); mGrid.setEntry(mSubtitle1, glm::ivec2 {0, 2}, false, true); mSubtitle2 = std::make_shared("HOLD ANY BUTTON 1 SECOND TO SKIP", Font::get(FONT_SIZE_SMALL), mMenuColorSecondary, ALIGN_CENTER); // The opacity will be set to visible for any row that is skippable. mSubtitle2->setOpacity(0.0f); mGrid.setEntry(mSubtitle2, glm::ivec2 {0, 3}, false, true); // 4 is a spacer row. mList = std::make_shared(); mGrid.setEntry(mList, glm::ivec2 {0, 5}, true, true); for (int i {0}; i < inputCount; ++i) { ComponentListRow row; // Icon. auto icon = std::make_shared(); icon->setResize(0, Font::get(FONT_SIZE_MEDIUM)->getLetterHeight() * 1.25f); icon->setImage(sGuiInputConfigList[i].icon); icon->setColorShift(mMenuColorPrimary); row.addElement(icon, false); // Spacer between icon and text. auto spacer = std::make_shared(); spacer->setSize(16, 0); row.addElement(spacer, false); auto text = std::make_shared(sGuiInputConfigList[i].dispName, Font::get(FONT_SIZE_MEDIUM), mMenuColorPrimary); row.addElement(text, true); auto mapping = std::make_shared("-NOT DEFINED-", Font::get(FONT_SIZE_MEDIUM, FONT_PATH_LIGHT), mMenuColorSecondary, ALIGN_RIGHT); setNotDefined(mapping); // Overrides the text and color set above. row.addElement(mapping, true); mMappings.push_back(mapping); row.inputHandler = [this, i, mapping](InputConfig* config, Input input) -> bool { // Ignore input not from our target device. if (config != mTargetConfig) return false; // If we're not configuring, start configuring when A is pressed. if (!mConfiguringRow) { if (config->isMappedTo("a", input) && input.value) { mList->stopScrolling(); mConfiguringRow = true; setPress(mapping); return true; } // We're not configuring and A wasn't pressed to start, so ignore this input. return false; } // We are configuring. if (input.value != 0) { // Button pressed. If we're already holding something, ignore this, // otherwise plan to map this input. if (mHoldingInput) return true; mHoldingInput = true; mHeldInput = input; mHeldTime = 0; mHeldInputId = i; return true; } else { // Button released. Make sure we were holding something and we let go of // what we were previously holding. if (!mHoldingInput || mHeldInput.device != input.device || mHeldInput.id != input.id || mHeldInput.type != input.type) { return true; } mHoldingInput = false; if (assign(mHeldInput, i)) // If successful, move cursor/stop configuring - if not, we'll just try again. rowDone(); return true; } }; mList->addRow(row); } // Only show "HOLD TO SKIP" if this input is skippable. mList->setCursorChangedCallback([this](CursorState) { if (sGuiInputConfigList[mList->getCursorId()].skippable) mSubtitle2->setOpacity(1.0f); else mSubtitle2->setOpacity(0.0f); }); // Make the first one say "PRESS ANYTHING" if we're re-configuring everything. if (mConfiguringAll) setPress(mMappings.front()); // GUI buttons. std::vector> buttons; std::function okFunction {[this, okCallback] { InputManager::getInstance().writeDeviceConfig(mTargetConfig); // Save. if (okCallback) okCallback(); delete this; }}; buttons.push_back( std::make_shared("OK", "ok", [okFunction] { okFunction(); })); mButtonGrid = MenuComponent::makeButtonGrid(buttons); mGrid.setEntry(mButtonGrid, glm::ivec2 {0, 6}, true, false); // Adjust the width relative to the aspect ratio of the screen to make the GUI look coherent // regardless of screen type. The 1.778 aspect ratio value is the 16:9 reference. float aspectValue {1.778f / Renderer::getScreenAspectRatio()}; float width {glm::clamp(0.60f * aspectValue, 0.50f, 0.80f) * Renderer::getScreenWidth()}; setSize(width, (Renderer::getIsVerticalOrientation() ? Renderer::getScreenWidth() * 0.75f : Renderer::getScreenHeight() * 0.75f)); setPosition((Renderer::getScreenWidth() - mSize.x) / 2.0f, (Renderer::getScreenHeight() - mSize.y) / 2.0f); } void GuiInputConfig::populateConfigList() { std::string controllerType {Settings::getInstance()->getString("InputControllerType")}; // clang-format off sGuiInputConfigList[0] = {"Up", false, "D-PAD UP", ":/graphics/help/dpad_up.svg"}; sGuiInputConfigList[1] = {"Down", false, "D-PAD DOWN", ":/graphics/help/dpad_down.svg"}; sGuiInputConfigList[2] = {"Left", false, "D-PAD LEFT", ":/graphics/help/dpad_left.svg"}; sGuiInputConfigList[3] = {"Right", false, "D-PAD RIGHT", ":/graphics/help/dpad_right.svg"}; if (controllerType == "snes") { sGuiInputConfigList[4] = {"Back", false, "SELECT", ":/graphics/help/button_back_SNES.svg"}; sGuiInputConfigList[5] = {"Start", false, "START", ":/graphics/help/button_start_SNES.svg"}; sGuiInputConfigList[6] = {"A", false, "B", ":/graphics/help/mbuttons_b_SNES.svg"}; sGuiInputConfigList[7] = {"B", false, "A", ":/graphics/help/mbuttons_a_SNES.svg"}; sGuiInputConfigList[8] = {"X", true, "Y", ":/graphics/help/mbuttons_y_SNES.svg"}; sGuiInputConfigList[9] = {"Y", true, "X", ":/graphics/help/mbuttons_x_SNES.svg"}; } else if (controllerType == "switchpro") { sGuiInputConfigList[4] = {"Back", false, "MINUS", ":/graphics/help/button_back_switch.svg"}; sGuiInputConfigList[5] = {"Start", false, "PLUS", ":/graphics/help/button_start_switch.svg"}; sGuiInputConfigList[7] = {"A", false, "A", ":/graphics/help/mbuttons_a_switch.svg"}; sGuiInputConfigList[6] = {"B", false, "B", ":/graphics/help/mbuttons_b_switch.svg"}; sGuiInputConfigList[9] = {"X", true, "X", ":/graphics/help/mbuttons_x_switch.svg"}; sGuiInputConfigList[8] = {"Y", true, "Y", ":/graphics/help/mbuttons_y_switch.svg"}; } else if (controllerType == "ps123") { sGuiInputConfigList[4] = {"Back", false, "SELECT", ":/graphics/help/button_back_PS123.svg"}; sGuiInputConfigList[5] = {"Start", false, "START", ":/graphics/help/button_start_PS123.svg"}; sGuiInputConfigList[6] = {"A", false, "CROSS", ":/graphics/help/mbuttons_a_PS.svg"}; sGuiInputConfigList[7] = {"B", false, "CIRCLE", ":/graphics/help/mbuttons_b_PS.svg"}; sGuiInputConfigList[8] = {"X", true, "SQUARE", ":/graphics/help/mbuttons_x_PS.svg"}; sGuiInputConfigList[9] = {"Y", true, "TRIANGLE", ":/graphics/help/mbuttons_y_PS.svg"}; } else if (controllerType == "ps4") { sGuiInputConfigList[4] = {"Back", false, "SHARE", ":/graphics/help/button_back_PS4.svg"}; sGuiInputConfigList[5] = {"Start", false, "OPTIONS", ":/graphics/help/button_start_PS4.svg"}; sGuiInputConfigList[6] = {"A", false, "CROSS", ":/graphics/help/mbuttons_a_PS.svg"}; sGuiInputConfigList[7] = {"B", false, "CIRCLE", ":/graphics/help/mbuttons_b_PS.svg"}; sGuiInputConfigList[8] = {"X", true, "SQUARE", ":/graphics/help/mbuttons_x_PS.svg"}; sGuiInputConfigList[9] = {"Y", true, "TRIANGLE", ":/graphics/help/mbuttons_y_PS.svg"}; } else if (controllerType == "ps5") { sGuiInputConfigList[4] = {"Back", false, "CREATE", ":/graphics/help/button_back_PS5.svg"}; sGuiInputConfigList[5] = {"Start", false, "OPTIONS", ":/graphics/help/button_start_PS5.svg"}; sGuiInputConfigList[6] = {"A", false, "CROSS", ":/graphics/help/mbuttons_a_PS.svg"}; sGuiInputConfigList[7] = {"B", false, "CIRCLE", ":/graphics/help/mbuttons_b_PS.svg"}; sGuiInputConfigList[8] = {"X", true, "SQUARE", ":/graphics/help/mbuttons_x_PS.svg"}; sGuiInputConfigList[9] = {"Y", true, "TRIANGLE", ":/graphics/help/mbuttons_y_PS.svg"}; } else if (controllerType == "xbox360") { sGuiInputConfigList[4] = {"Back", false, "BACK", ":/graphics/help/button_back_XBOX360.svg"}; sGuiInputConfigList[5] = {"Start", false, "START", ":/graphics/help/button_start_XBOX360.svg"}; sGuiInputConfigList[6] = {"A", false, "A", ":/graphics/help/mbuttons_a_XBOX.svg"}; sGuiInputConfigList[7] = {"B", false, "B", ":/graphics/help/mbuttons_b_XBOX.svg"}; sGuiInputConfigList[8] = {"X", true, "X", ":/graphics/help/mbuttons_x_XBOX.svg"}; sGuiInputConfigList[9] = {"Y", true, "Y", ":/graphics/help/mbuttons_y_XBOX.svg"}; } else { // Xbox One and later. sGuiInputConfigList[4] = {"Back", false, "VIEW", ":/graphics/help/button_back_XBOX.svg"}; sGuiInputConfigList[5] = {"Start", false, "MENU", ":/graphics/help/button_start_XBOX.svg"}; sGuiInputConfigList[6] = {"A", false, "A", ":/graphics/help/mbuttons_a_XBOX.svg"}; sGuiInputConfigList[7] = {"B", false, "B", ":/graphics/help/mbuttons_b_XBOX.svg"}; sGuiInputConfigList[8] = {"X", true, "X", ":/graphics/help/mbuttons_x_XBOX.svg"}; sGuiInputConfigList[9] = {"Y", true, "Y", ":/graphics/help/mbuttons_y_XBOX.svg"}; } sGuiInputConfigList[10] = {"LeftShoulder", true, "LEFT SHOULDER", ":/graphics/help/button_l.svg"}; sGuiInputConfigList[11] = {"RightShoulder", true, "RIGHT SHOULDER", ":/graphics/help/button_r.svg"}; sGuiInputConfigList[12] = {"LeftTrigger", true, "LEFT TRIGGER", ":/graphics/help/button_lt.svg"}; sGuiInputConfigList[13] = {"RightTrigger", true, "RIGHT TRIGGER", ":/graphics/help/button_rt.svg"}; sGuiInputConfigList[14] = {"LeftThumbstickUp", true, "LEFT THUMBSTICK UP", ":/graphics/help/thumbstick_up.svg"}; sGuiInputConfigList[15] = {"LeftThumbstickDown", true, "LEFT THUMBSTICK DOWN", ":/graphics/help/thumbstick_down.svg"}; sGuiInputConfigList[16] = {"LeftThumbstickLeft", true, "LEFT THUMBSTICK LEFT", ":/graphics/help/thumbstick_left.svg"}; sGuiInputConfigList[17] = {"LeftThumbstickRight", true, "LEFT THUMBSTICK RIGHT", ":/graphics/help/thumbstick_right.svg"}; sGuiInputConfigList[18] = {"LeftThumbstickClick", true, "LEFT THUMBSTICK CLICK", ":/graphics/help/thumbstick_click.svg"}; sGuiInputConfigList[19] = {"RightThumbstickUp", true, "RIGHT THUMBSTICK UP", ":/graphics/help/thumbstick_up.svg"}; sGuiInputConfigList[20] = {"RightThumbstickDown", true, "RIGHT THUMBSTICK DOWN", ":/graphics/help/thumbstick_down.svg"}; sGuiInputConfigList[21] = {"RightThumbstickLeft", true, "RIGHT THUMBSTICK LEFT", ":/graphics/help/thumbstick_left.svg"}; sGuiInputConfigList[22] = {"RightThumbstickRight", true, "RIGHT THUMBSTICK RIGHT", ":/graphics/help/thumbstick_right.svg"}; sGuiInputConfigList[23] = {"RightThumbstickClick", true, "RIGHT THUMBSTICK CLICK", ":/graphics/help/thumbstick_click.svg"}; // clang-format on } void GuiInputConfig::update(int deltaTime) { if (mConfiguringRow && mHoldingInput && sGuiInputConfigList[mHeldInputId].skippable) { int prevSec {mHeldTime / 1000}; mHeldTime += deltaTime; int curSec {mHeldTime / 1000}; if (mHeldTime >= HOLD_TO_SKIP_MS) { setNotDefined(mMappings.at(mHeldInputId)); clearAssignment(mHeldInputId); mHoldingInput = false; rowDone(); } else { if (prevSec != curSec) { // Crossed the second boundary, update text. const auto& text = mMappings.at(mHeldInputId); std::stringstream ss; ss << "HOLD FOR " << HOLD_TO_SKIP_MS / 1000 - curSec << "S TO SKIP"; text->setText(ss.str()); text->setColor(mMenuColorPrimary); } } } } void GuiInputConfig::onSizeChanged() { mBackground.fitTo(mSize); // Update grid. mGrid.setSize(mSize); mGrid.setRowHeightPerc(1, mTitle->getFont()->getHeight() * 0.75f / mSize.y); mGrid.setRowHeightPerc(2, mSubtitle1->getFont()->getHeight() / mSize.y); mGrid.setRowHeightPerc(3, mSubtitle2->getFont()->getHeight() / mSize.y); mGrid.setRowHeightPerc(5, (mList->getRowHeight() * 5.0f + 2.0f) / mSize.y); mGrid.setRowHeightPerc(6, mButtonGrid->getSize().y / mSize.y); } void GuiInputConfig::rowDone() { if (mConfiguringAll) { // Try to move to the next row. if (!mList->moveCursor(1)) { // At bottom of list, we're done. mConfiguringAll = false; mConfiguringRow = false; mGrid.moveCursor(glm::ivec2 {0, 1}); } else { // On another row. setPress(mMappings.at(mList->getCursorId())); } } else { // Only configuring one row, so stop. mConfiguringRow = false; } } void GuiInputConfig::error(const std::shared_ptr& text, const std::string& /*msg*/) { text->setText("ALREADY TAKEN"); text->setColor(mMenuColorPrimary); } void GuiInputConfig::setPress(const std::shared_ptr& text) { text->setText("PRESS ANYTHING"); text->setColor(mMenuColorPrimary); } void GuiInputConfig::setNotDefined(const std::shared_ptr& text) { text->setText("-NOT DEFINED-"); text->setColor(mMenuColorSecondary); } void GuiInputConfig::setAssignedTo(const std::shared_ptr& text, Input input) { text->setText(Utils::String::toUpper(input.string())); text->setColor(mMenuColorPrimary); } bool GuiInputConfig::assign(Input input, int inputId) { // Input is from InputConfig* mTargetConfig. // If this input is mapped to something other than "nothing" or the current row, // generate an error. (If it's the same as what it was before, allow it.) if (mTargetConfig->getMappedTo(input).size() > 0 && !mTargetConfig->isMappedTo(sGuiInputConfigList[inputId].name, input) && sGuiInputConfigList[inputId].name != "HotKeyEnable") { error(mMappings.at(inputId), "Already mapped!"); return false; } setAssignedTo(mMappings.at(inputId), input); input.configured = true; mTargetConfig->mapInput(sGuiInputConfigList[inputId].name, input); LOG(LogInfo) << "Mapping [" << input.string() << "] to [" << sGuiInputConfigList[inputId].name << "]"; return true; } void GuiInputConfig::clearAssignment(int inputId) { mTargetConfig->unmapInput(sGuiInputConfigList[inputId].name); }