From ff2989f6e4ec9a4fb369873c0e9295e4969c3812 Mon Sep 17 00:00:00 2001 From: rpnesseling Date: Fri, 5 Jun 2026 01:50:05 +0200 Subject: [PATCH 1/3] qt: add optional on-screen software keyboard --- src/citra_qt/applets/swkbd.cpp | 193 ++++++++++++++++-- src/citra_qt/applets/swkbd.h | 34 ++- src/citra_qt/configuration/config.cpp | 2 + .../configuration/configure_input.cpp | 4 + src/citra_qt/configuration/configure_input.ui | 7 + src/citra_qt/uisettings.h | 2 + 6 files changed, 227 insertions(+), 15 deletions(-) diff --git a/src/citra_qt/applets/swkbd.cpp b/src/citra_qt/applets/swkbd.cpp index 44e6c5738..7dc60d141 100644 --- a/src/citra_qt/applets/swkbd.cpp +++ b/src/citra_qt/applets/swkbd.cpp @@ -2,14 +2,44 @@ // Licensed under GPLv2 or any later version // Refer to the license.txt file included. +#include #include #include +#include +#include #include #include #include +#include #include #include #include "citra_qt/applets/swkbd.h" +#include "citra_qt/uisettings.h" + +namespace { +QString GetValidationErrorMessage(Frontend::ValidationError error, int max_text_length) { + using namespace Frontend; + const std::unordered_map validation_error_messages = { + {ValidationError::FixedLengthRequired, + QObject::tr("Text length is not correct (should be %1 characters)").arg(max_text_length)}, + {ValidationError::MaxLengthExceeded, + QObject::tr("Text is too long (should be no more than %1 characters)") + .arg(max_text_length)}, + {ValidationError::BlankInputNotAllowed, QObject::tr("Blank input is not allowed")}, + {ValidationError::EmptyInputNotAllowed, QObject::tr("Empty input is not allowed")}, + }; + const auto message = validation_error_messages.find(error); + return message == validation_error_messages.end() + ? QObject::tr("Input does not match the requested software keyboard format") + : message->second; +} + +void ShowValidationError(QWidget* parent, Frontend::ValidationError error, int max_text_length) { + QMessageBox::critical(parent, QObject::tr("Validation error"), + GetValidationErrorMessage(error, max_text_length)); +} + +} // Anonymous namespace QtKeyboardValidator::QtKeyboardValidator(QtKeyboard* keyboard_) : keyboard(keyboard_) {} @@ -73,7 +103,7 @@ QtKeyboardDialog::QtKeyboardDialog(QWidget* parent, QtKeyboard* keyboard_) void QtKeyboardDialog::Submit() { auto error = keyboard->ValidateInput(line_edit->text().toStdString()); if (error != Frontend::ValidationError::None) { - HandleValidationError(error); + ShowValidationError(this, error, keyboard->config.max_text_length); } else { button = keyboard->ok_id; text = line_edit->text(); @@ -81,21 +111,139 @@ void QtKeyboardDialog::Submit() { } } -void QtKeyboardDialog::HandleValidationError(Frontend::ValidationError error) { - using namespace Frontend; - const std::unordered_map VALIDATION_ERROR_MESSAGES = { - {ValidationError::FixedLengthRequired, - tr("Text length is not correct (should be %1 characters)") - .arg(keyboard->config.max_text_length)}, - {ValidationError::MaxLengthExceeded, - tr("Text is too long (should be no more than %1 characters)") - .arg(keyboard->config.max_text_length)}, - {ValidationError::BlankInputNotAllowed, tr("Blank input is not allowed")}, - {ValidationError::EmptyInputNotAllowed, tr("Empty input is not allowed")}, +QtSoftwareKeyboardDialog::QtSoftwareKeyboardDialog(QWidget* parent, QtKeyboard* keyboard_) + : QDialog(parent), keyboard(keyboard_) { + setWindowTitle(tr("Software Keyboard")); + setMinimumWidth(560); + + auto* const layout = new QVBoxLayout; + auto* const label = new QLabel(QString::fromStdString(keyboard->config.hint_text)); + line_edit = new QLineEdit; + line_edit->setMinimumHeight(34); + line_edit->setValidator(new QtKeyboardValidator(keyboard)); + length_label = new QLabel; + validation_label = new QLabel; + validation_label->setStyleSheet(QStringLiteral("color: palette(highlight);")); + validation_label->setVisible(false); + + auto* const keys = new QGridLayout; + const QString rows[] = { + QStringLiteral("1234567890"), + QStringLiteral("QWERTYUIOP"), + QStringLiteral("ASDFGHJKL"), + QStringLiteral("ZXCVBNM"), }; - QMessageBox::critical(this, tr("Validation error"), VALIDATION_ERROR_MESSAGES.at(error)); + + for (int row = 0; row < std::size(rows); ++row) { + for (int column = 0; column < rows[row].size(); ++column) { + const QString value = rows[row].mid(column, 1); + auto* const button = new QPushButton(value); + button->setMinimumHeight(42); + button->setFocusPolicy(Qt::NoFocus); + if (value.front().isLetter()) { + letter_buttons.push_back(button); + } + connect(button, &QPushButton::clicked, this, [this, button] { + AppendText(button->text()); + }); + keys->addWidget(button, row, column); + } + } + + auto* const controls = new QHBoxLayout; + shift_button = new QPushButton(tr("Shift")); + shift_button->setCheckable(true); + shift_button->setChecked(uppercase); + auto* const space = new QPushButton(tr("Space")); + auto* const backspace = new QPushButton(tr("Backspace")); + auto* const ok = new QPushButton(tr(Frontend::SWKBD_BUTTON_OKAY)); + auto* const cancel = new QPushButton(tr(Frontend::SWKBD_BUTTON_CANCEL)); + + for (auto* const button : {shift_button, space, backspace, ok, cancel}) { + button->setMinimumHeight(42); + button->setFocusPolicy(Qt::NoFocus); + } + + connect(shift_button, &QPushButton::clicked, this, [this] { ToggleCase(); }); + connect(space, &QPushButton::clicked, this, [this] { AppendText(QStringLiteral(" ")); }); + connect(backspace, &QPushButton::clicked, this, [this] { Backspace(); }); + connect(ok, &QPushButton::clicked, this, [this] { Submit(); }); + connect(cancel, &QPushButton::clicked, this, [this] { Cancel(); }); + + controls->addWidget(shift_button); + controls->addWidget(space); + controls->addWidget(backspace); + controls->addWidget(ok); + controls->addWidget(cancel); + + layout->addWidget(label); + layout->addWidget(line_edit); + layout->addWidget(length_label); + layout->addWidget(validation_label); + layout->addLayout(keys); + layout->addLayout(controls); + setLayout(layout); + UpdateLengthLabel(); + line_edit->setFocus(); } +void QtSoftwareKeyboardDialog::AppendText(const QString& value) { + QString next = line_edit->text(); + const int cursor_position = line_edit->cursorPosition(); + next.insert(cursor_position, value); + if (next.size() > keyboard->config.max_text_length) { + return; + } + line_edit->setText(next); + line_edit->setCursorPosition(cursor_position + value.size()); + UpdateLengthLabel(); + ClearValidationError(); +} + +void QtSoftwareKeyboardDialog::Backspace() { + line_edit->backspace(); + UpdateLengthLabel(); + ClearValidationError(); +} + +void QtSoftwareKeyboardDialog::Cancel() { + button = QtKeyboard::cancel_id; + accept(); +} + +void QtSoftwareKeyboardDialog::ToggleCase() { + uppercase = !uppercase; + for (auto* const button : letter_buttons) { + button->setText(uppercase ? button->text().toUpper() : button->text().toLower()); + } + shift_button->setChecked(uppercase); +} + +void QtSoftwareKeyboardDialog::UpdateLengthLabel() { + length_label->setText( + tr("%1 / %2").arg(line_edit->text().size()).arg(keyboard->config.max_text_length)); +} + +void QtSoftwareKeyboardDialog::Submit() { + auto error = keyboard->ValidateInput(line_edit->text().toStdString()); + if (error != Frontend::ValidationError::None) { + ShowInlineValidationError(error); + } else { + button = keyboard->ok_id; + text = line_edit->text(); + accept(); + } +} + +void QtSoftwareKeyboardDialog::ShowInlineValidationError(Frontend::ValidationError error) { + validation_label->setText(GetValidationErrorMessage(error, keyboard->config.max_text_length)); + validation_label->setVisible(true); +} + +void QtSoftwareKeyboardDialog::ClearValidationError() { + validation_label->clear(); + validation_label->setVisible(false); +} QtKeyboard::QtKeyboard(QWidget& parent_) : parent(parent_) {} void QtKeyboard::Execute(const Frontend::KeyboardConfig& config) { @@ -103,7 +251,11 @@ void QtKeyboard::Execute(const Frontend::KeyboardConfig& config) { if (this->config.button_config != Frontend::ButtonConfig::None) { ok_id = static_cast(this->config.button_config); } - QMetaObject::invokeMethod(this, "OpenInputDialog", Qt::BlockingQueuedConnection); + if (UISettings::values.use_on_screen_software_keyboard.GetValue()) { + QMetaObject::invokeMethod(this, "OpenSoftwareKeyboardDialog", Qt::BlockingQueuedConnection); + } else { + QMetaObject::invokeMethod(this, "OpenInputDialog", Qt::BlockingQueuedConnection); + } Finalize(result_text, result_button); } @@ -126,6 +278,19 @@ void QtKeyboard::OpenInputDialog() { result_button); } +void QtKeyboard::OpenSoftwareKeyboardDialog() { + QtSoftwareKeyboardDialog dialog(&parent, this); + dialog.setWindowFlags(dialog.windowFlags() & + ~(Qt::WindowCloseButtonHint | Qt::WindowContextHelpButtonHint)); + dialog.setWindowModality(Qt::WindowModal); + dialog.exec(); + + result_text = dialog.text.toStdString(); + result_button = dialog.button; + LOG_INFO(Frontend, "SWKBD software keyboard dialog finished, text={}, button={}", result_text, + result_button); +} + void QtKeyboard::ShowErrorDialog(QString message) { QMessageBox::critical(&parent, tr("Software Keyboard"), message); } diff --git a/src/citra_qt/applets/swkbd.h b/src/citra_qt/applets/swkbd.h index 95ed95844..ae43f97d2 100644 --- a/src/citra_qt/applets/swkbd.h +++ b/src/citra_qt/applets/swkbd.h @@ -4,6 +4,7 @@ #pragma once +#include #include #include #include "core/frontend/applets/swkbd.h" @@ -11,6 +12,7 @@ class QDialogButtonBox; class QLabel; class QLineEdit; +class QPushButton; class QVBoxLayout; class QtKeyboard; @@ -31,7 +33,6 @@ public: void Submit(); private: - void HandleValidationError(Frontend::ValidationError error); QDialogButtonBox* buttons; QLabel* label; QLineEdit* line_edit; @@ -43,6 +44,35 @@ private: friend class QtKeyboard; }; +class QtSoftwareKeyboardDialog final : public QDialog { + Q_OBJECT + +public: + QtSoftwareKeyboardDialog(QWidget* parent, QtKeyboard* keyboard); + +private: + void AppendText(const QString& value); + void Backspace(); + void Cancel(); + void Submit(); + void ToggleCase(); + void UpdateLengthLabel(); + void ShowInlineValidationError(Frontend::ValidationError error); + void ClearValidationError(); + + QLineEdit* line_edit; + QLabel* length_label; + QLabel* validation_label; + QPushButton* shift_button; + QtKeyboard* keyboard; + QString text; + u8 button; + std::vector letter_buttons; + bool uppercase = true; + + friend class QtKeyboard; +}; + class QtKeyboard final : public QObject, public Frontend::SoftwareKeyboard { Q_OBJECT @@ -53,6 +83,7 @@ public: private: Q_INVOKABLE void OpenInputDialog(); + Q_INVOKABLE void OpenSoftwareKeyboardDialog(); Q_INVOKABLE void ShowErrorDialog(QString message); /// Index of the buttons @@ -66,5 +97,6 @@ private: int result_button; friend class QtKeyboardDialog; + friend class QtSoftwareKeyboardDialog; friend class QtKeyboardValidator; }; diff --git a/src/citra_qt/configuration/config.cpp b/src/citra_qt/configuration/config.cpp index 9091d734b..0cfd39658 100644 --- a/src/citra_qt/configuration/config.cpp +++ b/src/citra_qt/configuration/config.cpp @@ -858,6 +858,7 @@ void QtConfig::ReadUIValues() { ReadBasicSetting(UISettings::values.pause_when_in_background); ReadBasicSetting(UISettings::values.mute_when_in_background); ReadBasicSetting(UISettings::values.hide_mouse); + ReadBasicSetting(UISettings::values.use_on_screen_software_keyboard); } qt_config->endGroup(); @@ -1391,6 +1392,7 @@ void QtConfig::SaveUIValues() { WriteBasicSetting(UISettings::values.pause_when_in_background); WriteBasicSetting(UISettings::values.mute_when_in_background); WriteBasicSetting(UISettings::values.hide_mouse); + WriteBasicSetting(UISettings::values.use_on_screen_software_keyboard); } qt_config->endGroup(); diff --git a/src/citra_qt/configuration/configure_input.cpp b/src/citra_qt/configuration/configure_input.cpp index 2cc319968..5409c886e 100644 --- a/src/citra_qt/configuration/configure_input.cpp +++ b/src/citra_qt/configuration/configure_input.cpp @@ -15,6 +15,7 @@ #include "citra_qt/configuration/config.h" #include "citra_qt/configuration/configure_input.h" #include "citra_qt/configuration/configure_motion_touch.h" +#include "citra_qt/uisettings.h" #include "common/param_package.h" #include "core/core.h" #include "ui_configure_input.h" @@ -422,6 +423,7 @@ ConfigureInput::~ConfigureInput() = default; void ConfigureInput::ApplyConfiguration() { Settings::values.use_artic_base_controller = ui->use_artic_controller->isChecked(); + UISettings::values.use_on_screen_software_keyboard = ui->use_onscreen_keyboard->isChecked(); std::transform(buttons_param.begin(), buttons_param.end(), Settings::values.current_input_profile.buttons.begin(), @@ -470,6 +472,8 @@ void ConfigureInput::LoadConfiguration() { ui->use_artic_controller->setChecked(Settings::values.use_artic_base_controller.GetValue()); ui->use_artic_controller->setEnabled(!system.IsPoweredOn()); + ui->use_onscreen_keyboard->setChecked( + UISettings::values.use_on_screen_software_keyboard.GetValue()); std::transform(Settings::values.current_input_profile.buttons.begin(), Settings::values.current_input_profile.buttons.end(), buttons_param.begin(), diff --git a/src/citra_qt/configuration/configure_input.ui b/src/citra_qt/configuration/configure_input.ui index 67cc7688f..39a50c331 100644 --- a/src/citra_qt/configuration/configure_input.ui +++ b/src/citra_qt/configuration/configure_input.ui @@ -1057,6 +1057,13 @@ + + + + Use on-screen software keyboard + + + diff --git a/src/citra_qt/uisettings.h b/src/citra_qt/uisettings.h index ef87b3fe3..93ee6a324 100644 --- a/src/citra_qt/uisettings.h +++ b/src/citra_qt/uisettings.h @@ -89,6 +89,8 @@ struct Values { Settings::Setting pause_when_in_background{false, "pauseWhenInBackground"}; Settings::Setting mute_when_in_background{false, "muteWhenInBackground"}; Settings::Setting hide_mouse{false, "hideInactiveMouse"}; + Settings::Setting use_on_screen_software_keyboard{false, + "useOnScreenSoftwareKeyboard"}; #ifdef ENABLE_QT_UPDATE_CHECKER Settings::Setting check_for_update_on_start{true, "check_for_update_on_start"}; Settings::Setting update_check_channel{UpdateCheckChannels::STABLE, From f72fefc0878f7c31de9e77e462962d9ac993c312 Mon Sep 17 00:00:00 2001 From: rpnesseling Date: Fri, 5 Jun 2026 12:16:02 +0200 Subject: [PATCH 2/3] 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; From d0e22bf22034f79ee17d99dad2985f3a4dab6f9a Mon Sep 17 00:00:00 2001 From: rpnesseling Date: Fri, 5 Jun 2026 12:31:58 +0200 Subject: [PATCH 3/3] qt: improve software keyboard vertical navigation --- src/citra_qt/applets/swkbd.cpp | 34 +++++++++++++++++++++++++++++----- src/citra_qt/applets/swkbd.h | 1 + 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/src/citra_qt/applets/swkbd.cpp b/src/citra_qt/applets/swkbd.cpp index c67fedf3c..c7883ba8c 100644 --- a/src/citra_qt/applets/swkbd.cpp +++ b/src/citra_qt/applets/swkbd.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include #include @@ -371,17 +372,40 @@ void QtSoftwareKeyboardDialog::MoveSelection(int row_delta, int column_delta) { 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; + int next_column = selected_column; + if (row_delta != 0) { + const auto* const selected_button = button_rows[selected_row][selected_column]; + next_column = FindClosestColumnInRow( + next_row, selected_button->geometry().center().x()); + } else { + next_column += column_delta; + if (next_column < 0) { + next_column = row_size - 1; + } else if (next_column >= row_size) { + next_column = 0; + } } SetSelectedButton(next_row, next_column); } +int QtSoftwareKeyboardDialog::FindClosestColumnInRow(int row, int source_x) const { + int closest_column = 0; + int closest_distance = std::numeric_limits::max(); + + for (int column = 0; column < static_cast(button_rows[row].size()); ++column) { + const int distance = + std::abs(button_rows[row][column]->geometry().center().x() - source_x); + if (distance < closest_distance) { + closest_column = column; + closest_distance = distance; + } + } + + return closest_column; +} + void QtSoftwareKeyboardDialog::SetSelectedButton(int row, int column) { if (!button_rows.empty()) { auto* const previous_button = button_rows[selected_row][selected_column]; diff --git a/src/citra_qt/applets/swkbd.h b/src/citra_qt/applets/swkbd.h index 738bfd09e..8661f37a5 100644 --- a/src/citra_qt/applets/swkbd.h +++ b/src/citra_qt/applets/swkbd.h @@ -103,6 +103,7 @@ private: void ShowInlineValidationError(Frontend::ValidationError error); void ClearValidationError(); void MoveSelection(int row_delta, int column_delta); + int FindClosestColumnInRow(int row, int source_x) const; void SetSelectedButton(int row, int column); void ActivateSelectedButton(); void HandleInputAction(SoftwareKeyboardInputInterpreter::Action action);