// SPDX-License-Identifier: MIT // // EmulationStation Desktop Edition // GuiInputConfig.cpp // // Input device configuration GUI (for keyboards, joysticks and gamepads). // #include "guis/GuiInputConfig.h" #include "components/ButtonComponent.h" #include "components/MenuComponent.h" #include "guis/GuiMsgBox.h" #include "InputManager.h" #include "Log.h" #include "Window.h" struct InputConfigStructure { std::string name; const bool skippable; std::string dispName; std::string icon; }; static const int inputCount = 22; static const InputConfigStructure GUI_INPUT_CONFIG_LIST[inputCount] = { { "Up", false, "D-PAD UP", ":/help/dpad_up.svg" }, { "Down", false, "D-PAD DOWN", ":/help/dpad_down.svg" }, { "Left", false, "D-PAD LEFT", ":/help/dpad_left.svg" }, { "Right", false, "D-PAD RIGHT", ":/help/dpad_right.svg" }, { "Start", true, "START", ":/help/button_start.svg" }, { "Select", true, "SELECT", ":/help/button_select.svg" }, { "A", false, "BUTTON A / EAST", ":/help/buttons_east.svg" }, { "B", true, "BUTTON B / SOUTH", ":/help/buttons_south.svg" }, { "X", true, "BUTTON X / NORTH", ":/help/buttons_north.svg" }, { "Y", true, "BUTTON Y / WEST", ":/help/buttons_west.svg" }, { "LeftShoulder", true, "LEFT SHOULDER", ":/help/button_l.svg" }, { "RightShoulder", true, "RIGHT SHOULDER", ":/help/button_r.svg" }, { "LeftTrigger", true, "LEFT TRIGGER", ":/help/button_lt.svg" }, { "RightTrigger", true, "RIGHT TRIGGER", ":/help/button_rt.svg" }, // { "LeftThumb", true, "LEFT THUMB", ":/help/analog_thumb.svg" }, // { "RightThumb", true, "RIGHT THUMB", ":/help/analog_thumb.svg" }, { "LeftAnalogUp", true, "LEFT ANALOG UP", ":/help/analog_up.svg" }, { "LeftAnalogDown", true, "LEFT ANALOG DOWN", ":/help/analog_down.svg" }, { "LeftAnalogLeft", true, "LEFT ANALOG LEFT", ":/help/analog_left.svg" }, { "LeftAnalogRight", true, "LEFT ANALOG RIGHT", ":/help/analog_right.svg" }, { "RightAnalogUp", true, "RIGHT ANALOG UP", ":/help/analog_up.svg" }, { "RightAnalogDown", true, "RIGHT ANALOG DOWN", ":/help/analog_down.svg" }, { "RightAnalogLeft", true, "RIGHT ANALOG LEFT", ":/help/analog_left.svg" }, { "RightAnalogRight", true, "RIGHT ANALOG RIGHT", ":/help/analog_right.svg" }, // { "HotKeyEnable", true, "HOTKEY ENABLE", ":/help/button_hotkey.svg" } }; // MasterVolUp and MasterVolDown are also hooked up, but do not appear on this screen. // If you want, you can manually add them to es_input.cfg. #define HOLD_TO_SKIP_MS 1000 GuiInputConfig::GuiInputConfig( Window* window, InputConfig* target, bool reconfigureAll, const std::function<void()>& okCallback) : GuiComponent(window), mBackground(window, ":/graphics/frame.png"), mGrid(window, Vector2i(1, 7)), mTargetConfig(target), mHoldingInput(false), mBusyAnim(window) { 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<GuiComponent>(mWindow), Vector2i(0, 0), false); mTitle = std::make_shared<TextComponent>(mWindow, "CONFIGURING", Font::get(FONT_SIZE_LARGE), 0x555555FF, ALIGN_CENTER); mGrid.setEntry(mTitle, Vector2i(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); mSubtitle1 = std::make_shared<TextComponent>(mWindow, Utils::String::toUpper(ss.str()), Font::get(FONT_SIZE_MEDIUM), 0x555555FF, ALIGN_CENTER); mGrid.setEntry(mSubtitle1, Vector2i(0, 2), false, true); mSubtitle2 = std::make_shared<TextComponent>(mWindow, "HOLD ANY BUTTON TO SKIP", Font::get(FONT_SIZE_SMALL), 0x999999FF, ALIGN_CENTER); mGrid.setEntry(mSubtitle2, Vector2i(0, 3), false, true); // 4 is a spacer row. mList = std::make_shared<ComponentList>(mWindow); mGrid.setEntry(mList, Vector2i(0, 5), true, true); for (int i = 0; i < inputCount; i++) { ComponentListRow row; // Icon. auto icon = std::make_shared<ImageComponent>(mWindow); icon->setImage(GUI_INPUT_CONFIG_LIST[i].icon); icon->setColorShift(0x777777FF); icon->setResize(0, Font::get(FONT_SIZE_MEDIUM)->getLetterHeight() * 1.25f); row.addElement(icon, false); // Spacer between icon and text. auto spacer = std::make_shared<GuiComponent>(mWindow); spacer->setSize(16, 0); row.addElement(spacer, false); auto text = std::make_shared<TextComponent>(mWindow, GUI_INPUT_CONFIG_LIST[i].dispName, Font::get(FONT_SIZE_MEDIUM), 0x777777FF); row.addElement(text, true); auto mapping = std::make_shared<TextComponent>(mWindow, "-NOT DEFINED-", Font::get(FONT_SIZE_MEDIUM, FONT_PATH_LIGHT), 0x999999FF, ALIGN_RIGHT); setNotDefined(mapping); // Overrides the text and color set above. row.addElement(mapping, true); mMappings.push_back(mapping); row.input_handler = [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 they didn't press A to start, so ignore this. return false; } // Apply filtering for quirks related to trigger mapping. if (filterTrigger(input, config, i)) return false; // We are configuring. if (input.value != 0) { // Input down. // 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 { // Input up. // 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 /*state*/) { bool skippable = GUI_INPUT_CONFIG_LIST[mList->getCursorId()].skippable; mSubtitle2->setOpacity(skippable * 255); }); // Make the first one say "PRESS ANYTHING" if we're re-configuring everything. if (mConfiguringAll) setPress(mMappings.front()); // Buttons. std::vector< std::shared_ptr<ButtonComponent> > buttons; std::function<void()> okFunction = [this, okCallback] { // If we have just configured the keyboard, then unset the flag to indicate that // we are using the default keyboard mappings. if (mTargetConfig->getDeviceId() == DEVICE_KEYBOARD) { InputManager::getInstance()-> getInputConfigByDevice(DEVICE_KEYBOARD)->unsetDefaultConfigFlag(); } InputManager::getInstance()->writeDeviceConfig(mTargetConfig); // Save. if (okCallback) okCallback(); delete this; }; buttons.push_back(std::make_shared<ButtonComponent> (mWindow, "OK", "ok", [this, okFunction] { okFunction(); })); // This code is disabled as there is no intention to provide emulator configuration or // control via ES Desktop Edition. Let's keep the code for reference though. // buttons.push_back(std::make_shared<ButtonComponent>(mWindow, "OK", "ok", [this, okFunction] { // // Check if the hotkey enable button is set. if not prompt the // // user to use select or nothing. // Input input; // okFunction(); // if (!mTargetConfig->getInputByName("HotKeyEnable", &input)) { // mWindow->pushGui(new GuiMsgBox(mWindow, getHelpStyle(), // "YOU DIDN'T CHOOSE A HOTKEY ENABLE BUTTON. THIS IS REQUIRED FOR EXITING " // "GAMES WITH A CONTROLLER. DO YOU WANT TO USE THE SELECT BUTTON DEFAULT ? " // "PLEASE ANSWER YES TO USE SELECT OR NO TO NOT SET A HOTKEY ENABLE BUTTON.", // "YES", [this, okFunction] { // Input input; // mTargetConfig->getInputByName("Select", &input); // mTargetConfig->mapInput("HotKeyEnable", input); // okFunction(); // }, // "NO", [this, okFunction] { // // For a disabled hotkey enable button, set to a key with id 0, // // so the input configuration script can be backwards compatible. // mTargetConfig->mapInput("HotKeyEnable", Input(DEVICE_KEYBOARD, // TYPE_KEY, 0, 1, true)); // okFunction(); // })); // } // else { // okFunction(); // } // })); mButtonGrid = makeButtonGrid(mWindow, buttons); mGrid.setEntry(mButtonGrid, Vector2i(0, 6), true, false); setSize(Renderer::getScreenWidth() * 0.6f, Renderer::getScreenHeight() * 0.75f); setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, (Renderer::getScreenHeight() - mSize.y()) / 2); } void GuiInputConfig::onSizeChanged() { mBackground.fitTo(mSize, Vector3f::Zero(), Vector2f(-32, -32)); // Update grid. mGrid.setSize(mSize); //mGrid.setRowHeightPerc(0, 0.025f); 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(4, 0.03f); mGrid.setRowHeightPerc(5, (mList->getRowHeight(0) * 5 + 2) / mSize.y()); mGrid.setRowHeightPerc(6, mButtonGrid->getSize().y() / mSize.y()); mBusyAnim.setSize(mSize); } void GuiInputConfig::update(int deltaTime) { if (mConfiguringRow && mHoldingInput && GUI_INPUT_CONFIG_LIST[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(0x777777FF); } } } } // Move cursor to the next thing if we're configuring all, // or come out of "configure mode" if we were only configuring one row. void GuiInputConfig::rowDone() { if (mConfiguringAll) { // Try to move to the next one. if (!mList->moveCursor(1)) { // At bottom of list, we're done. mConfiguringAll = false; mConfiguringRow = false; mGrid.moveCursor(Vector2i(0, 1)); } else { // On another one. setPress(mMappings.at(mList->getCursorId())); } } else { // Only configuring one row, so stop. mConfiguringRow = false; } } void GuiInputConfig::setPress(const std::shared_ptr<TextComponent>& text) { text->setText("PRESS ANYTHING"); text->setColor(0x656565FF); } void GuiInputConfig::setNotDefined(const std::shared_ptr<TextComponent>& text) { text->setText("-NOT DEFINED-"); text->setColor(0x999999FF); } void GuiInputConfig::setAssignedTo(const std::shared_ptr<TextComponent>& text, Input input) { text->setText(Utils::String::toUpper(input.string())); text->setColor(0x777777FF); } void GuiInputConfig::error(const std::shared_ptr<TextComponent>& text, const std::string& /*msg*/) { text->setText("ALREADY TAKEN"); text->setColor(0x656565FF); } 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(GUI_INPUT_CONFIG_LIST[inputId].name, input) && GUI_INPUT_CONFIG_LIST[inputId].name != "HotKeyEnable") { error(mMappings.at(inputId), "Already mapped!"); return false; } setAssignedTo(mMappings.at(inputId), input); input.configured = true; mTargetConfig->mapInput(GUI_INPUT_CONFIG_LIST[inputId].name, input); LOG(LogInfo) << "Mapping [" << input.string() << "] to [" << GUI_INPUT_CONFIG_LIST[inputId].name << "]"; return true; } void GuiInputConfig::clearAssignment(int inputId) { mTargetConfig->unmapInput(GUI_INPUT_CONFIG_LIST[inputId].name); } bool GuiInputConfig::filterTrigger(Input input, InputConfig* config, int inputId) { #if defined(__linux__) || defined(__APPLE__) // On Linux and macOS, some gamepads return both an analog axis and a digital button for // the trigger; we want the analog axis only, so this function removes the button press event. // This is relevant mostly for Sony Dual Shock controllers. if (InputManager::getInstance()->getAxisCountByDevice(config->getDeviceId()) == 6) { if (config->getDeviceName().find("PLAYSTATION") != std::string::npos || config->getDeviceName().find("PS3 Ga") != std::string::npos || config->getDeviceName().find("PS(R) Ga") != std::string::npos || config->getDeviceName().find("PS4 Controller") != std::string::npos || config->getDeviceName().find("Sony Interactive") != std::string::npos || // BigBen kid's PS3 gamepad 146b:0902, matched on SDL GUID because its name // "Bigben Interactive Bigben Game Pad" may be too generic. config->getDeviceGUIDString().find("030000006b1400000209000011010000") != std::string::npos ) { // Remove digital trigger events. if (input.type == TYPE_BUTTON && (input.id == 6 || input.id == 7)) { mHoldingInput = false; return true; } } } #endif // Ignore negative poles when triggers are being configured. // This is not a good solution as it's hardcoded to input 2 and 5 (Xbox controllers) and // input 4 and 5 (Playstation Dual Shock controllers) instead of using a general detection // for which type of axis input is used. This is also hardcoded to only work when configuring // the trigger buttons, so it will not be possible to map trigger buttons to the shoulder // button functions in ES for instance. It's probably necessary to update ES to use the SDL // GameController API to fix this properly. if (input.type == TYPE_AXIS && (input.id == 2 || input.id == 4 || input.id == 5)) { if (!(std::string(GUI_INPUT_CONFIG_LIST[inputId].name).find("Trigger") != std::string::npos)) { return false; } else if (std::string(GUI_INPUT_CONFIG_LIST[inputId].name).find("Trigger") != std::string::npos) { if (input.value == 1) mSkipAxis = true; else if (input.value == -1) return true; } else if (mSkipAxis) { mSkipAxis = false; return true; } } // (void)input; // (void)config; // (void)inputId; return false; }