From f72fefc0878f7c31de9e77e462962d9ac993c312 Mon Sep 17 00:00:00 2001 From: rpnesseling Date: Fri, 5 Jun 2026 12:16:02 +0200 Subject: [PATCH] qt: add controller navigation for software keyboard --- src/citra_qt/applets/swkbd.cpp | 196 +++++++++++++++++++++++++++++++++ src/citra_qt/applets/swkbd.h | 53 +++++++++ 2 files changed, 249 insertions(+) diff --git a/src/citra_qt/applets/swkbd.cpp b/src/citra_qt/applets/swkbd.cpp index 7dc60d141..c67fedf3c 100644 --- a/src/citra_qt/applets/swkbd.cpp +++ b/src/citra_qt/applets/swkbd.cpp @@ -2,8 +2,10 @@ // Licensed under GPLv2 or any later version // Refer to the license.txt file included. +#include #include #include +#include #include #include #include @@ -12,11 +14,50 @@ #include #include #include +#include +#include #include #include "citra_qt/applets/swkbd.h" #include "citra_qt/uisettings.h" +#include "common/param_package.h" +#include "common/settings.h" +#ifdef HAVE_SDL2 +#include +#endif namespace { +constexpr int CONTROLLER_POLL_INTERVAL_MS = 16; +constexpr float CIRCLE_PAD_DIRECTION_THRESHOLD = 0.5f; + +void PumpAppletInputEvents() { +#ifdef HAVE_SDL2 + if (SDL_WasInit(SDL_INIT_GAMECONTROLLER) != 0) { + SDL_PumpEvents(); + SDL_GameControllerUpdate(); + } +#endif +} + +std::unique_ptr CreateAppletButtonDevice(const std::string& params) { + const Common::ParamPackage package(params); + const auto engine = package.Get("engine", ""); + // Keyboard bindings are left to Qt text input so physical typing works even when + // the active emulator input profile is controller-based. + if (engine.empty() || engine == "keyboard") { + return {}; + } + return Input::CreateDevice(params); +} + +std::unique_ptr CreateAppletAnalogDevice(const std::string& params) { + const Common::ParamPackage package(params); + const auto engine = package.Get("engine", ""); + if (engine.empty() || engine == "analog_from_button") { + return {}; + } + return Input::CreateDevice(params); +} + QString GetValidationErrorMessage(Frontend::ValidationError error, int max_text_length) { using namespace Frontend; const std::unordered_map validation_error_messages = { @@ -53,6 +94,66 @@ QtKeyboardValidator::State QtKeyboardValidator::validate(QString& input, int& po } } +SoftwareKeyboardInputInterpreter::SoftwareKeyboardInputInterpreter() { + using namespace Settings::NativeButton; + + const auto& profile_buttons = Settings::values.current_input_profile.buttons; + buttons[ButtonA] = CreateAppletButtonDevice(profile_buttons[A]); + buttons[ButtonB] = CreateAppletButtonDevice(profile_buttons[B]); + buttons[ButtonUp] = CreateAppletButtonDevice(profile_buttons[Up]); + buttons[ButtonDown] = CreateAppletButtonDevice(profile_buttons[Down]); + buttons[ButtonLeft] = CreateAppletButtonDevice(profile_buttons[Left]); + buttons[ButtonRight] = CreateAppletButtonDevice(profile_buttons[Right]); + + circle_pad = CreateAppletAnalogDevice( + Settings::values.current_input_profile.analogs[Settings::NativeAnalog::CirclePad]); +} + +std::vector SoftwareKeyboardInputInterpreter::Poll() { + PumpAppletInputEvents(); + + std::vector actions; + const std::array button_actions{{ + Action::Accept, + Action::CancelOrBackspace, + Action::MoveUp, + Action::MoveDown, + Action::MoveLeft, + Action::MoveRight, + }}; + + for (std::size_t index = 0; index < buttons.size(); ++index) { + if (!buttons[index]) { + continue; + } + const bool is_pressed = buttons[index]->GetStatus(); + if (is_pressed && !previous_button_state[index]) { + actions.push_back(button_actions[index]); + } + previous_button_state[index] = is_pressed; + } + + if (circle_pad) { + const auto [x, y] = circle_pad->GetStatus(); + const std::array, NumDirections> direction_actions{{ + {y > CIRCLE_PAD_DIRECTION_THRESHOLD, Action::MoveUp}, + {y < -CIRCLE_PAD_DIRECTION_THRESHOLD, Action::MoveDown}, + {x < -CIRCLE_PAD_DIRECTION_THRESHOLD, Action::MoveLeft}, + {x > CIRCLE_PAD_DIRECTION_THRESHOLD, Action::MoveRight}, + }}; + + for (std::size_t index = 0; index < direction_actions.size(); ++index) { + const auto [is_pressed, action] = direction_actions[index]; + if (is_pressed && !previous_direction_state[index]) { + actions.push_back(action); + } + previous_direction_state[index] = is_pressed; + } + } + + return actions; +} + QtKeyboardDialog::QtKeyboardDialog(QWidget* parent, QtKeyboard* keyboard_) : QDialog(parent), keyboard(keyboard_) { using namespace Frontend; @@ -115,6 +216,12 @@ QtSoftwareKeyboardDialog::QtSoftwareKeyboardDialog(QWidget* parent, QtKeyboard* : QDialog(parent), keyboard(keyboard_) { setWindowTitle(tr("Software Keyboard")); setMinimumWidth(560); + setStyleSheet(QStringLiteral( + "QPushButton[controllerSelected=\"true\"] {" + "border: 2px solid palette(highlight);" + "background-color: palette(highlight);" + "color: palette(highlighted-text);" + "}")); auto* const layout = new QVBoxLayout; auto* const label = new QLabel(QString::fromStdString(keyboard->config.hint_text)); @@ -135,6 +242,7 @@ QtSoftwareKeyboardDialog::QtSoftwareKeyboardDialog(QWidget* parent, QtKeyboard* }; for (int row = 0; row < std::size(rows); ++row) { + std::vector button_row; for (int column = 0; column < rows[row].size(); ++column) { const QString value = rows[row].mid(column, 1); auto* const button = new QPushButton(value); @@ -147,7 +255,9 @@ QtSoftwareKeyboardDialog::QtSoftwareKeyboardDialog(QWidget* parent, QtKeyboard* AppendText(button->text()); }); keys->addWidget(button, row, column); + button_row.push_back(button); } + button_rows.push_back(std::move(button_row)); } auto* const controls = new QHBoxLayout; @@ -170,6 +280,8 @@ QtSoftwareKeyboardDialog::QtSoftwareKeyboardDialog(QWidget* parent, QtKeyboard* connect(ok, &QPushButton::clicked, this, [this] { Submit(); }); connect(cancel, &QPushButton::clicked, this, [this] { Cancel(); }); + button_rows.push_back({shift_button, space, backspace, ok, cancel}); + controls->addWidget(shift_button); controls->addWidget(space); controls->addWidget(backspace); @@ -184,7 +296,13 @@ QtSoftwareKeyboardDialog::QtSoftwareKeyboardDialog(QWidget* parent, QtKeyboard* layout->addLayout(controls); setLayout(layout); UpdateLengthLabel(); + SetSelectedButton(0, 0); line_edit->setFocus(); + + controller_poll_timer = new QTimer(this); + connect(controller_poll_timer, &QTimer::timeout, this, + &QtSoftwareKeyboardDialog::PollControllerInput); + controller_poll_timer->start(CONTROLLER_POLL_INTERVAL_MS); } void QtSoftwareKeyboardDialog::AppendText(const QString& value) { @@ -244,6 +362,84 @@ void QtSoftwareKeyboardDialog::ClearValidationError() { validation_label->clear(); validation_label->setVisible(false); } + +void QtSoftwareKeyboardDialog::MoveSelection(int row_delta, int column_delta) { + int next_row = selected_row + row_delta; + if (next_row < 0) { + next_row = static_cast(button_rows.size()) - 1; + } else if (next_row >= static_cast(button_rows.size())) { + next_row = 0; + } + + int next_column = selected_column + column_delta; + const int row_size = static_cast(button_rows[next_row].size()); + if (next_column < 0) { + next_column = row_size - 1; + } else if (next_column >= row_size) { + next_column = 0; + } + + SetSelectedButton(next_row, next_column); +} + +void QtSoftwareKeyboardDialog::SetSelectedButton(int row, int column) { + if (!button_rows.empty()) { + auto* const previous_button = button_rows[selected_row][selected_column]; + previous_button->setProperty("controllerSelected", false); + previous_button->style()->unpolish(previous_button); + previous_button->style()->polish(previous_button); + previous_button->update(); + } + + selected_row = row; + selected_column = std::min(column, static_cast(button_rows[selected_row].size()) - 1); + + auto* const selected_button = button_rows[selected_row][selected_column]; + selected_button->setProperty("controllerSelected", true); + selected_button->style()->unpolish(selected_button); + selected_button->style()->polish(selected_button); + selected_button->update(); +} + +void QtSoftwareKeyboardDialog::ActivateSelectedButton() { + button_rows[selected_row][selected_column]->click(); +} + +void QtSoftwareKeyboardDialog::HandleInputAction(SoftwareKeyboardInputInterpreter::Action action) { + using Action = SoftwareKeyboardInputInterpreter::Action; + + switch (action) { + case Action::MoveUp: + MoveSelection(-1, 0); + break; + case Action::MoveDown: + MoveSelection(1, 0); + break; + case Action::MoveLeft: + MoveSelection(0, -1); + break; + case Action::MoveRight: + MoveSelection(0, 1); + break; + case Action::Accept: + ActivateSelectedButton(); + break; + case Action::CancelOrBackspace: + if (line_edit->text().isEmpty()) { + Cancel(); + } else { + Backspace(); + } + break; + } +} + +void QtSoftwareKeyboardDialog::PollControllerInput() { + for (const auto action : input_interpreter.Poll()) { + HandleInputAction(action); + } +} + QtKeyboard::QtKeyboard(QWidget& parent_) : parent(parent_) {} void QtKeyboard::Execute(const Frontend::KeyboardConfig& config) { diff --git a/src/citra_qt/applets/swkbd.h b/src/citra_qt/applets/swkbd.h index ae43f97d2..738bfd09e 100644 --- a/src/citra_qt/applets/swkbd.h +++ b/src/citra_qt/applets/swkbd.h @@ -4,15 +4,19 @@ #pragma once +#include +#include #include #include #include #include "core/frontend/applets/swkbd.h" +#include "core/frontend/input.h" class QDialogButtonBox; class QLabel; class QLineEdit; class QPushButton; +class QTimer; class QVBoxLayout; class QtKeyboard; @@ -25,6 +29,45 @@ private: QtKeyboard* keyboard; }; +class SoftwareKeyboardInputInterpreter { +public: + enum class Action { + MoveUp, + MoveDown, + MoveLeft, + MoveRight, + Accept, + CancelOrBackspace, + }; + + SoftwareKeyboardInputInterpreter(); + std::vector Poll(); + +private: + enum Button { + ButtonA, + ButtonB, + ButtonUp, + ButtonDown, + ButtonLeft, + ButtonRight, + NumButtons, + }; + + enum Direction { + DirectionUp, + DirectionDown, + DirectionLeft, + DirectionRight, + NumDirections, + }; + + std::array, NumButtons> buttons; + std::unique_ptr circle_pad; + std::array previous_button_state{}; + std::array previous_direction_state{}; +}; + class QtKeyboardDialog final : public QDialog { Q_OBJECT @@ -59,15 +102,25 @@ private: void UpdateLengthLabel(); void ShowInlineValidationError(Frontend::ValidationError error); void ClearValidationError(); + void MoveSelection(int row_delta, int column_delta); + void SetSelectedButton(int row, int column); + void ActivateSelectedButton(); + void HandleInputAction(SoftwareKeyboardInputInterpreter::Action action); + void PollControllerInput(); QLineEdit* line_edit; QLabel* length_label; QLabel* validation_label; QPushButton* shift_button; + QTimer* controller_poll_timer; QtKeyboard* keyboard; QString text; u8 button; + std::vector> button_rows; std::vector letter_buttons; + SoftwareKeyboardInputInterpreter input_interpreter; + int selected_row = 0; + int selected_column = 0; bool uppercase = true; friend class QtKeyboard;