From 43ead51b50f58e48ff2be44a3ad30f1a35bc9fad Mon Sep 17 00:00:00 2001 From: David Griswold Date: Fri, 6 Feb 2026 21:12:27 +0300 Subject: [PATCH] configuration dialog for controller hotkeys --- src/citra_qt/CMakeLists.txt | 2 + .../configure_hotkeys_controller.cpp | 26 ++++- .../configure_hotkeys_controller.h | 2 +- .../configure_hotkeys_controller.ui | 2 + .../configuration/configure_input.cpp | 3 +- .../controller_sequence_dialog.cpp | 96 +++++++++++++++++++ .../controller_sequence_dialog.h | 31 ++++++ src/input_common/sdl/sdl_impl.cpp | 24 +++-- 8 files changed, 173 insertions(+), 13 deletions(-) create mode 100644 src/citra_qt/util/sequence_dialog/controller_sequence_dialog.cpp create mode 100644 src/citra_qt/util/sequence_dialog/controller_sequence_dialog.h diff --git a/src/citra_qt/CMakeLists.txt b/src/citra_qt/CMakeLists.txt index a46532bba..d395adf23 100644 --- a/src/citra_qt/CMakeLists.txt +++ b/src/citra_qt/CMakeLists.txt @@ -189,6 +189,8 @@ add_library(citra_qt STATIC EXCLUDE_FROM_ALL util/graphics_device_info.h util/sequence_dialog/sequence_dialog.cpp util/sequence_dialog/sequence_dialog.h + util/sequence_dialog/controller_sequence_dialog.cpp + util/sequence_dialog/controller_sequence_dialog.h util/spinbox.cpp util/spinbox.h util/util.cpp diff --git a/src/citra_qt/configuration/configure_hotkeys_controller.cpp b/src/citra_qt/configuration/configure_hotkeys_controller.cpp index baf8f8c29..8fa4f478a 100644 --- a/src/citra_qt/configuration/configure_hotkeys_controller.cpp +++ b/src/citra_qt/configuration/configure_hotkeys_controller.cpp @@ -8,7 +8,7 @@ #include "citra_qt/configuration/config.h" #include "citra_qt/configuration/configure_hotkeys_controller.h" #include "citra_qt/hotkeys.h" -#include "citra_qt/util/sequence_dialog/sequence_dialog.h" +#include "citra_qt/util/sequence_dialog/controller_sequence_dialog.h" #include "ui_configure_hotkeys_controller.h" constexpr int name_column = 0; @@ -24,7 +24,7 @@ ConfigureControllerHotkeys::ConfigureControllerHotkeys(QWidget* parent) model->setColumnCount(2); model->setHorizontalHeaderLabels({tr("Action"), tr("Controller Hotkey")}); // TODO: re-enable and get profiles workin - ui->horizontalLayout_5->setEnabled(false); + ui->profileGroup->setEnabled(false); connect(ui->hotkey_list, &QTreeView::doubleClicked, this, &ConfigureControllerHotkeys::Configure); connect(ui->hotkey_list, &QTreeView::customContextMenuRequested, this, @@ -60,7 +60,27 @@ void ConfigureControllerHotkeys::Populate(const HotkeyRegistry& registry) { ui->hotkey_list->expandAll(); } -void ConfigureControllerHotkeys::Configure(QModelIndex index) {} +void ConfigureControllerHotkeys::Configure(QModelIndex index) { + if (!index.parent().isValid()) { + return; + } + + // Swap to the hotkey column + index = index.sibling(index.row(), hotkey_column); + QModelIndex readableIndex = index.sibling(index.row(), readable_hotkey_column); + + const auto previous_key = model->data(index); + + ControllerSequenceDialog hotkey_dialog{this}; + + const int return_code = hotkey_dialog.exec(); + const auto key_sequence = hotkey_dialog.GetSequence(); + if (return_code == QDialog::Rejected || key_sequence.isEmpty()) { + return; + } + model->setData(index, key_sequence); + model->setData(readableIndex, CleanSequence(key_sequence)); +} void ConfigureControllerHotkeys::ApplyConfiguration(HotkeyRegistry& registry) { for (int key_id = 0; key_id < model->rowCount(); key_id++) { diff --git a/src/citra_qt/configuration/configure_hotkeys_controller.h b/src/citra_qt/configuration/configure_hotkeys_controller.h index a61c289ca..0b263a358 100644 --- a/src/citra_qt/configuration/configure_hotkeys_controller.h +++ b/src/citra_qt/configuration/configure_hotkeys_controller.h @@ -30,12 +30,12 @@ public: * @param registry The HotkeyRegistry whose data is used to populate the list. */ void Populate(const HotkeyRegistry& registry); + static QString CleanSequence(QString controller_keyseq); private: void Configure(QModelIndex index); void ClearAll(); void PopupContextMenu(const QPoint& menu_location); - QString CleanSequence(QString controller_keyseq); std::unique_ptr ui; diff --git a/src/citra_qt/configuration/configure_hotkeys_controller.ui b/src/citra_qt/configuration/configure_hotkeys_controller.ui index f00acba78..b91a562f4 100644 --- a/src/citra_qt/configuration/configure_hotkeys_controller.ui +++ b/src/citra_qt/configuration/configure_hotkeys_controller.ui @@ -15,6 +15,7 @@ + @@ -61,6 +62,7 @@ + diff --git a/src/citra_qt/configuration/configure_input.cpp b/src/citra_qt/configuration/configure_input.cpp index 2cc319968..6cc2972e4 100644 --- a/src/citra_qt/configuration/configure_input.cpp +++ b/src/citra_qt/configuration/configure_input.cpp @@ -407,7 +407,8 @@ ConfigureInput::ConfigureInput(Core::System& _system, QWidget* parent) Common::ParamPackage params; for (auto& poller : device_pollers) { params = poller->GetNextInput(); - if (params.Has("engine")) { + // skip button downs and only process button ups to maintain former behavior + if (params.Has("engine") && !params.Has("down")) { SetPollingResult(params, false); return; } diff --git a/src/citra_qt/util/sequence_dialog/controller_sequence_dialog.cpp b/src/citra_qt/util/sequence_dialog/controller_sequence_dialog.cpp new file mode 100644 index 000000000..1baa10868 --- /dev/null +++ b/src/citra_qt/util/sequence_dialog/controller_sequence_dialog.cpp @@ -0,0 +1,96 @@ +// Copyright 2026 Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include +#include +#include +#include "common/param_package.h" +#include "configuration/configure_hotkeys_controller.h" +#include "controller_sequence_dialog.h" +#include "util/sequence_dialog/controller_sequence_dialog.h" + +ControllerSequenceDialog::ControllerSequenceDialog(QWidget* parent) + : QDialog(parent), poll_timer(std::make_unique()) { + setWindowTitle(tr("Press then release one or two controller buttons")); + + auto* const buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + buttons->setCenterButtons(true); + + textBox = new QLabel(QStringLiteral("Waiting..."), this); + auto* const layout = new QVBoxLayout(this); + layout->addWidget(textBox); + layout->addWidget(buttons); + + connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); + + LaunchPollers(); // Fixed: added semicolon +} + +ControllerSequenceDialog::~ControllerSequenceDialog() = default; + +QString ControllerSequenceDialog::GetSequence() { + return key_sequence; +} + +void ControllerSequenceDialog::closeEvent(QCloseEvent*) { + reject(); +} + +bool ControllerSequenceDialog::focusNextPrevChild(bool next) { + return false; +} + +void ControllerSequenceDialog::LaunchPollers() { + device_pollers = InputCommon::Polling::GetPollers(InputCommon::Polling::DeviceType::Button); + + for (auto& poller : device_pollers) { + poller->Start(); + } + + connect(poll_timer.get(), &QTimer::timeout, this, [this, downCount = 0]() mutable { + Common::ParamPackage params; + for (auto& poller : device_pollers) { + params = poller->GetNextInput(); + if (params.Has("engine") && params.Has("button")) { // for now, no analog inputs + if (params.Has("down")) { + std::cerr << "button " + params.Get("button", "") + " down" << std::endl; + downCount++; + if (downCount > 2) { + // ignore third and fourth and fifth buttons + } else if (downCount == 1) { + key_sequence = QStringLiteral(""); + params1 = params; + params2 = Common::ParamPackage(); + textBox->setText(ConfigureControllerHotkeys::CleanSequence( + QString::fromStdString(params1.Serialize())) + + QStringLiteral("...")); + } else if (downCount == 2) { + // this is a second button while the first one is held down, + // go ahead and set the key_sequence as the chord and clear params + params2 = params; + key_sequence = QString::fromStdString(params1.Serialize() + "||" + + params2.Serialize()); + textBox->setText(ConfigureControllerHotkeys::CleanSequence(key_sequence)); + } + } else { // button release + downCount--; + std::cerr << "button " + params.Get("button", "") + " up" << std::endl; + if (downCount == 0) { + // all released, clear all saved params + params1 = Common::ParamPackage(); + params2 = Common::ParamPackage(); + } + if (key_sequence.isEmpty()) { + key_sequence = QString::fromStdString(params.Serialize()); + textBox->setText(ConfigureControllerHotkeys::CleanSequence(key_sequence)); + } + } + } + } + }); + poll_timer->start(100); +} \ No newline at end of file diff --git a/src/citra_qt/util/sequence_dialog/controller_sequence_dialog.h b/src/citra_qt/util/sequence_dialog/controller_sequence_dialog.h new file mode 100644 index 000000000..a168d2050 --- /dev/null +++ b/src/citra_qt/util/sequence_dialog/controller_sequence_dialog.h @@ -0,0 +1,31 @@ +// Copyright 2018 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include +#include "common/param_package.h" +#include "input_common/main.h" + +class ControllerSequenceDialog : public QDialog { + Q_OBJECT + +public: + explicit ControllerSequenceDialog(QWidget* parent = nullptr); + ~ControllerSequenceDialog(); + + QString GetSequence(); + void closeEvent(QCloseEvent*) override; + +private: + void LaunchPollers(); + QLabel* textBox; + QString key_sequence; + Common::ParamPackage params1, params2; + bool focusNextPrevChild(bool next) override; + std::vector> device_pollers; + std::unique_ptr poll_timer; +}; diff --git a/src/input_common/sdl/sdl_impl.cpp b/src/input_common/sdl/sdl_impl.cpp index 47862380c..9e6e330fc 100644 --- a/src/input_common/sdl/sdl_impl.cpp +++ b/src/input_common/sdl/sdl_impl.cpp @@ -894,12 +894,12 @@ SDLState::~SDLState() { } } -Common::ParamPackage SDLEventToButtonParamPackage(SDLState& state, const SDL_Event& event) { +Common::ParamPackage SDLEventToButtonParamPackage(SDLState& state, const SDL_Event& event, + const bool down = false) { Common::ParamPackage params({{"engine", "sdl"}}); - + auto joystick = state.GetSDLJoystickBySDLID(event.jhat.which); switch (event.type) { case SDL_JOYAXISMOTION: { - auto joystick = state.GetSDLJoystickBySDLID(event.jaxis.which); params.Set("port", joystick->GetPort()); params.Set("guid", joystick->GetGUID()); params.Set("axis", event.jaxis.axis); @@ -912,15 +912,14 @@ Common::ParamPackage SDLEventToButtonParamPackage(SDLState& state, const SDL_Eve } break; } - case SDL_JOYBUTTONUP: { - auto joystick = state.GetSDLJoystickBySDLID(event.jbutton.which); + case SDL_JOYBUTTONUP: + case SDL_JOYBUTTONDOWN: { params.Set("port", joystick->GetPort()); params.Set("guid", joystick->GetGUID()); params.Set("button", event.jbutton.button); break; } case SDL_JOYHATMOTION: { - auto joystick = state.GetSDLJoystickBySDLID(event.jhat.which); params.Set("port", joystick->GetPort()); params.Set("guid", joystick->GetGUID()); params.Set("hat", event.jhat.hat); @@ -943,6 +942,8 @@ Common::ParamPackage SDLEventToButtonParamPackage(SDLState& state, const SDL_Eve break; } } + if (down) + params.Set("down", 1); return params; } @@ -971,6 +972,7 @@ public: Common::ParamPackage GetNextInput() override { SDL_Event event; + bool down = false; while (state.event_queue.Pop(event)) { switch (event.type) { case SDL_JOYAXISMOTION: @@ -978,7 +980,11 @@ public: !axis_memory[event.jaxis.which].count(event.jaxis.axis)) { axis_memory[event.jaxis.which][event.jaxis.axis] = event.jaxis.value; axis_event_count[event.jaxis.which][event.jaxis.axis] = 1; - break; + if (IsAxisAtPole(event.jaxis.value)) { + down = true; + } else { + break; + } } else { axis_event_count[event.jaxis.which][event.jaxis.axis]++; // The joystick and axis exist in our map if we take this branch, so no checks @@ -1010,9 +1016,11 @@ public: axis_event_count.clear(); } } + case SDL_JOYBUTTONDOWN: + down = true; case SDL_JOYBUTTONUP: case SDL_JOYHATMOTION: - return SDLEventToButtonParamPackage(state, event); + return SDLEventToButtonParamPackage(state, event, down); } } return {};