From 15ef6119d2b02a606948db008c403178cc2456a9 Mon Sep 17 00:00:00 2001 From: rpnesseling Date: Mon, 22 Jun 2026 15:12:04 +0200 Subject: [PATCH] qt: add optional on-screen software keyboard --- src/citra_qt/applets/swkbd.cpp | 528 +++++++++++++++++- src/citra_qt/applets/swkbd.h | 97 +++- 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 + src/core/frontend/applets/swkbd.h | 8 + src/core/hle/applets/swkbd.cpp | 1 + 8 files changed, 633 insertions(+), 16 deletions(-) diff --git a/src/citra_qt/applets/swkbd.cpp b/src/citra_qt/applets/swkbd.cpp index 44e6c5738..47e420c7e 100644 --- a/src/citra_qt/applets/swkbd.cpp +++ b/src/citra_qt/applets/swkbd.cpp @@ -2,14 +2,111 @@ // 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 #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 = { + {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)); +} + +QString GetButtonText(const Frontend::KeyboardConfig& config, std::size_t index, + const char* fallback) { + return index < config.button_text.size() && !config.button_text[index].empty() + ? QString::fromStdString(config.button_text[index]) + : QObject::tr(fallback); +} + +std::array GetLetterRows(bool uppercase) { + return { + QStringLiteral("1234567890"), + uppercase ? QStringLiteral("QWERTYUIOP") : QStringLiteral("qwertyuiop"), + uppercase ? QStringLiteral("ASDFGHJKL") : QStringLiteral("asdfghjkl"), + uppercase ? QStringLiteral("ZXCVBNM") : QStringLiteral("zxcvbnm"), + }; +} + +std::array GetNumberPadRows() { + return { + QStringLiteral("123"), + QStringLiteral("456"), + QStringLiteral("789"), + QStringLiteral("0"), + }; +} + +} // Anonymous namespace QtKeyboardValidator::QtKeyboardValidator(QtKeyboard* keyboard_) : keyboard(keyboard_) {} @@ -23,6 +120,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; @@ -73,7 +230,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,19 +238,343 @@ 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")}, - }; - QMessageBox::critical(this, tr("Validation error"), VALIDATION_ERROR_MESSAGES.at(error)); +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)); + connect(line_edit, &QLineEdit::textChanged, this, [this] { + UpdateLengthLabel(); + ClearValidationError(); + }); + 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 auto rows = IsNumberPad() ? GetNumberPadRows() : GetLetterRows(uppercase); + + for (int row = 0; row < std::size(rows); ++row) { + std::vector key_button_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); + button->setMinimumHeight(42); + button->setFocusPolicy(Qt::NoFocus); + connect(button, &QPushButton::clicked, this, + [this, button] { AppendText(button->text()); }); + keys->addWidget(button, row, column); + key_button_row.push_back(button); + button_row.push_back(button); + } + key_buttons.push_back(std::move(key_button_row)); + button_rows.push_back(std::move(button_row)); + } + + auto* const controls = new QHBoxLayout; + shift_button = new QPushButton(tr("Shift")); + shift_button->setCheckable(true); + shift_button->setChecked(uppercase); + symbols_button = new QPushButton(tr("Symbols")); + shift_button->setEnabled(!IsNumberPad()); + symbols_button->setEnabled(!IsNumberPad()); + auto* const space = new QPushButton(tr("Space")); + auto* const backspace = new QPushButton(tr("Backspace")); + std::vector control_buttons{shift_button, symbols_button, space, backspace}; + + QPushButton* ok = nullptr; + QPushButton* cancel = nullptr; + QPushButton* forgot = nullptr; + + switch (keyboard->config.button_config) { + case Frontend::ButtonConfig::Triple: + forgot = new QPushButton(GetButtonText(keyboard->config, 1, Frontend::SWKBD_BUTTON_FORGOT)); + [[fallthrough]]; + case Frontend::ButtonConfig::Dual: + cancel = new QPushButton(GetButtonText(keyboard->config, 0, Frontend::SWKBD_BUTTON_CANCEL)); + [[fallthrough]]; + case Frontend::ButtonConfig::Single: + ok = new QPushButton(GetButtonText(keyboard->config, 2, Frontend::SWKBD_BUTTON_OKAY)); + break; + case Frontend::ButtonConfig::None: + break; + } + if (cancel) { + control_buttons.push_back(cancel); + } + if (forgot) { + control_buttons.push_back(forgot); + } + if (ok) { + control_buttons.push_back(ok); + } + + for (auto* const button : control_buttons) { + button->setMinimumHeight(42); + button->setFocusPolicy(Qt::NoFocus); + } + + default_button_palette = control_buttons.front()->palette(); + selected_button_palette = default_button_palette; + selected_button_palette.setColor(QPalette::Button, palette().color(QPalette::Highlight)); + selected_button_palette.setColor(QPalette::ButtonText, + palette().color(QPalette::HighlightedText)); + + connect(shift_button, &QPushButton::clicked, this, [this] { ToggleCase(); }); + connect(symbols_button, &QPushButton::clicked, this, [this] { ToggleSymbols(); }); + connect(space, &QPushButton::clicked, this, [this] { AppendText(QStringLiteral(" ")); }); + connect(backspace, &QPushButton::clicked, this, [this] { Backspace(); }); + if (forgot) { + connect(forgot, &QPushButton::clicked, this, [this] { + button = QtKeyboard::forgot_id; + accept(); + }); + } + if (cancel) { + connect(cancel, &QPushButton::clicked, this, [this] { Cancel(); }); + } + if (ok) { + connect(ok, &QPushButton::clicked, this, [this] { Submit(); }); + } + + button_rows.push_back(control_buttons); + + for (auto* const button : control_buttons) { + controls->addWidget(button); + } + + layout->addWidget(label); + layout->addWidget(line_edit); + layout->addWidget(length_label); + layout->addWidget(validation_label); + layout->addLayout(keys); + 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) { + 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()); +} + +void QtSoftwareKeyboardDialog::Backspace() { + line_edit->backspace(); +} + +void QtSoftwareKeyboardDialog::Cancel() { + button = QtKeyboard::cancel_id; + accept(); +} + +void QtSoftwareKeyboardDialog::ToggleCase() { + if (IsNumberPad()) { + return; + } + + uppercase = !uppercase; + shift_button->setChecked(uppercase); + if (!symbols) { + UpdateKeyLabels(); + } +} + +void QtSoftwareKeyboardDialog::ToggleSymbols() { + if (IsNumberPad()) { + return; + } + + if (!symbols) { + symbols = true; + symbol_page = 0; + } else if (symbol_page == 0) { + symbol_page = 1; + } else { + symbols = false; + symbol_page = 0; + } + + UpdateKeyLabels(); + if (symbols && button_rows[selected_row][selected_column] == shift_button) { + SetSelectedButton(static_cast(button_rows.size()) - 1, 1); + } +} + +void QtSoftwareKeyboardDialog::UpdateKeyLabels() { + const auto letter_rows = IsNumberPad() ? GetNumberPadRows() : GetLetterRows(uppercase); + const std::array, 2> symbol_rows{{ + { + QStringLiteral("1234567890"), + QStringLiteral("-_.,!?@#&/"), + QStringLiteral("'\":;()[]\\"), + QStringLiteral("+*=<>%~"), + }, + { + QStringLiteral("{}[]()<>|\\"), + QStringLiteral("$%^`~+-=*/"), + QStringLiteral("@#&.,?!:;"), + QStringLiteral("'\"_`~/\\"), + }, + }}; + const std::array& rows = symbols ? symbol_rows[symbol_page] : letter_rows; + + for (int row = 0; row < static_cast(key_buttons.size()); ++row) { + for (int column = 0; column < static_cast(key_buttons[row].size()); ++column) { + key_buttons[row][column]->setText(rows[row].mid(column, 1)); + } + } + + shift_button->setEnabled(!symbols); + symbols_button->setText(symbols ? (symbol_page == 0 ? tr("More") : tr("ABC")) : tr("Symbols")); +} + +bool QtSoftwareKeyboardDialog::IsNumberPad() const { + return keyboard->config.type == Frontend::KeyboardType::NumPad; +} + +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); +} + +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; + } + + const int row_size = static_cast(button_rows[next_row].size()); + 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::SetButtonControllerSelected(QPushButton* button, bool selected) { + button->setAutoFillBackground(selected); + button->setPalette(selected ? selected_button_palette : default_button_palette); + button->update(); +} + +void QtSoftwareKeyboardDialog::SetSelectedButton(int row, int column) { + if (!button_rows.empty()) { + auto* const previous_button = button_rows[selected_row][selected_column]; + SetButtonControllerSelected(previous_button, false); + } + + 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]; + SetButtonControllerSelected(selected_button, true); +} + +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()) { + Backspace(); + } else if (keyboard->config.button_config == Frontend::ButtonConfig::Dual || + keyboard->config.button_config == Frontend::ButtonConfig::Triple) { + Cancel(); + } + break; + } +} + +void QtSoftwareKeyboardDialog::PollControllerInput() { + for (const auto action : input_interpreter.Poll()) { + HandleInputAction(action); + } } QtKeyboard::QtKeyboard(QWidget& parent_) : parent(parent_) {} @@ -103,7 +584,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 +611,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..1b140dafd 100644 --- a/src/citra_qt/applets/swkbd.h +++ b/src/citra_qt/applets/swkbd.h @@ -4,13 +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; @@ -23,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 @@ -31,7 +76,6 @@ public: void Submit(); private: - void HandleValidationError(Frontend::ValidationError error); QDialogButtonBox* buttons; QLabel* label; QLineEdit* line_edit; @@ -43,6 +87,55 @@ 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 ToggleSymbols(); + void UpdateKeyLabels(); + bool IsNumberPad() const; + void UpdateLengthLabel(); + void ShowInlineValidationError(Frontend::ValidationError error); + void ClearValidationError(); + void MoveSelection(int row_delta, int column_delta); + int FindClosestColumnInRow(int row, int source_x) const; + void SetButtonControllerSelected(QPushButton* button, bool selected); + 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; + QPushButton* symbols_button; + QTimer* controller_poll_timer; + QtKeyboard* keyboard; + QString text; + u8 button; + std::vector> key_buttons; + std::vector> button_rows; + SoftwareKeyboardInputInterpreter input_interpreter; + QPalette default_button_palette; + QPalette selected_button_palette; + int selected_row = 0; + int selected_column = 0; + int symbol_page = 0; + bool uppercase = true; + bool symbols = false; + + friend class QtKeyboard; +}; + class QtKeyboard final : public QObject, public Frontend::SoftwareKeyboard { Q_OBJECT @@ -53,6 +146,7 @@ public: private: Q_INVOKABLE void OpenInputDialog(); + Q_INVOKABLE void OpenSoftwareKeyboardDialog(); Q_INVOKABLE void ShowErrorDialog(QString message); /// Index of the buttons @@ -66,5 +160,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 ea474d30e..381b14229 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 b2a459071..e11e28ccd 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, diff --git a/src/core/frontend/applets/swkbd.h b/src/core/frontend/applets/swkbd.h index 82a1a2edb..603e3e7e9 100644 --- a/src/core/frontend/applets/swkbd.h +++ b/src/core/frontend/applets/swkbd.h @@ -32,6 +32,13 @@ enum class ButtonConfig { None, /// No button (returned by swkbdInputText in special cases) }; +enum class KeyboardType { + Normal, + QWERTY, + NumPad, + Western, +}; + /// Default English button text mappings. Frontends may need to copy this to internationalize it. constexpr char SWKBD_BUTTON_OKAY[] = "Ok"; constexpr char SWKBD_BUTTON_CANCEL[] = "Cancel"; @@ -40,6 +47,7 @@ constexpr char SWKBD_BUTTON_FORGOT[] = "I Forgot"; /// Configuration thats relevent to frontend implementation of applets. Anything missing that we /// later learn is needed can be added here and filled in by the backend HLE applet struct KeyboardConfig { + KeyboardType type = KeyboardType::Normal; ButtonConfig button_config; AcceptedInput accept_mode; /// What kinds of input are accepted (blank/empty/fixed width) bool multiline_mode; /// True if the keyboard accepts multiple lines of input diff --git a/src/core/hle/applets/swkbd.cpp b/src/core/hle/applets/swkbd.cpp index c4aceb10f..65b4256ea 100644 --- a/src/core/hle/applets/swkbd.cpp +++ b/src/core/hle/applets/swkbd.cpp @@ -178,6 +178,7 @@ Frontend::KeyboardConfig SoftwareKeyboard::ToFrontendConfig( const SoftwareKeyboardConfig& config) const { using namespace Frontend; KeyboardConfig frontend_config; + frontend_config.type = static_cast(static_cast(config.type)); frontend_config.button_config = static_cast(static_cast(config.num_buttons_m1)); frontend_config.accept_mode = static_cast(static_cast(config.valid_input));