This commit is contained in:
rpnesseling 2026-06-05 12:00:56 +00:00 committed by GitHub
commit b30e1cf341
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 501 additions and 15 deletions

View file

@ -2,14 +2,86 @@
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include <algorithm>
#include <iterator>
#include <limits>
#include <unordered_map>
#include <utility>
#include <QDialogButtonBox>
#include <QGridLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QLineEdit>
#include <QMessageBox>
#include <QPushButton>
#include <QString>
#include <QStyle>
#include <QTimer>
#include <QVBoxLayout>
#include "citra_qt/applets/swkbd.h"
#include "citra_qt/uisettings.h"
#include "common/param_package.h"
#include "common/settings.h"
#ifdef HAVE_SDL2
#include <SDL.h>
#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<Input::ButtonDevice> 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<Input::ButtonDevice>(params);
}
std::unique_ptr<Input::AnalogDevice> 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<Input::AnalogDevice>(params);
}
QString GetValidationErrorMessage(Frontend::ValidationError error, int max_text_length) {
using namespace Frontend;
const std::unordered_map<ValidationError, QString> 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_) {}
@ -23,6 +95,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::Action> SoftwareKeyboardInputInterpreter::Poll() {
PumpAppletInputEvents();
std::vector<Action> actions;
const std::array<Action, NumButtons> 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<std::pair<bool, Action>, 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 +205,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 +213,255 @@ void QtKeyboardDialog::Submit() {
}
}
void QtKeyboardDialog::HandleValidationError(Frontend::ValidationError error) {
using namespace Frontend;
const std::unordered_map<ValidationError, QString> 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);
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));
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) {
std::vector<QPushButton*> 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);
if (value.front().isLetter()) {
letter_buttons.push_back(button);
}
connect(button, &QPushButton::clicked, this, [this, button] {
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;
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(); });
button_rows.push_back({shift_button, space, backspace, ok, 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();
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());
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);
}
void QtSoftwareKeyboardDialog::MoveSelection(int row_delta, int column_delta) {
int next_row = selected_row + row_delta;
if (next_row < 0) {
next_row = static_cast<int>(button_rows.size()) - 1;
} else if (next_row >= static_cast<int>(button_rows.size())) {
next_row = 0;
}
const int row_size = static_cast<int>(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<int>::max();
for (int column = 0; column < static_cast<int>(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];
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<int>(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_) {}
@ -103,7 +471,11 @@ void QtKeyboard::Execute(const Frontend::KeyboardConfig& config) {
if (this->config.button_config != Frontend::ButtonConfig::None) {
ok_id = static_cast<u8>(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 +498,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);
}

View file

@ -4,13 +4,19 @@
#pragma once
#include <array>
#include <memory>
#include <vector>
#include <QDialog>
#include <QValidator>
#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<Action> Poll();
private:
enum Button {
ButtonA,
ButtonB,
ButtonUp,
ButtonDown,
ButtonLeft,
ButtonRight,
NumButtons,
};
enum Direction {
DirectionUp,
DirectionDown,
DirectionLeft,
DirectionRight,
NumDirections,
};
std::array<std::unique_ptr<Input::ButtonDevice>, NumButtons> buttons;
std::unique_ptr<Input::AnalogDevice> circle_pad;
std::array<bool, NumButtons> previous_button_state{};
std::array<bool, NumDirections> 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,46 @@ 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();
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);
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<std::vector<QPushButton*>> button_rows;
std::vector<QPushButton*> letter_buttons;
SoftwareKeyboardInputInterpreter input_interpreter;
int selected_row = 0;
int selected_column = 0;
bool uppercase = true;
friend class QtKeyboard;
};
class QtKeyboard final : public QObject, public Frontend::SoftwareKeyboard {
Q_OBJECT
@ -53,6 +137,7 @@ public:
private:
Q_INVOKABLE void OpenInputDialog();
Q_INVOKABLE void OpenSoftwareKeyboardDialog();
Q_INVOKABLE void ShowErrorDialog(QString message);
/// Index of the buttons
@ -66,5 +151,6 @@ private:
int result_button;
friend class QtKeyboardDialog;
friend class QtSoftwareKeyboardDialog;
friend class QtKeyboardValidator;
};

View file

@ -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();

View file

@ -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(),

View file

@ -1057,6 +1057,13 @@
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="use_onscreen_keyboard">
<property name="text">
<string>Use on-screen software keyboard</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>

View file

@ -89,6 +89,8 @@ struct Values {
Settings::Setting<bool> pause_when_in_background{false, "pauseWhenInBackground"};
Settings::Setting<bool> mute_when_in_background{false, "muteWhenInBackground"};
Settings::Setting<bool> hide_mouse{false, "hideInactiveMouse"};
Settings::Setting<bool> use_on_screen_software_keyboard{false,
"useOnScreenSoftwareKeyboard"};
#ifdef ENABLE_QT_UPDATE_CHECKER
Settings::Setting<bool> check_for_update_on_start{true, "check_for_update_on_start"};
Settings::Setting<int> update_check_channel{UpdateCheckChannels::STABLE,