diff --git a/src/duckstation-qt/CMakeLists.txt b/src/duckstation-qt/CMakeLists.txt index 628b89bcf..eaaa8e068 100644 --- a/src/duckstation-qt/CMakeLists.txt +++ b/src/duckstation-qt/CMakeLists.txt @@ -33,6 +33,9 @@ add_executable(duckstation-qt gpusettingswidget.ui hotkeysettingswidget.cpp hotkeysettingswidget.h + inputbindingdialog.cpp + inputbindingdialog.h + inputbindingdialog.ui inputbindingwidgets.cpp inputbindingwidgets.h main.cpp diff --git a/src/duckstation-qt/duckstation-qt.vcxproj b/src/duckstation-qt/duckstation-qt.vcxproj index 40e647562..662700f55 100644 --- a/src/duckstation-qt/duckstation-qt.vcxproj +++ b/src/duckstation-qt/duckstation-qt.vcxproj @@ -42,6 +42,7 @@ + @@ -68,6 +69,7 @@ + @@ -148,6 +150,7 @@ + @@ -171,6 +174,11 @@ + + + Document + + diff --git a/src/duckstation-qt/duckstation-qt.vcxproj.filters b/src/duckstation-qt/duckstation-qt.vcxproj.filters index 6ae320217..b96180b5e 100644 --- a/src/duckstation-qt/duckstation-qt.vcxproj.filters +++ b/src/duckstation-qt/duckstation-qt.vcxproj.filters @@ -40,11 +40,13 @@ + + @@ -95,4 +97,7 @@ + + + \ No newline at end of file diff --git a/src/duckstation-qt/inputbindingdialog.cpp b/src/duckstation-qt/inputbindingdialog.cpp new file mode 100644 index 000000000..56b261152 --- /dev/null +++ b/src/duckstation-qt/inputbindingdialog.cpp @@ -0,0 +1,319 @@ +#include "inputbindingdialog.h" +#include "common/bitutils.h" +#include "common/string_util.h" +#include "core/settings.h" +#include "frontend-common/controller_interface.h" +#include "qthostinterface.h" +#include "qtutils.h" +#include +#include +#include +#include +#include + +InputBindingDialog::InputBindingDialog(QtHostInterface* host_interface, std::string section_name, std::string key_name, + std::vector bindings, QWidget* parent) + : QDialog(parent), m_host_interface(host_interface), m_section_name(std::move(section_name)), + m_key_name(std::move(key_name)), m_bindings(std::move(bindings)) +{ + m_ui.setupUi(this); + m_ui.title->setText( + tr("Bindings for %1 %2").arg(QString::fromStdString(m_section_name)).arg(QString::fromStdString(m_key_name))); + + connect(m_ui.addBinding, &QPushButton::clicked, this, &InputBindingDialog::onAddBindingButtonClicked); + connect(m_ui.removeBinding, &QPushButton::clicked, this, &InputBindingDialog::onRemoveBindingButtonClicked); + connect(m_ui.clearBindings, &QPushButton::clicked, this, &InputBindingDialog::onClearBindingsButtonClicked); + connect(m_ui.buttonBox, &QDialogButtonBox::rejected, [this]() { done(0); }); + updateList(); +} + +InputBindingDialog::~InputBindingDialog() +{ + Q_ASSERT(!isListeningForInput()); +} + +bool InputBindingDialog::eventFilter(QObject* watched, QEvent* event) +{ + const QEvent::Type event_type = event->type(); + + if (event_type == QEvent::MouseButtonPress || event_type == QEvent::MouseButtonRelease || + event_type == QEvent::MouseButtonDblClick) + { + return true; + } + + return false; +} + +void InputBindingDialog::onInputListenTimerTimeout() +{ + m_input_listen_remaining_seconds--; + if (m_input_listen_remaining_seconds == 0) + { + stopListeningForInput(); + return; + } + + m_ui.status->setText(tr("Push Button/Axis... [%1]").arg(m_input_listen_remaining_seconds)); +} + +void InputBindingDialog::startListeningForInput(u32 timeout_in_seconds) +{ + m_input_listen_timer = new QTimer(this); + m_input_listen_timer->setSingleShot(false); + m_input_listen_timer->start(1000); + + m_input_listen_timer->connect(m_input_listen_timer, &QTimer::timeout, this, + &InputBindingDialog::onInputListenTimerTimeout); + m_input_listen_remaining_seconds = timeout_in_seconds; + m_ui.status->setText(tr("Push Button/Axis... [%1]").arg(m_input_listen_remaining_seconds)); + m_ui.addBinding->setEnabled(false); + m_ui.removeBinding->setEnabled(false); + m_ui.clearBindings->setEnabled(false); + m_ui.buttonBox->setEnabled(false); + + installEventFilter(this); + grabKeyboard(); + grabMouse(); +} + +void InputBindingDialog::stopListeningForInput() +{ + m_ui.status->clear(); + m_ui.addBinding->setEnabled(true); + m_ui.removeBinding->setEnabled(true); + m_ui.clearBindings->setEnabled(true); + m_ui.buttonBox->setEnabled(true); + + delete m_input_listen_timer; + m_input_listen_timer = nullptr; + + releaseMouse(); + releaseKeyboard(); + removeEventFilter(this); +} + +void InputBindingDialog::addNewBinding(std::string new_binding) +{ + if (std::find(m_bindings.begin(), m_bindings.end(), new_binding) != m_bindings.end()) + return; + + m_ui.bindingList->addItem(QString::fromStdString(new_binding)); + m_bindings.push_back(std::move(new_binding)); + saveListToSettings(); +} + +void InputBindingDialog::onAddBindingButtonClicked() +{ + if (isListeningForInput()) + stopListeningForInput(); + + startListeningForInput(TIMEOUT_FOR_BINDING); +} + +void InputBindingDialog::onRemoveBindingButtonClicked() +{ + const int row = m_ui.bindingList->currentRow(); + if (row < 0 || static_cast(row) >= m_bindings.size()) + return; + + m_bindings.erase(m_bindings.begin() + row); + delete m_ui.bindingList->takeItem(row); + saveListToSettings(); +} + +void InputBindingDialog::onClearBindingsButtonClicked() +{ + m_bindings.clear(); + m_ui.bindingList->clear(); + saveListToSettings(); +} + +void InputBindingDialog::updateList() +{ + m_ui.bindingList->clear(); + for (const std::string& binding : m_bindings) + m_ui.bindingList->addItem(QString::fromStdString(binding)); +} + +void InputBindingDialog::saveListToSettings() +{ + if (!m_bindings.empty()) + m_host_interface->SetStringListSettingValue(m_section_name.c_str(), m_key_name.c_str(), m_bindings); + else + m_host_interface->RemoveSettingValue(m_section_name.c_str(), m_key_name.c_str()); + + m_host_interface->updateInputMap(); +} + +InputButtonBindingDialog::InputButtonBindingDialog(QtHostInterface* host_interface, std::string section_name, + std::string key_name, std::vector bindings, + QWidget* parent) + : InputBindingDialog(host_interface, std::move(section_name), std::move(key_name), std::move(bindings), parent) +{ +} + +InputButtonBindingDialog::~InputButtonBindingDialog() +{ + if (isListeningForInput()) + InputButtonBindingDialog::stopListeningForInput(); +} + +bool InputButtonBindingDialog::eventFilter(QObject* watched, QEvent* event) +{ + const QEvent::Type event_type = event->type(); + + // if the key is being released, set the input + if (event_type == QEvent::KeyRelease) + { + addNewBinding(std::move(m_new_binding_value)); + stopListeningForInput(); + return true; + } + else if (event_type == QEvent::KeyPress) + { + QString binding = QtUtils::KeyEventToString(static_cast(event)); + if (!binding.isEmpty()) + m_new_binding_value = QStringLiteral("Keyboard/%1").arg(binding).toStdString(); + + return true; + } + else if (event_type == QEvent::MouseButtonRelease) + { + const u32 button_mask = static_cast(static_cast(event)->button()); + const u32 button_index = (button_mask == 0u) ? 0 : CountTrailingZeros(button_mask); + m_new_binding_value = StringUtil::StdStringFromFormat("Mouse/Button%d", button_index + 1); + addNewBinding(std::move(m_new_binding_value)); + stopListeningForInput(); + return true; + } + + return InputBindingDialog::eventFilter(watched, event); +} + +void InputButtonBindingDialog::hookControllerInput() +{ + ControllerInterface* controller_interface = m_host_interface->getControllerInterface(); + if (!controller_interface) + return; + + controller_interface->SetHook([this](const ControllerInterface::Hook& ei) { + if (ei.type == ControllerInterface::Hook::Type::Axis) + { + // wait until it's at least half pushed so we don't get confused between axises with small movement + if (std::abs(ei.value) < 0.5f) + return ControllerInterface::Hook::CallbackResult::ContinueMonitoring; + + // TODO: this probably should consider the "last value" + QMetaObject::invokeMethod(this, "bindToControllerAxis", Q_ARG(int, ei.controller_index), + Q_ARG(int, ei.button_or_axis_number), Q_ARG(bool, ei.value > 0)); + return ControllerInterface::Hook::CallbackResult::StopMonitoring; + } + else if (ei.type == ControllerInterface::Hook::Type::Button && ei.value > 0.0f) + { + QMetaObject::invokeMethod(this, "bindToControllerButton", Q_ARG(int, ei.controller_index), + Q_ARG(int, ei.button_or_axis_number)); + return ControllerInterface::Hook::CallbackResult::StopMonitoring; + } + + return ControllerInterface::Hook::CallbackResult::ContinueMonitoring; + }); +} + +void InputButtonBindingDialog::unhookControllerInput() +{ + ControllerInterface* controller_interface = m_host_interface->getControllerInterface(); + if (!controller_interface) + return; + + controller_interface->ClearHook(); +} + +void InputButtonBindingDialog::bindToControllerAxis(int controller_index, int axis_index, bool positive) +{ + std::string binding = + StringUtil::StdStringFromFormat("Controller%d/%cAxis%d", controller_index, positive ? '+' : '-', axis_index); + addNewBinding(std::move(binding)); + stopListeningForInput(); +} + +void InputButtonBindingDialog::bindToControllerButton(int controller_index, int button_index) +{ + std::string binding = StringUtil::StdStringFromFormat("Controller%d/Button%d", controller_index, button_index); + addNewBinding(std::move(binding)); + stopListeningForInput(); +} + +void InputButtonBindingDialog::startListeningForInput(u32 timeout_in_seconds) +{ + InputBindingDialog::startListeningForInput(timeout_in_seconds); + hookControllerInput(); +} + +void InputButtonBindingDialog::stopListeningForInput() +{ + unhookControllerInput(); + InputBindingDialog::stopListeningForInput(); +} + +InputAxisBindingDialog::InputAxisBindingDialog(QtHostInterface* host_interface, std::string section_name, + std::string key_name, std::vector bindings, QWidget* parent) + : InputBindingDialog(host_interface, std::move(section_name), std::move(key_name), std::move(bindings), parent) +{ +} + +InputAxisBindingDialog::~InputAxisBindingDialog() +{ + if (isListeningForInput()) + InputAxisBindingDialog::stopListeningForInput(); +} + +void InputAxisBindingDialog::hookControllerInput() +{ + ControllerInterface* controller_interface = m_host_interface->getControllerInterface(); + if (!controller_interface) + return; + + controller_interface->SetHook([this](const ControllerInterface::Hook& ei) { + if (ei.type == ControllerInterface::Hook::Type::Axis) + { + // wait until it's at least half pushed so we don't get confused between axises with small movement + if (std::abs(ei.value) < 0.5f) + return ControllerInterface::Hook::CallbackResult::ContinueMonitoring; + + QMetaObject::invokeMethod(this, "bindToControllerAxis", Q_ARG(int, ei.controller_index), + Q_ARG(int, ei.button_or_axis_number)); + return ControllerInterface::Hook::CallbackResult::StopMonitoring; + } + + return ControllerInterface::Hook::CallbackResult::ContinueMonitoring; + }); +} + +void InputAxisBindingDialog::unhookControllerInput() +{ + ControllerInterface* controller_interface = m_host_interface->getControllerInterface(); + if (!controller_interface) + return; + + controller_interface->ClearHook(); +} + +void InputAxisBindingDialog::bindToControllerAxis(int controller_index, int axis_index) +{ + std::string binding = StringUtil::StdStringFromFormat("Controller%d/Axis%d", controller_index, axis_index); + addNewBinding(std::move(binding)); + stopListeningForInput(); +} + +void InputAxisBindingDialog::startListeningForInput(u32 timeout_in_seconds) +{ + InputBindingDialog::startListeningForInput(timeout_in_seconds); + hookControllerInput(); +} + +void InputAxisBindingDialog::stopListeningForInput() +{ + unhookControllerInput(); + InputBindingDialog::stopListeningForInput(); +} diff --git a/src/duckstation-qt/inputbindingdialog.h b/src/duckstation-qt/inputbindingdialog.h new file mode 100644 index 000000000..84ee7efe0 --- /dev/null +++ b/src/duckstation-qt/inputbindingdialog.h @@ -0,0 +1,95 @@ +#pragma once +#include "common/types.h" +#include "ui_inputbindingdialog.h" +#include +#include +#include + +class QtHostInterface; + +class InputBindingDialog : public QDialog +{ + Q_OBJECT + +public: + InputBindingDialog(QtHostInterface* host_interface, std::string section_name, std::string key_name, + std::vector bindings, QWidget* parent); + ~InputBindingDialog(); + +protected Q_SLOTS: + void onAddBindingButtonClicked(); + void onRemoveBindingButtonClicked(); + void onClearBindingsButtonClicked(); + void onInputListenTimerTimeout(); + +protected: + enum : u32 + { + TIMEOUT_FOR_BINDING = 5 + }; + + virtual bool eventFilter(QObject* watched, QEvent* event) override; + + virtual void startListeningForInput(u32 timeout_in_seconds); + virtual void stopListeningForInput(); + + bool isListeningForInput() const { return m_input_listen_timer != nullptr; } + void addNewBinding(std::string new_binding); + + void updateList(); + void saveListToSettings(); + + Ui::InputBindingDialog m_ui; + + QtHostInterface* m_host_interface; + + std::string m_section_name; + std::string m_key_name; + std::vector m_bindings; + std::string m_new_binding_value; + + QTimer* m_input_listen_timer = nullptr; + u32 m_input_listen_remaining_seconds = 0; +}; + +class InputButtonBindingDialog : public InputBindingDialog +{ + Q_OBJECT + +public: + InputButtonBindingDialog(QtHostInterface* host_interface, std::string section_name, std::string key_name, + std::vector bindings, QWidget* parent); + ~InputButtonBindingDialog(); + +protected: + bool eventFilter(QObject* watched, QEvent* event) override; + +private Q_SLOTS: + void bindToControllerAxis(int controller_index, int axis_index, bool positive); + void bindToControllerButton(int controller_index, int button_index); + +protected: + void startListeningForInput(u32 timeout_in_seconds) override; + void stopListeningForInput() override; + void hookControllerInput(); + void unhookControllerInput(); +}; + +class InputAxisBindingDialog : public InputBindingDialog +{ + Q_OBJECT + +public: + InputAxisBindingDialog(QtHostInterface* host_interface, std::string section_name, std::string key_name, + std::vector bindings, QWidget* parent); + ~InputAxisBindingDialog(); + +private Q_SLOTS: + void bindToControllerAxis(int controller_index, int axis_index); + +protected: + void startListeningForInput(u32 timeout_in_seconds) override; + void stopListeningForInput() override; + void hookControllerInput(); + void unhookControllerInput(); +}; diff --git a/src/duckstation-qt/inputbindingdialog.ui b/src/duckstation-qt/inputbindingdialog.ui new file mode 100644 index 000000000..e9d720d6e --- /dev/null +++ b/src/duckstation-qt/inputbindingdialog.ui @@ -0,0 +1,89 @@ + + + InputBindingDialog + + + Qt::WindowModal + + + + 0 + 0 + 533 + 283 + + + + Edit Bindings + + + true + + + + + + Bindings for Controller0/ButtonCircle + + + + + + + + + + + + + + + + + + + Add Binding + + + + + + + Remove Binding + + + + + + + Clear Bindings + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + QDialogButtonBox::Close + + + + + + + + + + diff --git a/src/duckstation-qt/inputbindingwidgets.cpp b/src/duckstation-qt/inputbindingwidgets.cpp index a6853c9e3..ec97936dc 100644 --- a/src/duckstation-qt/inputbindingwidgets.cpp +++ b/src/duckstation-qt/inputbindingwidgets.cpp @@ -3,6 +3,7 @@ #include "common/string_util.h" #include "core/settings.h" #include "frontend-common/controller_interface.h" +#include "inputbindingdialog.h" #include "qthostinterface.h" #include "qtutils.h" #include @@ -15,8 +16,9 @@ InputBindingWidget::InputBindingWidget(QtHostInterface* host_interface, std::str : QPushButton(parent), m_host_interface(host_interface), m_section_name(std::move(section_name)), m_key_name(std::move(key_name)) { - m_current_binding_value = m_host_interface->GetStringSettingValue(m_section_name.c_str(), m_key_name.c_str()); - setText(QString::fromStdString(m_current_binding_value)); + m_bindings = m_host_interface->GetSettingStringList(m_section_name.c_str(), m_key_name.c_str()); + updateText(); + setMinimumWidth(150); setMaximumWidth(150); @@ -28,6 +30,16 @@ InputBindingWidget::~InputBindingWidget() Q_ASSERT(!isListeningForInput()); } +void InputBindingWidget::updateText() +{ + if (m_bindings.empty()) + setText(QString()); + else if (m_bindings.size() > 1) + setText(tr("%1 bindings").arg(m_bindings.size())); + else + setText(QString::fromStdString(m_bindings[0])); +} + void InputBindingWidget::beginRebindAll() { m_is_binding_all = true; @@ -50,6 +62,21 @@ bool InputBindingWidget::eventFilter(QObject* watched, QEvent* event) return false; } +bool InputBindingWidget::event(QEvent* event) +{ + if (event->type() == QEvent::MouseButtonRelease) + { + QMouseEvent* mev = static_cast(event); + if (mev->button() == Qt::LeftButton && mev->modifiers() & Qt::ShiftModifier) + { + openDialog(); + return false; + } + } + + return QPushButton::event(event); +} + void InputBindingWidget::mouseReleaseEvent(QMouseEvent* e) { if (e->button() == Qt::RightButton) @@ -69,26 +96,32 @@ void InputBindingWidget::setNewBinding() m_host_interface->SetStringSettingValue(m_section_name.c_str(), m_key_name.c_str(), m_new_binding_value.c_str()); m_host_interface->updateInputMap(); - m_current_binding_value = std::move(m_new_binding_value); - m_new_binding_value.clear(); + m_bindings.clear(); + m_bindings.push_back(std::move(m_new_binding_value)); } void InputBindingWidget::clearBinding() { - m_current_binding_value.clear(); + m_bindings.clear(); m_host_interface->RemoveSettingValue(m_section_name.c_str(), m_key_name.c_str()); m_host_interface->updateInputMap(); - setText(QString::fromStdString(m_current_binding_value)); + updateText(); } void InputBindingWidget::reloadBinding() { - m_current_binding_value = m_host_interface->GetStringSettingValue(m_section_name.c_str(), m_key_name.c_str()); - setText(QString::fromStdString(m_current_binding_value)); + m_bindings = m_host_interface->GetSettingStringList(m_section_name.c_str(), m_key_name.c_str()); + updateText(); } void InputBindingWidget::onClicked() { + if (m_bindings.size() > 1) + { + openDialog(); + return; + } + if (isListeningForInput()) stopListeningForInput(); @@ -125,7 +158,7 @@ void InputBindingWidget::startListeningForInput(u32 timeout_in_seconds) void InputBindingWidget::stopListeningForInput() { - setText(QString::fromStdString(m_current_binding_value)); + updateText(); delete m_input_listen_timer; m_input_listen_timer = nullptr; @@ -138,6 +171,8 @@ void InputBindingWidget::stopListeningForInput() m_is_binding_all = false; } +void InputBindingWidget::openDialog() {} + InputButtonBindingWidget::InputButtonBindingWidget(QtHostInterface* host_interface, std::string section_name, std::string key_name, QWidget* parent) : InputBindingWidget(host_interface, std::move(section_name), std::move(key_name), parent) @@ -247,6 +282,14 @@ void InputButtonBindingWidget::stopListeningForInput() InputBindingWidget::stopListeningForInput(); } +void InputButtonBindingWidget::openDialog() +{ + InputButtonBindingDialog binding_dialog(m_host_interface, m_section_name, m_key_name, m_bindings, + QtUtils::GetRootWidget(this)); + binding_dialog.exec(); + reloadBinding(); +} + InputAxisBindingWidget::InputAxisBindingWidget(QtHostInterface* host_interface, std::string section_name, std::string key_name, QWidget* parent) : InputBindingWidget(host_interface, std::move(section_name), std::move(key_name), parent) @@ -309,6 +352,14 @@ void InputAxisBindingWidget::stopListeningForInput() InputBindingWidget::stopListeningForInput(); } +void InputAxisBindingWidget::openDialog() +{ + InputAxisBindingDialog binding_dialog(m_host_interface, m_section_name, m_key_name, m_bindings, + QtUtils::GetRootWidget(this)); + binding_dialog.exec(); + reloadBinding(); +} + InputRumbleBindingWidget::InputRumbleBindingWidget(QtHostInterface* host_interface, std::string section_name, std::string key_name, QWidget* parent) : InputBindingWidget(host_interface, std::move(section_name), std::move(key_name), parent) diff --git a/src/duckstation-qt/inputbindingwidgets.h b/src/duckstation-qt/inputbindingwidgets.h index 2e826b2e2..9c3bc338d 100644 --- a/src/duckstation-qt/inputbindingwidgets.h +++ b/src/duckstation-qt/inputbindingwidgets.h @@ -34,18 +34,21 @@ protected: }; virtual bool eventFilter(QObject* watched, QEvent* event) override; + virtual bool event(QEvent* event) override; virtual void mouseReleaseEvent(QMouseEvent* e) override; virtual void startListeningForInput(u32 timeout_in_seconds); virtual void stopListeningForInput(); + virtual void openDialog(); bool isListeningForInput() const { return m_input_listen_timer != nullptr; } void setNewBinding(); + void updateText(); QtHostInterface* m_host_interface; std::string m_section_name; std::string m_key_name; - std::string m_current_binding_value; + std::vector m_bindings; std::string m_new_binding_value; QTimer* m_input_listen_timer = nullptr; u32 m_input_listen_remaining_seconds = 0; @@ -59,7 +62,8 @@ class InputButtonBindingWidget : public InputBindingWidget Q_OBJECT public: - InputButtonBindingWidget(QtHostInterface* host_interface, std::string section_name, std::string key_name, QWidget* parent); + InputButtonBindingWidget(QtHostInterface* host_interface, std::string section_name, std::string key_name, + QWidget* parent); ~InputButtonBindingWidget(); protected: @@ -72,6 +76,7 @@ private Q_SLOTS: protected: void startListeningForInput(u32 timeout_in_seconds) override; void stopListeningForInput() override; + void openDialog() override; void hookControllerInput(); void unhookControllerInput(); }; @@ -81,7 +86,8 @@ class InputAxisBindingWidget : public InputBindingWidget Q_OBJECT public: - InputAxisBindingWidget(QtHostInterface* host_interface, std::string section_name, std::string key_name, QWidget* parent); + InputAxisBindingWidget(QtHostInterface* host_interface, std::string section_name, std::string key_name, + QWidget* parent); ~InputAxisBindingWidget(); private Q_SLOTS: @@ -90,6 +96,7 @@ private Q_SLOTS: protected: void startListeningForInput(u32 timeout_in_seconds) override; void stopListeningForInput() override; + void openDialog() override; void hookControllerInput(); void unhookControllerInput(); }; @@ -99,7 +106,8 @@ class InputRumbleBindingWidget : public InputBindingWidget Q_OBJECT public: - InputRumbleBindingWidget(QtHostInterface* host_interface, std::string section_name, std::string key_name, QWidget* parent); + InputRumbleBindingWidget(QtHostInterface* host_interface, std::string section_name, std::string key_name, + QWidget* parent); ~InputRumbleBindingWidget(); private Q_SLOTS: diff --git a/src/duckstation-qt/settingsdialog.cpp b/src/duckstation-qt/settingsdialog.cpp index d1ab694bb..fa749b867 100644 --- a/src/duckstation-qt/settingsdialog.cpp +++ b/src/duckstation-qt/settingsdialog.cpp @@ -25,12 +25,14 @@ static constexpr std::array(SettingsDialog::Catego "Hotkey Settings
Binding a hotkey allows you to trigger events such as a resetting or taking " "screenshots at the press of a key/controller button. Hotkey titles are self-explanatory. Clicking a binding will " "start a countdown, in which case you should press the key or controller button/axis you wish to bind. If no button " - "is pressed and the timer lapses, the binding will be unchanged. To clear a binding, right-click the button.", + "is pressed and the timer lapses, the binding will be unchanged. To clear a binding, right-click the button. To " + "bind multiple buttons, hold Shift and click the button.", "Controller Settings
This page lets you choose the type of controller you wish to simulate for " "the console, and rebind the keys or host game controller buttons to your choosing. Clicking a binding will start a " "countdown, in which case you should press the key or controller button/axis you wish to bind. (For rumble, press " "any button/axis on the controller you wish to send rumble to.) If no button is pressed and the timer lapses, " - "the binding will be unchanged. To clear a binding, right-click the button.", + "the binding will be unchanged. To clear a binding, right-click the button. To bind multiple buttons, hold Shift " + "and click the button.", "Memory Card Settings
This page lets you control what mode the memory card emulation will " "function in, and where the images for these cards will be stored on disk.", "GPU Settings
These options control the simulation of the GPU in the console. Various "