From 0f40c8c6340aa858cd2e2ffe2e6c54885e0a3649 Mon Sep 17 00:00:00 2001 From: Morph <39850852+Morph1984@users.noreply.github.com> Date: Sat, 30 Jan 2021 14:38:00 -0500 Subject: [PATCH 01/14] applets: Remove the previous software keyboard applet implementation --- .../frontend/applets/software_keyboard.cpp | 20 +- src/core/frontend/applets/software_keyboard.h | 34 ---- .../service/am/applets/software_keyboard.cpp | 182 +----------------- .../service/am/applets/software_keyboard.h | 51 +---- src/yuzu/applets/software_keyboard.cpp | 143 +------------- src/yuzu/applets/software_keyboard.h | 48 +---- src/yuzu/main.cpp | 23 --- src/yuzu/main.h | 5 - 8 files changed, 14 insertions(+), 492 deletions(-) diff --git a/src/core/frontend/applets/software_keyboard.cpp b/src/core/frontend/applets/software_keyboard.cpp index 856ed33da5..73e7a89b99 100644 --- a/src/core/frontend/applets/software_keyboard.cpp +++ b/src/core/frontend/applets/software_keyboard.cpp @@ -2,28 +2,10 @@ // Licensed under GPLv2 or any later version // Refer to the license.txt file included. -#include "common/logging/backend.h" -#include "common/string_util.h" #include "core/frontend/applets/software_keyboard.h" namespace Core::Frontend { + SoftwareKeyboardApplet::~SoftwareKeyboardApplet() = default; -void DefaultSoftwareKeyboardApplet::RequestText( - std::function)> out, - SoftwareKeyboardParameters parameters) const { - if (parameters.initial_text.empty()) - out(u"yuzu"); - - out(parameters.initial_text); -} - -void DefaultSoftwareKeyboardApplet::SendTextCheckDialog( - std::u16string error_message, std::function finished_check) const { - LOG_WARNING(Service_AM, - "(STUBBED) called - Default fallback software keyboard does not support text " - "check! (error_message={})", - Common::UTF16ToUTF8(error_message)); - finished_check(); -} } // namespace Core::Frontend diff --git a/src/core/frontend/applets/software_keyboard.h b/src/core/frontend/applets/software_keyboard.h index f9b2026642..54528837ee 100644 --- a/src/core/frontend/applets/software_keyboard.h +++ b/src/core/frontend/applets/software_keyboard.h @@ -4,51 +4,17 @@ #pragma once -#include -#include -#include -#include "common/bit_field.h" #include "common/common_types.h" namespace Core::Frontend { -struct SoftwareKeyboardParameters { - std::u16string submit_text; - std::u16string header_text; - std::u16string sub_text; - std::u16string guide_text; - std::u16string initial_text; - std::size_t max_length; - bool password; - bool cursor_at_beginning; - - union { - u8 value; - - BitField<1, 1, u8> disable_space; - BitField<2, 1, u8> disable_address; - BitField<3, 1, u8> disable_percent; - BitField<4, 1, u8> disable_slash; - BitField<6, 1, u8> disable_number; - BitField<7, 1, u8> disable_download_code; - }; -}; class SoftwareKeyboardApplet { public: virtual ~SoftwareKeyboardApplet(); - - virtual void RequestText(std::function)> out, - SoftwareKeyboardParameters parameters) const = 0; - virtual void SendTextCheckDialog(std::u16string error_message, - std::function finished_check) const = 0; }; class DefaultSoftwareKeyboardApplet final : public SoftwareKeyboardApplet { public: - void RequestText(std::function)> out, - SoftwareKeyboardParameters parameters) const override; - void SendTextCheckDialog(std::u16string error_message, - std::function finished_check) const override; }; } // namespace Core::Frontend diff --git a/src/core/hle/service/am/applets/software_keyboard.cpp b/src/core/hle/service/am/applets/software_keyboard.cpp index 79b209c6b9..f966cf67b5 100644 --- a/src/core/hle/service/am/applets/software_keyboard.cpp +++ b/src/core/hle/service/am/applets/software_keyboard.cpp @@ -2,199 +2,27 @@ // Licensed under GPLv2 or any later version // Refer to the license.txt file included. -#include -#include "common/assert.h" -#include "common/string_util.h" #include "core/core.h" #include "core/frontend/applets/software_keyboard.h" -#include "core/hle/result.h" #include "core/hle/service/am/am.h" #include "core/hle/service/am/applets/software_keyboard.h" namespace Service::AM::Applets { -namespace { -enum class Request : u32 { - Finalize = 0x4, - SetUserWordInfo = 0x6, - SetCustomizeDic = 0x7, - Calc = 0xa, - SetCustomizedDictionaries = 0xb, - UnsetCustomizedDictionaries = 0xc, - UnknownD = 0xd, - UnknownE = 0xe, -}; -constexpr std::size_t SWKBD_INLINE_INIT_SIZE = 0x8; -constexpr std::size_t SWKBD_OUTPUT_BUFFER_SIZE = 0x7D8; -constexpr std::size_t SWKBD_OUTPUT_INTERACTIVE_BUFFER_SIZE = 0x7D4; -constexpr std::size_t DEFAULT_MAX_LENGTH = 500; -constexpr bool INTERACTIVE_STATUS_OK = false; -} // Anonymous namespace -static Core::Frontend::SoftwareKeyboardParameters ConvertToFrontendParameters( - KeyboardConfig config, std::u16string initial_text) { - Core::Frontend::SoftwareKeyboardParameters params{}; - - params.submit_text = Common::UTF16StringFromFixedZeroTerminatedBuffer( - config.submit_text.data(), config.submit_text.size()); - params.header_text = Common::UTF16StringFromFixedZeroTerminatedBuffer( - config.header_text.data(), config.header_text.size()); - params.sub_text = Common::UTF16StringFromFixedZeroTerminatedBuffer(config.sub_text.data(), - config.sub_text.size()); - params.guide_text = Common::UTF16StringFromFixedZeroTerminatedBuffer(config.guide_text.data(), - config.guide_text.size()); - params.initial_text = std::move(initial_text); - params.max_length = config.length_limit == 0 ? DEFAULT_MAX_LENGTH : config.length_limit; - params.password = static_cast(config.is_password); - params.cursor_at_beginning = static_cast(config.initial_cursor_position); - params.value = static_cast(config.keyset_disable_bitmask); - - return params; -} - SoftwareKeyboard::SoftwareKeyboard(Core::System& system_, const Core::Frontend::SoftwareKeyboardApplet& frontend_) : Applet{system_.Kernel()}, frontend{frontend_}, system{system_} {} SoftwareKeyboard::~SoftwareKeyboard() = default; -void SoftwareKeyboard::Initialize() { - complete = false; - is_inline = false; - initial_text.clear(); - final_data.clear(); +void SoftwareKeyboard::Initialize() {} - Applet::Initialize(); +bool SoftwareKeyboard::TransactionComplete() const {} - const auto keyboard_config_storage = broker.PopNormalDataToApplet(); - ASSERT(keyboard_config_storage != nullptr); - const auto& keyboard_config = keyboard_config_storage->GetData(); +ResultCode SoftwareKeyboard::GetStatus() const {} - if (keyboard_config.size() == SWKBD_INLINE_INIT_SIZE) { - is_inline = true; - return; - } +void SoftwareKeyboard::ExecuteInteractive() {} - ASSERT(keyboard_config.size() >= sizeof(KeyboardConfig)); - std::memcpy(&config, keyboard_config.data(), sizeof(KeyboardConfig)); +void SoftwareKeyboard::Execute() {} - const auto work_buffer_storage = broker.PopNormalDataToApplet(); - ASSERT_OR_EXECUTE(work_buffer_storage != nullptr, { return; }); - const auto& work_buffer = work_buffer_storage->GetData(); - - if (config.initial_string_size == 0) - return; - - std::vector string(config.initial_string_size); - std::memcpy(string.data(), work_buffer.data() + config.initial_string_offset, - string.size() * 2); - initial_text = Common::UTF16StringFromFixedZeroTerminatedBuffer(string.data(), string.size()); -} - -bool SoftwareKeyboard::TransactionComplete() const { - return complete; -} - -ResultCode SoftwareKeyboard::GetStatus() const { - return RESULT_SUCCESS; -} - -void SoftwareKeyboard::ExecuteInteractive() { - if (complete) - return; - - const auto storage = broker.PopInteractiveDataToApplet(); - ASSERT(storage != nullptr); - const auto data = storage->GetData(); - if (!is_inline) { - const auto status = static_cast(data[0]); - if (status == INTERACTIVE_STATUS_OK) { - complete = true; - } else { - std::array string; - std::memcpy(string.data(), data.data() + 4, string.size() * 2); - frontend.SendTextCheckDialog( - Common::UTF16StringFromFixedZeroTerminatedBuffer(string.data(), string.size()), - [this] { broker.SignalStateChanged(); }); - } - } else { - Request request{}; - std::memcpy(&request, data.data(), sizeof(Request)); - - switch (request) { - case Request::Finalize: - complete = true; - broker.SignalStateChanged(); - break; - case Request::Calc: { - broker.PushNormalDataFromApplet(std::make_shared(system, std::vector{1})); - broker.SignalStateChanged(); - break; - } - default: - UNIMPLEMENTED_MSG("Request {:X} is not implemented", request); - break; - } - } -} - -void SoftwareKeyboard::Execute() { - if (complete) { - broker.PushNormalDataFromApplet(std::make_shared(system, std::move(final_data))); - broker.SignalStateChanged(); - return; - } - - const auto parameters = ConvertToFrontendParameters(config, initial_text); - if (!is_inline) { - frontend.RequestText( - [this](std::optional text) { WriteText(std::move(text)); }, parameters); - } -} - -void SoftwareKeyboard::WriteText(std::optional text) { - std::vector output_main(SWKBD_OUTPUT_BUFFER_SIZE); - - if (text.has_value()) { - std::vector output_sub(SWKBD_OUTPUT_BUFFER_SIZE); - - if (config.utf_8) { - const u64 size = text->size() + sizeof(u64); - const auto new_text = Common::UTF16ToUTF8(*text); - - std::memcpy(output_sub.data(), &size, sizeof(u64)); - std::memcpy(output_sub.data() + 8, new_text.data(), - std::min(new_text.size(), SWKBD_OUTPUT_BUFFER_SIZE - 8)); - - output_main[0] = INTERACTIVE_STATUS_OK; - std::memcpy(output_main.data() + 4, new_text.data(), - std::min(new_text.size(), SWKBD_OUTPUT_BUFFER_SIZE - 4)); - } else { - const u64 size = text->size() * 2 + sizeof(u64); - std::memcpy(output_sub.data(), &size, sizeof(u64)); - std::memcpy(output_sub.data() + 8, text->data(), - std::min(text->size() * 2, SWKBD_OUTPUT_BUFFER_SIZE - 8)); - - output_main[0] = INTERACTIVE_STATUS_OK; - std::memcpy(output_main.data() + 4, text->data(), - std::min(text->size() * 2, SWKBD_OUTPUT_BUFFER_SIZE - 4)); - } - - complete = !config.text_check; - final_data = output_main; - - if (complete) { - broker.PushNormalDataFromApplet( - std::make_shared(system, std::move(output_main))); - broker.SignalStateChanged(); - } else { - broker.PushInteractiveDataFromApplet( - std::make_shared(system, std::move(output_sub))); - } - } else { - output_main[0] = 1; - complete = true; - broker.PushNormalDataFromApplet(std::make_shared(system, std::move(output_main))); - broker.SignalStateChanged(); - } -} } // namespace Service::AM::Applets diff --git a/src/core/hle/service/am/applets/software_keyboard.h b/src/core/hle/service/am/applets/software_keyboard.h index 1d260fef80..c161ec9ac8 100644 --- a/src/core/hle/service/am/applets/software_keyboard.h +++ b/src/core/hle/service/am/applets/software_keyboard.h @@ -4,59 +4,17 @@ #pragma once -#include -#include -#include - #include "common/common_funcs.h" #include "common/common_types.h" -#include "common/swap.h" -#include "core/hle/service/am/am.h" +#include "core/hle/result.h" #include "core/hle/service/am/applets/applets.h" -union ResultCode; - namespace Core { class System; } namespace Service::AM::Applets { -enum class KeysetDisable : u32 { - Space = 0x02, - Address = 0x04, - Percent = 0x08, - Slashes = 0x10, - Numbers = 0x40, - DownloadCode = 0x80, -}; - -struct KeyboardConfig { - INSERT_PADDING_BYTES(4); - std::array submit_text; - u16_le left_symbol_key; - u16_le right_symbol_key; - INSERT_PADDING_BYTES(1); - KeysetDisable keyset_disable_bitmask; - u32_le initial_cursor_position; - std::array header_text; - std::array sub_text; - std::array guide_text; - u32_le length_limit; - INSERT_PADDING_BYTES(4); - u32_le is_password; - INSERT_PADDING_BYTES(5); - bool utf_8; - bool draw_background; - u32_le initial_string_offset; - u32_le initial_string_size; - u32_le user_dictionary_offset; - u32_le user_dictionary_size; - bool text_check; - u64_le text_check_callback; -}; -static_assert(sizeof(KeyboardConfig) == 0x3E0, "KeyboardConfig has incorrect size."); - class SoftwareKeyboard final : public Applet { public: explicit SoftwareKeyboard(Core::System& system_, @@ -70,16 +28,9 @@ public: void ExecuteInteractive() override; void Execute() override; - void WriteText(std::optional text); - private: const Core::Frontend::SoftwareKeyboardApplet& frontend; - KeyboardConfig config; - std::u16string initial_text; - bool complete = false; - bool is_inline = false; - std::vector final_data; Core::System& system; }; diff --git a/src/yuzu/applets/software_keyboard.cpp b/src/yuzu/applets/software_keyboard.cpp index ab8cfd8ee1..da0fed774e 100644 --- a/src/yuzu/applets/software_keyboard.cpp +++ b/src/yuzu/applets/software_keyboard.cpp @@ -2,152 +2,17 @@ // Licensed under GPLv2 or any later version // Refer to the license.txt file included. -#include -#include -#include -#include -#include -#include -#include -#include "core/hle/lock.h" #include "yuzu/applets/software_keyboard.h" #include "yuzu/main.h" -QtSoftwareKeyboardValidator::QtSoftwareKeyboardValidator( - Core::Frontend::SoftwareKeyboardParameters parameters) - : parameters(std::move(parameters)) {} +QtSoftwareKeyboardValidator::QtSoftwareKeyboardValidator() {} -QValidator::State QtSoftwareKeyboardValidator::validate(QString& input, int& pos) const { - if (input.size() > static_cast(parameters.max_length)) { - return Invalid; - } - if (parameters.disable_space && input.contains(QLatin1Char{' '})) { - return Invalid; - } - if (parameters.disable_address && input.contains(QLatin1Char{'@'})) { - return Invalid; - } - if (parameters.disable_percent && input.contains(QLatin1Char{'%'})) { - return Invalid; - } - if (parameters.disable_slash && - (input.contains(QLatin1Char{'/'}) || input.contains(QLatin1Char{'\\'}))) { - return Invalid; - } - if (parameters.disable_number && - std::any_of(input.begin(), input.end(), [](QChar c) { return c.isDigit(); })) { - return Invalid; - } +QValidator::State QtSoftwareKeyboardValidator::validate(QString& input, int& pos) const {} - if (parameters.disable_download_code && std::any_of(input.begin(), input.end(), [](QChar c) { - return c == QLatin1Char{'O'} || c == QLatin1Char{'I'}; - })) { - return Invalid; - } - - return Acceptable; -} - -QtSoftwareKeyboardDialog::QtSoftwareKeyboardDialog( - QWidget* parent, Core::Frontend::SoftwareKeyboardParameters parameters_) - : QDialog(parent), parameters(std::move(parameters_)) { - layout = new QVBoxLayout; - - header_label = new QLabel(QString::fromStdU16String(parameters.header_text)); - header_label->setFont({header_label->font().family(), 11, QFont::Bold}); - if (header_label->text().isEmpty()) - header_label->setText(tr("Enter text:")); - - sub_label = new QLabel(QString::fromStdU16String(parameters.sub_text)); - sub_label->setFont({sub_label->font().family(), sub_label->font().pointSize(), - sub_label->font().weight(), true}); - sub_label->setHidden(parameters.sub_text.empty()); - - guide_label = new QLabel(QString::fromStdU16String(parameters.guide_text)); - guide_label->setHidden(parameters.guide_text.empty()); - - length_label = new QLabel(QStringLiteral("0/%1").arg(parameters.max_length)); - length_label->setAlignment(Qt::AlignRight); - length_label->setFont({length_label->font().family(), 8}); - - line_edit = new QLineEdit; - line_edit->setValidator(new QtSoftwareKeyboardValidator(parameters)); - line_edit->setMaxLength(static_cast(parameters.max_length)); - line_edit->setText(QString::fromStdU16String(parameters.initial_text)); - line_edit->setCursorPosition( - parameters.cursor_at_beginning ? 0 : static_cast(parameters.initial_text.size())); - line_edit->setEchoMode(parameters.password ? QLineEdit::Password : QLineEdit::Normal); - - connect(line_edit, &QLineEdit::textChanged, this, [this](const QString& text) { - length_label->setText(QStringLiteral("%1/%2").arg(text.size()).arg(parameters.max_length)); - }); - - buttons = new QDialogButtonBox(QDialogButtonBox::Cancel); - if (parameters.submit_text.empty()) { - buttons->addButton(QDialogButtonBox::Ok); - } else { - buttons->addButton(QString::fromStdU16String(parameters.submit_text), - QDialogButtonBox::AcceptRole); - } - connect(buttons, &QDialogButtonBox::accepted, this, &QtSoftwareKeyboardDialog::accept); - connect(buttons, &QDialogButtonBox::rejected, this, &QtSoftwareKeyboardDialog::reject); - layout->addWidget(header_label); - layout->addWidget(sub_label); - layout->addWidget(guide_label); - layout->addWidget(length_label); - layout->addWidget(line_edit); - layout->addWidget(buttons); - setLayout(layout); - setWindowTitle(tr("Software Keyboard")); -} +QtSoftwareKeyboardDialog::QtSoftwareKeyboardDialog(QWidget* parent) : QDialog(parent) {} QtSoftwareKeyboardDialog::~QtSoftwareKeyboardDialog() = default; -void QtSoftwareKeyboardDialog::accept() { - text = line_edit->text().toStdU16String(); - QDialog::accept(); -} - -void QtSoftwareKeyboardDialog::reject() { - text.clear(); - QDialog::reject(); -} - -std::u16string QtSoftwareKeyboardDialog::GetText() const { - return text; -} - -QtSoftwareKeyboard::QtSoftwareKeyboard(GMainWindow& main_window) { - connect(this, &QtSoftwareKeyboard::MainWindowGetText, &main_window, - &GMainWindow::SoftwareKeyboardGetText, Qt::QueuedConnection); - connect(this, &QtSoftwareKeyboard::MainWindowTextCheckDialog, &main_window, - &GMainWindow::SoftwareKeyboardInvokeCheckDialog, Qt::BlockingQueuedConnection); - connect(&main_window, &GMainWindow::SoftwareKeyboardFinishedText, this, - &QtSoftwareKeyboard::MainWindowFinishedText, Qt::QueuedConnection); -} +QtSoftwareKeyboard::QtSoftwareKeyboard(GMainWindow& main_window) {} QtSoftwareKeyboard::~QtSoftwareKeyboard() = default; - -void QtSoftwareKeyboard::RequestText(std::function)> out, - Core::Frontend::SoftwareKeyboardParameters parameters) const { - text_output = std::move(out); - emit MainWindowGetText(parameters); -} - -void QtSoftwareKeyboard::SendTextCheckDialog(std::u16string error_message, - std::function finished_check_) const { - finished_check = std::move(finished_check_); - emit MainWindowTextCheckDialog(error_message); -} - -void QtSoftwareKeyboard::MainWindowFinishedText(std::optional text) { - // Acquire the HLE mutex - std::lock_guard lock{HLE::g_hle_lock}; - text_output(std::move(text)); -} - -void QtSoftwareKeyboard::MainWindowFinishedCheckDialog() { - // Acquire the HLE mutex - std::lock_guard lock{HLE::g_hle_lock}; - finished_check(); -} diff --git a/src/yuzu/applets/software_keyboard.h b/src/yuzu/applets/software_keyboard.h index 9e1094cce1..8427c0a6ca 100644 --- a/src/yuzu/applets/software_keyboard.h +++ b/src/yuzu/applets/software_keyboard.h @@ -6,49 +6,23 @@ #include #include + #include "core/frontend/applets/software_keyboard.h" class GMainWindow; -class QDialogButtonBox; -class QLabel; -class QLineEdit; -class QVBoxLayout; -class QtSoftwareKeyboard; class QtSoftwareKeyboardValidator final : public QValidator { public: - explicit QtSoftwareKeyboardValidator(Core::Frontend::SoftwareKeyboardParameters parameters); + explicit QtSoftwareKeyboardValidator(); State validate(QString& input, int& pos) const override; - -private: - Core::Frontend::SoftwareKeyboardParameters parameters; }; class QtSoftwareKeyboardDialog final : public QDialog { Q_OBJECT public: - QtSoftwareKeyboardDialog(QWidget* parent, - Core::Frontend::SoftwareKeyboardParameters parameters); + QtSoftwareKeyboardDialog(QWidget* parent); ~QtSoftwareKeyboardDialog() override; - - void accept() override; - void reject() override; - - std::u16string GetText() const; - -private: - std::u16string text; - - QDialogButtonBox* buttons; - QLabel* header_label; - QLabel* sub_label; - QLabel* guide_label; - QLabel* length_label; - QLineEdit* line_edit; - QVBoxLayout* layout; - - Core::Frontend::SoftwareKeyboardParameters parameters; }; class QtSoftwareKeyboard final : public QObject, public Core::Frontend::SoftwareKeyboardApplet { @@ -57,20 +31,4 @@ class QtSoftwareKeyboard final : public QObject, public Core::Frontend::Software public: explicit QtSoftwareKeyboard(GMainWindow& parent); ~QtSoftwareKeyboard() override; - - void RequestText(std::function)> out, - Core::Frontend::SoftwareKeyboardParameters parameters) const override; - void SendTextCheckDialog(std::u16string error_message, - std::function finished_check_) const override; - -signals: - void MainWindowGetText(Core::Frontend::SoftwareKeyboardParameters parameters) const; - void MainWindowTextCheckDialog(std::u16string error_message) const; - -private: - void MainWindowFinishedText(std::optional text); - void MainWindowFinishedCheckDialog(); - - mutable std::function)> text_output; - mutable std::function finished_check; }; diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp index fbf96be03a..e9d6e74211 100644 --- a/src/yuzu/main.cpp +++ b/src/yuzu/main.cpp @@ -414,27 +414,6 @@ void GMainWindow::ProfileSelectorSelectProfile() { emit ProfileSelectorFinishedSelection(uuid); } -void GMainWindow::SoftwareKeyboardGetText( - const Core::Frontend::SoftwareKeyboardParameters& parameters) { - QtSoftwareKeyboardDialog dialog(this, parameters); - dialog.setWindowFlags(Qt::Dialog | Qt::CustomizeWindowHint | Qt::WindowStaysOnTopHint | - Qt::WindowTitleHint | Qt::WindowSystemMenuHint | - Qt::WindowCloseButtonHint); - dialog.setWindowModality(Qt::WindowModal); - - if (dialog.exec() == QDialog::Rejected) { - emit SoftwareKeyboardFinishedText(std::nullopt); - return; - } - - emit SoftwareKeyboardFinishedText(dialog.GetText()); -} - -void GMainWindow::SoftwareKeyboardInvokeCheckDialog(std::u16string error_message) { - QMessageBox::warning(this, tr("Text Check Failed"), QString::fromStdU16String(error_message)); - emit SoftwareKeyboardFinishedCheckDialog(); -} - void GMainWindow::WebBrowserOpenWebPage(std::string_view main_url, std::string_view additional_args, bool is_local) { #ifdef YUZU_USE_QT_WEB_ENGINE @@ -2188,8 +2167,6 @@ void GMainWindow::OnStartGame() { emu_thread->SetRunning(true); qRegisterMetaType("Core::Frontend::ControllerParameters"); - qRegisterMetaType( - "Core::Frontend::SoftwareKeyboardParameters"); qRegisterMetaType("Core::System::ResultStatus"); qRegisterMetaType("std::string"); qRegisterMetaType>("std::optional"); diff --git a/src/yuzu/main.h b/src/yuzu/main.h index 04d37d4ae5..6429549ae7 100644 --- a/src/yuzu/main.h +++ b/src/yuzu/main.h @@ -128,9 +128,6 @@ signals: void ProfileSelectorFinishedSelection(std::optional uuid); - void SoftwareKeyboardFinishedText(std::optional text); - void SoftwareKeyboardFinishedCheckDialog(); - void WebBrowserExtractOfflineRomFS(); void WebBrowserClosed(Service::AM::Applets::WebExitReason exit_reason, std::string last_url); @@ -141,8 +138,6 @@ public slots: const Core::Frontend::ControllerParameters& parameters); void ErrorDisplayDisplayError(QString body); void ProfileSelectorSelectProfile(); - void SoftwareKeyboardGetText(const Core::Frontend::SoftwareKeyboardParameters& parameters); - void SoftwareKeyboardInvokeCheckDialog(std::u16string error_message); void WebBrowserOpenWebPage(std::string_view main_url, std::string_view additional_args, bool is_local); void OnAppFocusStateChanged(Qt::ApplicationState state); From d1e40dd24427582b0329e6470c21a4e774afb400 Mon Sep 17 00:00:00 2001 From: Morph <39850852+Morph1984@users.noreply.github.com> Date: Sat, 6 Feb 2021 02:31:13 -0500 Subject: [PATCH 02/14] applets: Pass in the LibraryAppletMode each applet's constructor --- src/core/hle/service/am/am.cpp | 4 ++-- src/core/hle/service/am/applets/applets.cpp | 18 +++++++++--------- src/core/hle/service/am/applets/applets.h | 10 +++++++++- src/core/hle/service/am/applets/controller.cpp | 5 +++-- src/core/hle/service/am/applets/controller.h | 4 +++- src/core/hle/service/am/applets/error.cpp | 5 +++-- src/core/hle/service/am/applets/error.h | 4 +++- .../hle/service/am/applets/general_backend.cpp | 14 ++++++++------ .../hle/service/am/applets/general_backend.h | 11 ++++++++--- .../hle/service/am/applets/profile_select.cpp | 4 ++-- .../hle/service/am/applets/profile_select.h | 3 ++- .../hle/service/am/applets/web_browser.cpp | 5 +++-- src/core/hle/service/am/applets/web_browser.h | 4 +++- 13 files changed, 58 insertions(+), 33 deletions(-) diff --git a/src/core/hle/service/am/am.cpp b/src/core/hle/service/am/am.cpp index 416c5239ac..836fa564e2 100644 --- a/src/core/hle/service/am/am.cpp +++ b/src/core/hle/service/am/am.cpp @@ -1135,13 +1135,13 @@ ILibraryAppletCreator::~ILibraryAppletCreator() = default; void ILibraryAppletCreator::CreateLibraryApplet(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp{ctx}; const auto applet_id = rp.PopRaw(); - const auto applet_mode = rp.PopRaw(); + const auto applet_mode = rp.PopRaw(); LOG_DEBUG(Service_AM, "called with applet_id={:08X}, applet_mode={:08X}", applet_id, applet_mode); const auto& applet_manager{system.GetAppletManager()}; - const auto applet = applet_manager.GetApplet(applet_id); + const auto applet = applet_manager.GetApplet(applet_id, applet_mode); if (applet == nullptr) { LOG_ERROR(Service_AM, "Applet doesn't exist! applet_id={}", applet_id); diff --git a/src/core/hle/service/am/applets/applets.cpp b/src/core/hle/service/am/applets/applets.cpp index e2f3b75631..5ddad851ae 100644 --- a/src/core/hle/service/am/applets/applets.cpp +++ b/src/core/hle/service/am/applets/applets.cpp @@ -241,31 +241,31 @@ void AppletManager::ClearAll() { frontend = {}; } -std::shared_ptr AppletManager::GetApplet(AppletId id) const { +std::shared_ptr AppletManager::GetApplet(AppletId id, LibraryAppletMode mode) const { switch (id) { case AppletId::Auth: - return std::make_shared(system, *frontend.parental_controls); + return std::make_shared(system, mode, *frontend.parental_controls); case AppletId::Controller: - return std::make_shared(system, *frontend.controller); + return std::make_shared(system, mode, *frontend.controller); case AppletId::Error: - return std::make_shared(system, *frontend.error); + return std::make_shared(system, mode, *frontend.error); case AppletId::ProfileSelect: - return std::make_shared(system, *frontend.profile_select); + return std::make_shared(system, mode, *frontend.profile_select); case AppletId::SoftwareKeyboard: - return std::make_shared(system, *frontend.software_keyboard); + return std::make_shared(system, mode, *frontend.software_keyboard); case AppletId::Web: case AppletId::Shop: case AppletId::OfflineWeb: case AppletId::LoginShare: case AppletId::WebAuth: - return std::make_shared(system, *frontend.web_browser); + return std::make_shared(system, mode, *frontend.web_browser); case AppletId::PhotoViewer: - return std::make_shared(system, *frontend.photo_viewer); + return std::make_shared(system, mode, *frontend.photo_viewer); default: UNIMPLEMENTED_MSG( "No backend implementation exists for applet_id={:02X}! Falling back to stub applet.", static_cast(id)); - return std::make_shared(system, id); + return std::make_shared(system, id, mode); } } diff --git a/src/core/hle/service/am/applets/applets.h b/src/core/hle/service/am/applets/applets.h index b9a006317a..26b4820158 100644 --- a/src/core/hle/service/am/applets/applets.h +++ b/src/core/hle/service/am/applets/applets.h @@ -62,6 +62,14 @@ enum class AppletId : u32 { MyPage = 0x1A, }; +enum class LibraryAppletMode : u32 { + AllForeground = 0, + Background = 1, + NoUI = 2, + BackgroundIndirectDisplay = 3, + AllForegroundInitiallyHidden = 4, +}; + class AppletDataBroker final { public: explicit AppletDataBroker(Kernel::KernelCore& kernel_); @@ -200,7 +208,7 @@ public: void SetDefaultAppletsIfMissing(); void ClearAll(); - std::shared_ptr GetApplet(AppletId id) const; + std::shared_ptr GetApplet(AppletId id, LibraryAppletMode mode) const; private: AppletFrontendSet frontend; diff --git a/src/core/hle/service/am/applets/controller.cpp b/src/core/hle/service/am/applets/controller.cpp index c2bfe698f5..a33f05f97c 100644 --- a/src/core/hle/service/am/applets/controller.cpp +++ b/src/core/hle/service/am/applets/controller.cpp @@ -45,8 +45,9 @@ static Core::Frontend::ControllerParameters ConvertToFrontendParameters( }; } -Controller::Controller(Core::System& system_, const Core::Frontend::ControllerApplet& frontend_) - : Applet{system_.Kernel()}, frontend{frontend_}, system{system_} {} +Controller::Controller(Core::System& system_, LibraryAppletMode applet_mode_, + const Core::Frontend::ControllerApplet& frontend_) + : Applet{system_.Kernel()}, applet_mode{applet_mode_}, frontend{frontend_}, system{system_} {} Controller::~Controller() = default; diff --git a/src/core/hle/service/am/applets/controller.h b/src/core/hle/service/am/applets/controller.h index d4c9da7b13..07cb92bf94 100644 --- a/src/core/hle/service/am/applets/controller.h +++ b/src/core/hle/service/am/applets/controller.h @@ -106,7 +106,8 @@ static_assert(sizeof(ControllerSupportResultInfo) == 0xC, class Controller final : public Applet { public: - explicit Controller(Core::System& system_, const Core::Frontend::ControllerApplet& frontend_); + explicit Controller(Core::System& system_, LibraryAppletMode applet_mode_, + const Core::Frontend::ControllerApplet& frontend_); ~Controller() override; void Initialize() override; @@ -119,6 +120,7 @@ public: void ConfigurationComplete(); private: + LibraryAppletMode applet_mode; const Core::Frontend::ControllerApplet& frontend; Core::System& system; diff --git a/src/core/hle/service/am/applets/error.cpp b/src/core/hle/service/am/applets/error.cpp index 0c8b632e8b..a9f0a9c950 100644 --- a/src/core/hle/service/am/applets/error.cpp +++ b/src/core/hle/service/am/applets/error.cpp @@ -86,8 +86,9 @@ ResultCode Decode64BitError(u64 error) { } // Anonymous namespace -Error::Error(Core::System& system_, const Core::Frontend::ErrorApplet& frontend_) - : Applet{system_.Kernel()}, frontend{frontend_}, system{system_} {} +Error::Error(Core::System& system_, LibraryAppletMode applet_mode_, + const Core::Frontend::ErrorApplet& frontend_) + : Applet{system_.Kernel()}, applet_mode{applet_mode_}, frontend{frontend_}, system{system_} {} Error::~Error() = default; diff --git a/src/core/hle/service/am/applets/error.h b/src/core/hle/service/am/applets/error.h index a105cdb0c8..a3e520cd49 100644 --- a/src/core/hle/service/am/applets/error.h +++ b/src/core/hle/service/am/applets/error.h @@ -25,7 +25,8 @@ enum class ErrorAppletMode : u8 { class Error final : public Applet { public: - explicit Error(Core::System& system_, const Core::Frontend::ErrorApplet& frontend_); + explicit Error(Core::System& system_, LibraryAppletMode applet_mode_, + const Core::Frontend::ErrorApplet& frontend_); ~Error() override; void Initialize() override; @@ -40,6 +41,7 @@ public: private: union ErrorArguments; + LibraryAppletMode applet_mode; const Core::Frontend::ErrorApplet& frontend; ResultCode error_code = RESULT_SUCCESS; ErrorAppletMode mode = ErrorAppletMode::ShowError; diff --git a/src/core/hle/service/am/applets/general_backend.cpp b/src/core/hle/service/am/applets/general_backend.cpp index 4d1df5cbeb..71016cce74 100644 --- a/src/core/hle/service/am/applets/general_backend.cpp +++ b/src/core/hle/service/am/applets/general_backend.cpp @@ -37,8 +37,9 @@ static void LogCurrentStorage(AppletDataBroker& broker, std::string_view prefix) } } -Auth::Auth(Core::System& system_, Core::Frontend::ParentalControlsApplet& frontend_) - : Applet{system_.Kernel()}, frontend{frontend_}, system{system_} {} +Auth::Auth(Core::System& system_, LibraryAppletMode applet_mode_, + Core::Frontend::ParentalControlsApplet& frontend_) + : Applet{system_.Kernel()}, applet_mode{applet_mode_}, frontend{frontend_}, system{system_} {} Auth::~Auth() = default; @@ -152,8 +153,9 @@ void Auth::AuthFinished(bool is_successful) { broker.SignalStateChanged(); } -PhotoViewer::PhotoViewer(Core::System& system_, const Core::Frontend::PhotoViewerApplet& frontend_) - : Applet{system_.Kernel()}, frontend{frontend_}, system{system_} {} +PhotoViewer::PhotoViewer(Core::System& system_, LibraryAppletMode applet_mode_, + const Core::Frontend::PhotoViewerApplet& frontend_) + : Applet{system_.Kernel()}, applet_mode{applet_mode_}, frontend{frontend_}, system{system_} {} PhotoViewer::~PhotoViewer() = default; @@ -202,8 +204,8 @@ void PhotoViewer::ViewFinished() { broker.SignalStateChanged(); } -StubApplet::StubApplet(Core::System& system_, AppletId id_) - : Applet{system_.Kernel()}, id{id_}, system{system_} {} +StubApplet::StubApplet(Core::System& system_, AppletId id_, LibraryAppletMode applet_mode_) + : Applet{system_.Kernel()}, id{id_}, applet_mode{applet_mode_}, system{system_} {} StubApplet::~StubApplet() = default; diff --git a/src/core/hle/service/am/applets/general_backend.h b/src/core/hle/service/am/applets/general_backend.h index ba76ae3d3d..d9e6d43843 100644 --- a/src/core/hle/service/am/applets/general_backend.h +++ b/src/core/hle/service/am/applets/general_backend.h @@ -20,7 +20,8 @@ enum class AuthAppletType : u32 { class Auth final : public Applet { public: - explicit Auth(Core::System& system_, Core::Frontend::ParentalControlsApplet& frontend_); + explicit Auth(Core::System& system_, LibraryAppletMode applet_mode_, + Core::Frontend::ParentalControlsApplet& frontend_); ~Auth() override; void Initialize() override; @@ -32,6 +33,7 @@ public: void AuthFinished(bool is_successful = true); private: + LibraryAppletMode applet_mode; Core::Frontend::ParentalControlsApplet& frontend; Core::System& system; bool complete = false; @@ -50,7 +52,8 @@ enum class PhotoViewerAppletMode : u8 { class PhotoViewer final : public Applet { public: - explicit PhotoViewer(Core::System& system_, const Core::Frontend::PhotoViewerApplet& frontend_); + explicit PhotoViewer(Core::System& system_, LibraryAppletMode applet_mode_, + const Core::Frontend::PhotoViewerApplet& frontend_); ~PhotoViewer() override; void Initialize() override; @@ -62,6 +65,7 @@ public: void ViewFinished(); private: + LibraryAppletMode applet_mode; const Core::Frontend::PhotoViewerApplet& frontend; bool complete = false; PhotoViewerAppletMode mode = PhotoViewerAppletMode::CurrentApp; @@ -70,7 +74,7 @@ private: class StubApplet final : public Applet { public: - explicit StubApplet(Core::System& system_, AppletId id_); + explicit StubApplet(Core::System& system_, AppletId id_, LibraryAppletMode applet_mode_); ~StubApplet() override; void Initialize() override; @@ -82,6 +86,7 @@ public: private: AppletId id; + LibraryAppletMode applet_mode; Core::System& system; }; diff --git a/src/core/hle/service/am/applets/profile_select.cpp b/src/core/hle/service/am/applets/profile_select.cpp index 77fba16c7d..ab8b6fcc5d 100644 --- a/src/core/hle/service/am/applets/profile_select.cpp +++ b/src/core/hle/service/am/applets/profile_select.cpp @@ -15,9 +15,9 @@ namespace Service::AM::Applets { constexpr ResultCode ERR_USER_CANCELLED_SELECTION{ErrorModule::Account, 1}; -ProfileSelect::ProfileSelect(Core::System& system_, +ProfileSelect::ProfileSelect(Core::System& system_, LibraryAppletMode applet_mode_, const Core::Frontend::ProfileSelectApplet& frontend_) - : Applet{system_.Kernel()}, frontend{frontend_}, system{system_} {} + : Applet{system_.Kernel()}, applet_mode{applet_mode_}, frontend{frontend_}, system{system_} {} ProfileSelect::~ProfileSelect() = default; diff --git a/src/core/hle/service/am/applets/profile_select.h b/src/core/hle/service/am/applets/profile_select.h index 648d33a249..90f054030f 100644 --- a/src/core/hle/service/am/applets/profile_select.h +++ b/src/core/hle/service/am/applets/profile_select.h @@ -33,7 +33,7 @@ static_assert(sizeof(UserSelectionOutput) == 0x18, "UserSelectionOutput has inco class ProfileSelect final : public Applet { public: - explicit ProfileSelect(Core::System& system_, + explicit ProfileSelect(Core::System& system_, LibraryAppletMode applet_mode_, const Core::Frontend::ProfileSelectApplet& frontend_); ~ProfileSelect() override; @@ -47,6 +47,7 @@ public: void SelectionComplete(std::optional uuid); private: + LibraryAppletMode applet_mode; const Core::Frontend::ProfileSelectApplet& frontend; UserSelectionConfig config; diff --git a/src/core/hle/service/am/applets/web_browser.cpp b/src/core/hle/service/am/applets/web_browser.cpp index 2ab4207894..b28b849bc7 100644 --- a/src/core/hle/service/am/applets/web_browser.cpp +++ b/src/core/hle/service/am/applets/web_browser.cpp @@ -208,8 +208,9 @@ void ExtractSharedFonts(Core::System& system) { } // namespace -WebBrowser::WebBrowser(Core::System& system_, const Core::Frontend::WebBrowserApplet& frontend_) - : Applet{system_.Kernel()}, frontend(frontend_), system{system_} {} +WebBrowser::WebBrowser(Core::System& system_, LibraryAppletMode applet_mode_, + const Core::Frontend::WebBrowserApplet& frontend_) + : Applet{system_.Kernel()}, applet_mode{applet_mode_}, frontend(frontend_), system{system_} {} WebBrowser::~WebBrowser() = default; diff --git a/src/core/hle/service/am/applets/web_browser.h b/src/core/hle/service/am/applets/web_browser.h index 04c2747546..5eafbae7b0 100644 --- a/src/core/hle/service/am/applets/web_browser.h +++ b/src/core/hle/service/am/applets/web_browser.h @@ -25,7 +25,8 @@ namespace Service::AM::Applets { class WebBrowser final : public Applet { public: - WebBrowser(Core::System& system_, const Core::Frontend::WebBrowserApplet& frontend_); + WebBrowser(Core::System& system_, LibraryAppletMode applet_mode_, + const Core::Frontend::WebBrowserApplet& frontend_); ~WebBrowser() override; @@ -63,6 +64,7 @@ private: void ExecuteWifi(); void ExecuteLobby(); + LibraryAppletMode applet_mode; const Core::Frontend::WebBrowserApplet& frontend; bool complete{false}; From 0a40106cf143c5ae5e029ce3f828bead6d3fdb92 Mon Sep 17 00:00:00 2001 From: Morph <39850852+Morph1984@users.noreply.github.com> Date: Thu, 11 Mar 2021 22:31:05 -0500 Subject: [PATCH 03/14] ILibraryAppletAccessor: Demote from ERROR to DEBUG for null storage logs Avoids unnecessary console spam when the inline software keyboard is used. --- src/core/hle/service/am/am.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/hle/service/am/am.cpp b/src/core/hle/service/am/am.cpp index 836fa564e2..6373d816dd 100644 --- a/src/core/hle/service/am/am.cpp +++ b/src/core/hle/service/am/am.cpp @@ -971,7 +971,7 @@ private: auto storage = applet->GetBroker().PopNormalDataToGame(); if (storage == nullptr) { - LOG_ERROR(Service_AM, + LOG_DEBUG(Service_AM, "storage is a nullptr. There is no data in the current normal channel"); IPC::ResponseBuilder rb{ctx, 2}; rb.Push(ERR_NO_DATA_IN_CHANNEL); @@ -1002,7 +1002,7 @@ private: auto storage = applet->GetBroker().PopInteractiveDataToGame(); if (storage == nullptr) { - LOG_ERROR(Service_AM, + LOG_DEBUG(Service_AM, "storage is a nullptr. There is no data in the current interactive channel"); IPC::ResponseBuilder rb{ctx, 2}; rb.Push(ERR_NO_DATA_IN_CHANNEL); From e3e6a11ab8fc14a99bce35eb6fd860d583f255d8 Mon Sep 17 00:00:00 2001 From: Morph <39850852+Morph1984@users.noreply.github.com> Date: Fri, 12 Mar 2021 10:13:31 -0500 Subject: [PATCH 04/14] hle_ipc: Add helper functions to get copy/move handles --- src/core/hle/kernel/hle_ipc.cpp | 8 ++++++-- src/core/hle/kernel/hle_ipc.h | 10 ++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/core/hle/kernel/hle_ipc.cpp b/src/core/hle/kernel/hle_ipc.cpp index 161d9f7826..2b363b1d98 100644 --- a/src/core/hle/kernel/hle_ipc.cpp +++ b/src/core/hle/kernel/hle_ipc.cpp @@ -75,10 +75,14 @@ void HLERequestContext::ParseCommandBuffer(const HandleTable& handle_table, u32_ if (incoming) { // Populate the object lists with the data in the IPC request. for (u32 handle = 0; handle < handle_descriptor_header->num_handles_to_copy; ++handle) { - copy_objects.push_back(handle_table.GetGeneric(rp.Pop())); + const u32 copy_handle{rp.Pop()}; + copy_handles.push_back(copy_handle); + copy_objects.push_back(handle_table.GetGeneric(copy_handle)); } for (u32 handle = 0; handle < handle_descriptor_header->num_handles_to_move; ++handle) { - move_objects.push_back(handle_table.GetGeneric(rp.Pop())); + const u32 move_handle{rp.Pop()}; + move_handles.push_back(move_handle); + move_objects.push_back(handle_table.GetGeneric(move_handle)); } } else { // For responses we just ignore the handles, they're empty and will be populated when diff --git a/src/core/hle/kernel/hle_ipc.h b/src/core/hle/kernel/hle_ipc.h index 9a769781b6..6fba426153 100644 --- a/src/core/hle/kernel/hle_ipc.h +++ b/src/core/hle/kernel/hle_ipc.h @@ -210,6 +210,14 @@ public: /// Helper function to test whether the output buffer at buffer_index can be written bool CanWriteBuffer(std::size_t buffer_index = 0) const; + Handle GetCopyHandle(std::size_t index) const { + return copy_handles.at(index); + } + + Handle GetMoveHandle(std::size_t index) const { + return move_handles.at(index); + } + template std::shared_ptr GetCopyObject(std::size_t index) { return DynamicObjectCast(copy_objects.at(index)); @@ -285,6 +293,8 @@ private: std::shared_ptr server_session; std::shared_ptr thread; // TODO(yuriks): Check common usage of this and optimize size accordingly + boost::container::small_vector move_handles; + boost::container::small_vector copy_handles; boost::container::small_vector, 8> move_objects; boost::container::small_vector, 8> copy_objects; boost::container::small_vector, 8> domain_objects; From a8c09cd5e46904441165c4aeaacb5397efdf2173 Mon Sep 17 00:00:00 2001 From: Morph <39850852+Morph1984@users.noreply.github.com> Date: Fri, 12 Mar 2021 10:14:01 -0500 Subject: [PATCH 05/14] ILibraryAppletCreator: Implement CreateHandleStorage Used by Monster Hunter Generations Ultimate --- src/core/hle/service/am/am.cpp | 71 ++++++++++++++++++++++++++++++---- src/core/hle/service/am/am.h | 1 + 2 files changed, 65 insertions(+), 7 deletions(-) diff --git a/src/core/hle/service/am/am.cpp b/src/core/hle/service/am/am.cpp index 6373d816dd..c59054468f 100644 --- a/src/core/hle/service/am/am.cpp +++ b/src/core/hle/service/am/am.cpp @@ -1125,7 +1125,7 @@ ILibraryAppletCreator::ILibraryAppletCreator(Core::System& system_) {2, nullptr, "AreAnyLibraryAppletsLeft"}, {10, &ILibraryAppletCreator::CreateStorage, "CreateStorage"}, {11, &ILibraryAppletCreator::CreateTransferMemoryStorage, "CreateTransferMemoryStorage"}, - {12, nullptr, "CreateHandleStorage"}, + {12, &ILibraryAppletCreator::CreateHandleStorage, "CreateHandleStorage"}, }; RegisterHandlers(functions); } @@ -1134,6 +1134,7 @@ ILibraryAppletCreator::~ILibraryAppletCreator() = default; void ILibraryAppletCreator::CreateLibraryApplet(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp{ctx}; + const auto applet_id = rp.PopRaw(); const auto applet_mode = rp.PopRaw(); @@ -1159,9 +1160,18 @@ void ILibraryAppletCreator::CreateLibraryApplet(Kernel::HLERequestContext& ctx) void ILibraryAppletCreator::CreateStorage(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp{ctx}; - const u64 size{rp.Pop()}; + + const s64 size{rp.Pop()}; + LOG_DEBUG(Service_AM, "called, size={}", size); + if (size <= 0) { + LOG_ERROR(Service_AM, "size is less than or equal to 0"); + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(RESULT_UNKNOWN); + return; + } + std::vector buffer(size); IPC::ResponseBuilder rb{ctx, 2, 0, 1}; @@ -1170,18 +1180,65 @@ void ILibraryAppletCreator::CreateStorage(Kernel::HLERequestContext& ctx) { } void ILibraryAppletCreator::CreateTransferMemoryStorage(Kernel::HLERequestContext& ctx) { - LOG_DEBUG(Service_AM, "called"); - IPC::RequestParser rp{ctx}; - rp.SetCurrentOffset(3); - const auto handle{rp.Pop()}; + struct Parameters { + u8 permissions; + s64 size; + }; + + const auto parameters{rp.PopRaw()}; + const auto handle{ctx.GetCopyHandle(0)}; + + LOG_DEBUG(Service_AM, "called, permissions={}, size={}, handle={:08X}", parameters.permissions, + parameters.size, handle); + + if (parameters.size <= 0) { + LOG_ERROR(Service_AM, "size is less than or equal to 0"); + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(RESULT_UNKNOWN); + return; + } auto transfer_mem = system.CurrentProcess()->GetHandleTable().Get(handle); if (transfer_mem == nullptr) { - LOG_ERROR(Service_AM, "shared_mem is a nullpr for handle={:08X}", handle); + LOG_ERROR(Service_AM, "transfer_mem is a nullptr for handle={:08X}", handle); + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(RESULT_UNKNOWN); + return; + } + + const u8* const mem_begin = transfer_mem->GetPointer(); + const u8* const mem_end = mem_begin + transfer_mem->GetSize(); + std::vector memory{mem_begin, mem_end}; + + IPC::ResponseBuilder rb{ctx, 2, 0, 1}; + rb.Push(RESULT_SUCCESS); + rb.PushIpcInterface(system, std::move(memory)); +} + +void ILibraryAppletCreator::CreateHandleStorage(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp{ctx}; + + const s64 size{rp.Pop()}; + const auto handle{ctx.GetCopyHandle(0)}; + + LOG_DEBUG(Service_AM, "called, size={}, handle={:08X}", size, handle); + + if (size <= 0) { + LOG_ERROR(Service_AM, "size is less than or equal to 0"); + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(RESULT_UNKNOWN); + return; + } + + auto transfer_mem = + system.CurrentProcess()->GetHandleTable().Get(handle); + + if (transfer_mem == nullptr) { + LOG_ERROR(Service_AM, "transfer_mem is a nullptr for handle={:08X}", handle); IPC::ResponseBuilder rb{ctx, 2}; rb.Push(RESULT_UNKNOWN); return; diff --git a/src/core/hle/service/am/am.h b/src/core/hle/service/am/am.h index f6a453ab7c..aefbdf0d55 100644 --- a/src/core/hle/service/am/am.h +++ b/src/core/hle/service/am/am.h @@ -254,6 +254,7 @@ private: void CreateLibraryApplet(Kernel::HLERequestContext& ctx); void CreateStorage(Kernel::HLERequestContext& ctx); void CreateTransferMemoryStorage(Kernel::HLERequestContext& ctx); + void CreateHandleStorage(Kernel::HLERequestContext& ctx); }; class IApplicationFunctions final : public ServiceFramework { From 5bc9f15c6d3270862fb495778ecc706ff35750a4 Mon Sep 17 00:00:00 2001 From: Morph <39850852+Morph1984@users.noreply.github.com> Date: Tue, 16 Mar 2021 12:59:52 -0400 Subject: [PATCH 06/14] applets/swkbd: Implement the Normal and Inline Software Keyboard Applet --- src/core/CMakeLists.txt | 1 + .../service/am/applets/software_keyboard.cpp | 1067 ++++++++++++++++- .../service/am/applets/software_keyboard.h | 140 ++- .../am/applets/software_keyboard_types.h | 295 +++++ 4 files changed, 1489 insertions(+), 14 deletions(-) create mode 100644 src/core/hle/service/am/applets/software_keyboard_types.h diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 286e912e3e..532e418b0e 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -273,6 +273,7 @@ add_library(core STATIC hle/service/am/applets/profile_select.h hle/service/am/applets/software_keyboard.cpp hle/service/am/applets/software_keyboard.h + hle/service/am/applets/software_keyboard_types.h hle/service/am/applets/web_browser.cpp hle/service/am/applets/web_browser.h hle/service/am/applets/web_types.h diff --git a/src/core/hle/service/am/applets/software_keyboard.cpp b/src/core/hle/service/am/applets/software_keyboard.cpp index f966cf67b5..c3a05de9cc 100644 --- a/src/core/hle/service/am/applets/software_keyboard.cpp +++ b/src/core/hle/service/am/applets/software_keyboard.cpp @@ -1,7 +1,8 @@ -// Copyright 2018 yuzu emulator team +// Copyright 2021 yuzu Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. +#include "common/string_util.h" #include "core/core.h" #include "core/frontend/applets/software_keyboard.h" #include "core/hle/service/am/am.h" @@ -9,20 +10,1068 @@ namespace Service::AM::Applets { -SoftwareKeyboard::SoftwareKeyboard(Core::System& system_, - const Core::Frontend::SoftwareKeyboardApplet& frontend_) - : Applet{system_.Kernel()}, frontend{frontend_}, system{system_} {} +namespace { + +// The maximum number of UTF-16 characters that can be input into the swkbd text field. +constexpr u32 DEFAULT_MAX_TEXT_LENGTH = 500; + +constexpr std::size_t REPLY_BASE_SIZE = sizeof(SwkbdState) + sizeof(SwkbdReplyType); +constexpr std::size_t REPLY_UTF8_SIZE = 0x7D4; +constexpr std::size_t REPLY_UTF16_SIZE = 0x3EC; + +constexpr const char* GetTextCheckResultName(SwkbdTextCheckResult text_check_result) { + switch (text_check_result) { + case SwkbdTextCheckResult::Success: + return "Success"; + case SwkbdTextCheckResult::Failure: + return "Failure"; + case SwkbdTextCheckResult::Confirm: + return "Confirm"; + case SwkbdTextCheckResult::Silent: + return "Silent"; + default: + UNIMPLEMENTED_MSG("Unknown TextCheckResult={}", text_check_result); + return "Unknown"; + } +} + +void SetReplyBase(std::vector& reply, SwkbdState state, SwkbdReplyType reply_type) { + std::memcpy(reply.data(), &state, sizeof(SwkbdState)); + std::memcpy(reply.data() + sizeof(SwkbdState), &reply_type, sizeof(SwkbdReplyType)); +} + +} // Anonymous namespace + +SoftwareKeyboard::SoftwareKeyboard(Core::System& system_, LibraryAppletMode applet_mode_, + Core::Frontend::SoftwareKeyboardApplet& frontend_) + : Applet{system_.Kernel()}, applet_mode{applet_mode_}, frontend{frontend_}, system{system_} {} SoftwareKeyboard::~SoftwareKeyboard() = default; -void SoftwareKeyboard::Initialize() {} +void SoftwareKeyboard::Initialize() { + Applet::Initialize(); -bool SoftwareKeyboard::TransactionComplete() const {} + LOG_INFO(Service_AM, "Initializing Software Keyboard Applet with LibraryAppletMode={}", + applet_mode); -ResultCode SoftwareKeyboard::GetStatus() const {} + LOG_DEBUG(Service_AM, + "Initializing Applet with common_args: arg_version={}, lib_version={}, " + "play_startup_sound={}, size={}, system_tick={}, theme_color={}", + common_args.arguments_version, common_args.library_version, + common_args.play_startup_sound, common_args.size, common_args.system_tick, + common_args.theme_color); -void SoftwareKeyboard::ExecuteInteractive() {} + swkbd_applet_version = SwkbdAppletVersion{common_args.library_version}; -void SoftwareKeyboard::Execute() {} + switch (applet_mode) { + case LibraryAppletMode::AllForeground: + InitializeForeground(); + break; + case LibraryAppletMode::Background: + case LibraryAppletMode::BackgroundIndirectDisplay: + InitializeBackground(applet_mode); + break; + default: + UNREACHABLE_MSG("Invalid LibraryAppletMode={}", applet_mode); + break; + } +} + +bool SoftwareKeyboard::TransactionComplete() const { + return complete; +} + +ResultCode SoftwareKeyboard::GetStatus() const { + return status; +} + +void SoftwareKeyboard::ExecuteInteractive() { + if (complete) { + return; + } + + if (is_background) { + ProcessInlineKeyboardRequest(); + } else { + ProcessTextCheck(); + } +} + +void SoftwareKeyboard::Execute() { + if (complete) { + return; + } + + if (is_background) { + return; + } + + ShowNormalKeyboard(); +} + +void SoftwareKeyboard::SubmitTextNormal(SwkbdResult result, std::u16string submitted_text) { + if (complete) { + return; + } + + if (swkbd_config_common.use_text_check && result == SwkbdResult::Ok) { + SubmitForTextCheck(submitted_text); + } else { + SubmitNormalOutputAndExit(result, submitted_text); + } +} + +void SoftwareKeyboard::SubmitTextInline(SwkbdReplyType reply_type, std::u16string submitted_text, + s32 cursor_position) { + if (complete) { + return; + } + + current_text = std::move(submitted_text); + current_cursor_position = cursor_position; + + if (inline_use_utf8) { + switch (reply_type) { + case SwkbdReplyType::ChangedString: + reply_type = SwkbdReplyType::ChangedStringUtf8; + break; + case SwkbdReplyType::MovedCursor: + reply_type = SwkbdReplyType::MovedCursorUtf8; + break; + case SwkbdReplyType::DecidedEnter: + reply_type = SwkbdReplyType::DecidedEnterUtf8; + break; + default: + break; + } + } + + if (use_changed_string_v2) { + switch (reply_type) { + case SwkbdReplyType::ChangedString: + reply_type = SwkbdReplyType::ChangedStringV2; + break; + case SwkbdReplyType::ChangedStringUtf8: + reply_type = SwkbdReplyType::ChangedStringUtf8V2; + break; + default: + break; + } + } + + if (use_moved_cursor_v2) { + switch (reply_type) { + case SwkbdReplyType::MovedCursor: + reply_type = SwkbdReplyType::MovedCursorV2; + break; + case SwkbdReplyType::MovedCursorUtf8: + reply_type = SwkbdReplyType::MovedCursorUtf8V2; + break; + default: + break; + } + } + + SendReply(reply_type); +} + +void SoftwareKeyboard::InitializeForeground() { + LOG_INFO(Service_AM, "Initializing Normal Software Keyboard Applet."); + + is_background = false; + + const auto swkbd_config_storage = broker.PopNormalDataToApplet(); + ASSERT(swkbd_config_storage != nullptr); + + const auto& swkbd_config_data = swkbd_config_storage->GetData(); + ASSERT(swkbd_config_data.size() >= sizeof(SwkbdConfigCommon)); + + std::memcpy(&swkbd_config_common, swkbd_config_data.data(), sizeof(SwkbdConfigCommon)); + + switch (swkbd_applet_version) { + case SwkbdAppletVersion::Version5: + case SwkbdAppletVersion::Version65542: + ASSERT(swkbd_config_data.size() == sizeof(SwkbdConfigCommon) + sizeof(SwkbdConfigOld)); + std::memcpy(&swkbd_config_old, swkbd_config_data.data() + sizeof(SwkbdConfigCommon), + sizeof(SwkbdConfigOld)); + break; + case SwkbdAppletVersion::Version196615: + case SwkbdAppletVersion::Version262152: + case SwkbdAppletVersion::Version327689: + ASSERT(swkbd_config_data.size() == sizeof(SwkbdConfigCommon) + sizeof(SwkbdConfigOld2)); + std::memcpy(&swkbd_config_old2, swkbd_config_data.data() + sizeof(SwkbdConfigCommon), + sizeof(SwkbdConfigOld2)); + break; + case SwkbdAppletVersion::Version393227: + case SwkbdAppletVersion::Version524301: + ASSERT(swkbd_config_data.size() == sizeof(SwkbdConfigCommon) + sizeof(SwkbdConfigNew)); + std::memcpy(&swkbd_config_new, swkbd_config_data.data() + sizeof(SwkbdConfigCommon), + sizeof(SwkbdConfigNew)); + break; + default: + UNIMPLEMENTED_MSG("Unknown SwkbdConfig revision={} with size={}", swkbd_applet_version, + swkbd_config_data.size()); + ASSERT(swkbd_config_data.size() >= sizeof(SwkbdConfigCommon) + sizeof(SwkbdConfigNew)); + std::memcpy(&swkbd_config_new, swkbd_config_data.data() + sizeof(SwkbdConfigCommon), + sizeof(SwkbdConfigNew)); + break; + } + + const auto work_buffer_storage = broker.PopNormalDataToApplet(); + ASSERT(work_buffer_storage != nullptr); + + if (swkbd_config_common.initial_string_length == 0) { + InitializeFrontendKeyboard(); + return; + } + + const auto& work_buffer = work_buffer_storage->GetData(); + + std::vector initial_string(swkbd_config_common.initial_string_length); + + std::memcpy(initial_string.data(), + work_buffer.data() + swkbd_config_common.initial_string_offset, + swkbd_config_common.initial_string_length * sizeof(char16_t)); + + initial_text = Common::UTF16StringFromFixedZeroTerminatedBuffer(initial_string.data(), + initial_string.size()); + + LOG_DEBUG(Service_AM, "\nInitial Text: {}", Common::UTF16ToUTF8(initial_text)); + + InitializeFrontendKeyboard(); +} + +void SoftwareKeyboard::InitializeBackground(LibraryAppletMode applet_mode) { + LOG_INFO(Service_AM, "Initializing Inline Software Keyboard Applet."); + + is_background = true; + + const auto swkbd_inline_initialize_arg_storage = broker.PopNormalDataToApplet(); + ASSERT(swkbd_inline_initialize_arg_storage != nullptr); + + const auto& swkbd_inline_initialize_arg = swkbd_inline_initialize_arg_storage->GetData(); + ASSERT(swkbd_inline_initialize_arg.size() == sizeof(SwkbdInitializeArg)); + + std::memcpy(&swkbd_initialize_arg, swkbd_inline_initialize_arg.data(), + swkbd_inline_initialize_arg.size()); + + if (swkbd_initialize_arg.library_applet_mode_flag) { + ASSERT(applet_mode == LibraryAppletMode::Background); + } else { + ASSERT(applet_mode == LibraryAppletMode::BackgroundIndirectDisplay); + } +} + +void SoftwareKeyboard::ProcessTextCheck() { + const auto text_check_storage = broker.PopInteractiveDataToApplet(); + ASSERT(text_check_storage != nullptr); + + const auto& text_check_data = text_check_storage->GetData(); + ASSERT(text_check_data.size() == sizeof(SwkbdTextCheck)); + + SwkbdTextCheck swkbd_text_check; + + std::memcpy(&swkbd_text_check, text_check_data.data(), sizeof(SwkbdTextCheck)); + + std::u16string text_check_message = Common::UTF16StringFromFixedZeroTerminatedBuffer( + swkbd_text_check.text_check_message.data(), swkbd_text_check.text_check_message.size()); + + LOG_INFO(Service_AM, "\nTextCheckResult: {}\nTextCheckMessage: {}", + GetTextCheckResultName(swkbd_text_check.text_check_result), + Common::UTF16ToUTF8(text_check_message)); + + switch (swkbd_text_check.text_check_result) { + case SwkbdTextCheckResult::Success: + SubmitNormalOutputAndExit(SwkbdResult::Ok, current_text); + break; + case SwkbdTextCheckResult::Failure: + ShowTextCheckDialog(SwkbdTextCheckResult::Failure, text_check_message); + break; + case SwkbdTextCheckResult::Confirm: + ShowTextCheckDialog(SwkbdTextCheckResult::Confirm, text_check_message); + break; + case SwkbdTextCheckResult::Silent: + default: + break; + } +} + +void SoftwareKeyboard::ProcessInlineKeyboardRequest() { + const auto request_data_storage = broker.PopInteractiveDataToApplet(); + ASSERT(request_data_storage != nullptr); + + const auto& request_data = request_data_storage->GetData(); + ASSERT(request_data.size() >= sizeof(SwkbdRequestCommand)); + + SwkbdRequestCommand request_command; + + std::memcpy(&request_command, request_data.data(), sizeof(SwkbdRequestCommand)); + + switch (request_command) { + case SwkbdRequestCommand::Finalize: + RequestFinalize(request_data); + break; + case SwkbdRequestCommand::SetUserWordInfo: + RequestSetUserWordInfo(request_data); + break; + case SwkbdRequestCommand::SetCustomizeDic: + RequestSetCustomizeDic(request_data); + break; + case SwkbdRequestCommand::Calc: + RequestCalc(request_data); + break; + case SwkbdRequestCommand::SetCustomizedDictionaries: + RequestSetCustomizedDictionaries(request_data); + break; + case SwkbdRequestCommand::UnsetCustomizedDictionaries: + RequestUnsetCustomizedDictionaries(request_data); + break; + case SwkbdRequestCommand::SetChangedStringV2Flag: + RequestSetChangedStringV2Flag(request_data); + break; + case SwkbdRequestCommand::SetMovedCursorV2Flag: + RequestSetMovedCursorV2Flag(request_data); + break; + default: + UNIMPLEMENTED_MSG("Unknown SwkbdRequestCommand={}", request_command); + break; + } +} + +void SoftwareKeyboard::SubmitNormalOutputAndExit(SwkbdResult result, + std::u16string submitted_text) { + std::vector out_data(sizeof(SwkbdResult) + STRING_BUFFER_SIZE); + + if (swkbd_config_common.use_utf8) { + std::string utf8_submitted_text = Common::UTF16ToUTF8(submitted_text); + + LOG_DEBUG(Service_AM, "\nSwkbdResult: {}\nUTF-8 Submitted Text: {}", result, + utf8_submitted_text); + + std::memcpy(out_data.data(), &result, sizeof(SwkbdResult)); + std::memcpy(out_data.data() + sizeof(SwkbdResult), utf8_submitted_text.data(), + utf8_submitted_text.size()); + } else { + LOG_DEBUG(Service_AM, "\nSwkbdResult: {}\nUTF-16 Submitted Text: {}", result, + Common::UTF16ToUTF8(submitted_text)); + + std::memcpy(out_data.data(), &result, sizeof(SwkbdResult)); + std::memcpy(out_data.data() + sizeof(SwkbdResult), submitted_text.data(), + submitted_text.size() * sizeof(char16_t)); + } + + broker.PushNormalDataFromApplet(std::make_shared(system, std::move(out_data))); + + ExitKeyboard(); +} + +void SoftwareKeyboard::SubmitForTextCheck(std::u16string submitted_text) { + current_text = std::move(submitted_text); + + std::vector out_data(sizeof(u64) + STRING_BUFFER_SIZE); + + if (swkbd_config_common.use_utf8) { + std::string utf8_submitted_text = Common::UTF16ToUTF8(current_text); + const u64 buffer_size = sizeof(u64) + utf8_submitted_text.size(); + + LOG_DEBUG(Service_AM, "\nBuffer Size: {}\nUTF-8 Submitted Text: {}", buffer_size, + utf8_submitted_text); + + std::memcpy(out_data.data(), &buffer_size, sizeof(u64)); + std::memcpy(out_data.data() + sizeof(u64), utf8_submitted_text.data(), + utf8_submitted_text.size()); + } else { + const u64 buffer_size = sizeof(u64) + current_text.size() * sizeof(char16_t); + + LOG_DEBUG(Service_AM, "\nBuffer Size: {}\nUTF-16 Submitted Text: {}", buffer_size, + Common::UTF16ToUTF8(current_text)); + + std::memcpy(out_data.data(), &buffer_size, sizeof(u64)); + std::memcpy(out_data.data() + sizeof(u64), current_text.data(), + current_text.size() * sizeof(char16_t)); + } + + broker.PushInteractiveDataFromApplet(std::make_shared(system, std::move(out_data))); +} + +void SoftwareKeyboard::SendReply(SwkbdReplyType reply_type) { + switch (reply_type) { + case SwkbdReplyType::FinishedInitialize: + ReplyFinishedInitialize(); + break; + case SwkbdReplyType::Default: + ReplyDefault(); + break; + case SwkbdReplyType::ChangedString: + ReplyChangedString(); + break; + case SwkbdReplyType::MovedCursor: + ReplyMovedCursor(); + break; + case SwkbdReplyType::MovedTab: + ReplyMovedTab(); + break; + case SwkbdReplyType::DecidedEnter: + ReplyDecidedEnter(); + break; + case SwkbdReplyType::DecidedCancel: + ReplyDecidedCancel(); + break; + case SwkbdReplyType::ChangedStringUtf8: + ReplyChangedStringUtf8(); + break; + case SwkbdReplyType::MovedCursorUtf8: + ReplyMovedCursorUtf8(); + break; + case SwkbdReplyType::DecidedEnterUtf8: + ReplyDecidedEnterUtf8(); + break; + case SwkbdReplyType::UnsetCustomizeDic: + ReplyUnsetCustomizeDic(); + break; + case SwkbdReplyType::ReleasedUserWordInfo: + ReplyReleasedUserWordInfo(); + break; + case SwkbdReplyType::UnsetCustomizedDictionaries: + ReplyUnsetCustomizedDictionaries(); + break; + case SwkbdReplyType::ChangedStringV2: + ReplyChangedStringV2(); + break; + case SwkbdReplyType::MovedCursorV2: + ReplyMovedCursorV2(); + break; + case SwkbdReplyType::ChangedStringUtf8V2: + ReplyChangedStringUtf8V2(); + break; + case SwkbdReplyType::MovedCursorUtf8V2: + ReplyMovedCursorUtf8V2(); + break; + default: + UNIMPLEMENTED_MSG("Unknown SwkbdReplyType={}", reply_type); + ReplyDefault(); + break; + } +} + +void SoftwareKeyboard::ChangeState(SwkbdState state) { + swkbd_state = state; + + ReplyDefault(); +} + +void SoftwareKeyboard::InitializeFrontendKeyboard() { + if (is_background) { + const auto& appear_arg = swkbd_calc_arg.appear_arg; + + std::u16string ok_text = Common::UTF16StringFromFixedZeroTerminatedBuffer( + appear_arg.ok_text.data(), appear_arg.ok_text.size()); + + const u32 max_text_length = + appear_arg.max_text_length > 0 && appear_arg.max_text_length <= DEFAULT_MAX_TEXT_LENGTH + ? appear_arg.max_text_length + : DEFAULT_MAX_TEXT_LENGTH; + + const u32 min_text_length = + appear_arg.min_text_length <= max_text_length ? appear_arg.min_text_length : 0; + + const s32 initial_cursor_position = + current_cursor_position > 0 ? current_cursor_position : 0; + + const auto text_draw_type = + max_text_length <= 32 ? SwkbdTextDrawType::Line : SwkbdTextDrawType::Box; + + Core::Frontend::KeyboardInitializeParameters initialize_parameters{ + .ok_text{ok_text}, + .header_text{}, + .sub_text{}, + .guide_text{}, + .initial_text{current_text}, + .max_text_length{max_text_length}, + .min_text_length{min_text_length}, + .initial_cursor_position{initial_cursor_position}, + .type{appear_arg.type}, + .password_mode{SwkbdPasswordMode::Disabled}, + .text_draw_type{text_draw_type}, + .key_disable_flags{appear_arg.key_disable_flags}, + .use_blur_background{false}, + .enable_backspace_button{swkbd_calc_arg.enable_backspace_button}, + .enable_return_button{appear_arg.enable_return_button}, + .disable_cancel_button{appear_arg.disable_cancel_button}, + }; + + frontend.InitializeKeyboard( + true, std::move(initialize_parameters), {}, + [this](SwkbdReplyType reply_type, std::u16string submitted_text, s32 cursor_position) { + SubmitTextInline(reply_type, submitted_text, cursor_position); + }); + } else { + std::u16string ok_text = Common::UTF16StringFromFixedZeroTerminatedBuffer( + swkbd_config_common.ok_text.data(), swkbd_config_common.ok_text.size()); + + std::u16string header_text = Common::UTF16StringFromFixedZeroTerminatedBuffer( + swkbd_config_common.header_text.data(), swkbd_config_common.header_text.size()); + + std::u16string sub_text = Common::UTF16StringFromFixedZeroTerminatedBuffer( + swkbd_config_common.sub_text.data(), swkbd_config_common.sub_text.size()); + + std::u16string guide_text = Common::UTF16StringFromFixedZeroTerminatedBuffer( + swkbd_config_common.guide_text.data(), swkbd_config_common.guide_text.size()); + + const u32 max_text_length = + swkbd_config_common.max_text_length > 0 && + swkbd_config_common.max_text_length <= DEFAULT_MAX_TEXT_LENGTH + ? swkbd_config_common.max_text_length + : DEFAULT_MAX_TEXT_LENGTH; + + const u32 min_text_length = swkbd_config_common.min_text_length <= max_text_length + ? swkbd_config_common.min_text_length + : 0; + + const s32 initial_cursor_position = [this] { + switch (swkbd_config_common.initial_cursor_position) { + case SwkbdInitialCursorPosition::Start: + default: + return 0; + case SwkbdInitialCursorPosition::End: + return static_cast(initial_text.size()); + } + }(); + + const auto text_draw_type = [this, max_text_length] { + switch (swkbd_config_common.text_draw_type) { + case SwkbdTextDrawType::Line: + default: + return max_text_length <= 32 ? SwkbdTextDrawType::Line : SwkbdTextDrawType::Box; + case SwkbdTextDrawType::Box: + case SwkbdTextDrawType::DownloadCode: + return swkbd_config_common.text_draw_type; + } + }(); + + const auto enable_return_button = text_draw_type == SwkbdTextDrawType::Box + ? swkbd_config_common.enable_return_button + : false; + + const auto disable_cancel_button = swkbd_applet_version >= SwkbdAppletVersion::Version393227 + ? swkbd_config_new.disable_cancel_button + : false; + + Core::Frontend::KeyboardInitializeParameters initialize_parameters{ + .ok_text{ok_text}, + .header_text{header_text}, + .sub_text{sub_text}, + .guide_text{guide_text}, + .initial_text{initial_text}, + .max_text_length{max_text_length}, + .min_text_length{min_text_length}, + .initial_cursor_position{initial_cursor_position}, + .type{swkbd_config_common.type}, + .password_mode{swkbd_config_common.password_mode}, + .text_draw_type{text_draw_type}, + .key_disable_flags{swkbd_config_common.key_disable_flags}, + .use_blur_background{swkbd_config_common.use_blur_background}, + .enable_backspace_button{true}, + .enable_return_button{enable_return_button}, + .disable_cancel_button{disable_cancel_button}, + }; + + frontend.InitializeKeyboard(false, std::move(initialize_parameters), + [this](SwkbdResult result, std::u16string submitted_text) { + SubmitTextNormal(result, submitted_text); + }, + {}); + } +} + +void SoftwareKeyboard::ShowNormalKeyboard() { + frontend.ShowNormalKeyboard(); +} + +void SoftwareKeyboard::ShowTextCheckDialog(SwkbdTextCheckResult text_check_result, + std::u16string text_check_message) { + frontend.ShowTextCheckDialog(text_check_result, text_check_message); +} + +void SoftwareKeyboard::ShowInlineKeyboard() { + if (swkbd_state != SwkbdState::InitializedIsHidden) { + return; + } + + ChangeState(SwkbdState::InitializedIsAppearing); + + const auto& appear_arg = swkbd_calc_arg.appear_arg; + + const u32 max_text_length = + appear_arg.max_text_length > 0 && appear_arg.max_text_length <= DEFAULT_MAX_TEXT_LENGTH + ? appear_arg.max_text_length + : DEFAULT_MAX_TEXT_LENGTH; + + const u32 min_text_length = + appear_arg.min_text_length <= max_text_length ? appear_arg.min_text_length : 0; + + Core::Frontend::InlineAppearParameters appear_parameters{ + .max_text_length{max_text_length}, + .min_text_length{min_text_length}, + .key_top_scale_x{swkbd_calc_arg.key_top_scale_x}, + .key_top_scale_y{swkbd_calc_arg.key_top_scale_y}, + .key_top_translate_x{swkbd_calc_arg.key_top_translate_x}, + .key_top_translate_y{swkbd_calc_arg.key_top_translate_y}, + .type{appear_arg.type}, + .key_disable_flags{appear_arg.key_disable_flags}, + .key_top_as_floating{swkbd_calc_arg.key_top_as_floating}, + .enable_backspace_button{swkbd_calc_arg.enable_backspace_button}, + .enable_return_button{appear_arg.enable_return_button}, + .disable_cancel_button{appear_arg.disable_cancel_button}, + }; + + frontend.ShowInlineKeyboard(std::move(appear_parameters)); + + ChangeState(SwkbdState::InitializedIsShown); +} + +void SoftwareKeyboard::HideInlineKeyboard() { + if (swkbd_state != SwkbdState::InitializedIsShown) { + return; + } + + ChangeState(SwkbdState::InitializedIsDisappearing); + + frontend.HideInlineKeyboard(); + + ChangeState(SwkbdState::InitializedIsHidden); +} + +void SoftwareKeyboard::InlineTextChanged() { + Core::Frontend::InlineTextParameters text_parameters{ + .input_text{current_text}, + .cursor_position{current_cursor_position}, + }; + + frontend.InlineTextChanged(std::move(text_parameters)); +} + +void SoftwareKeyboard::ExitKeyboard() { + complete = true; + status = RESULT_SUCCESS; + + frontend.ExitKeyboard(); + + broker.SignalStateChanged(); +} + +// Inline Software Keyboard Requests + +void SoftwareKeyboard::RequestFinalize(const std::vector& request_data) { + LOG_DEBUG(Service_AM, "Processing Request: Finalize"); + + ChangeState(SwkbdState::NotInitialized); + + ExitKeyboard(); +} + +void SoftwareKeyboard::RequestSetUserWordInfo(const std::vector& request_data) { + LOG_WARNING(Service_AM, "SetUserWordInfo is not implemented."); +} + +void SoftwareKeyboard::RequestSetCustomizeDic(const std::vector& request_data) { + LOG_WARNING(Service_AM, "SetCustomizeDic is not implemented."); +} + +void SoftwareKeyboard::RequestCalc(const std::vector& request_data) { + LOG_DEBUG(Service_AM, "Processing Request: Calc"); + + ASSERT(request_data.size() == sizeof(SwkbdRequestCommand) + sizeof(SwkbdCalcArg)); + + std::memcpy(&swkbd_calc_arg, request_data.data() + sizeof(SwkbdRequestCommand), + sizeof(SwkbdCalcArg)); + + if (swkbd_calc_arg.flags.set_input_text) { + current_text = Common::UTF16StringFromFixedZeroTerminatedBuffer( + swkbd_calc_arg.input_text.data(), swkbd_calc_arg.input_text.size()); + } + + if (swkbd_calc_arg.flags.set_cursor_position) { + current_cursor_position = swkbd_calc_arg.cursor_position; + } + + if (swkbd_calc_arg.flags.set_utf8_mode) { + inline_use_utf8 = swkbd_calc_arg.utf8_mode; + } + + if (swkbd_state <= SwkbdState::InitializedIsHidden && + swkbd_calc_arg.flags.unset_customize_dic) { + ReplyUnsetCustomizeDic(); + } + + if (swkbd_state <= SwkbdState::InitializedIsHidden && + swkbd_calc_arg.flags.unset_user_word_info) { + ReplyReleasedUserWordInfo(); + } + + if (swkbd_state == SwkbdState::NotInitialized && swkbd_calc_arg.flags.set_initialize_arg) { + InitializeFrontendKeyboard(); + + ChangeState(SwkbdState::InitializedIsHidden); + + ReplyFinishedInitialize(); + } + + if (!swkbd_calc_arg.flags.set_initialize_arg && + (swkbd_calc_arg.flags.set_input_text || swkbd_calc_arg.flags.set_cursor_position)) { + InlineTextChanged(); + } + + if (swkbd_state == SwkbdState::InitializedIsHidden && swkbd_calc_arg.flags.appear) { + ShowInlineKeyboard(); + return; + } + + if (swkbd_state == SwkbdState::InitializedIsShown && swkbd_calc_arg.flags.disappear) { + HideInlineKeyboard(); + return; + } +} + +void SoftwareKeyboard::RequestSetCustomizedDictionaries(const std::vector& request_data) { + LOG_WARNING(Service_AM, "SetCustomizedDictionaries is not implemented."); +} + +void SoftwareKeyboard::RequestUnsetCustomizedDictionaries(const std::vector& request_data) { + LOG_WARNING(Service_AM, "(STUBBED) Processing Request: UnsetCustomizedDictionaries"); + + ReplyUnsetCustomizedDictionaries(); +} + +void SoftwareKeyboard::RequestSetChangedStringV2Flag(const std::vector& request_data) { + LOG_DEBUG(Service_AM, "Processing Request: SetChangedStringV2Flag"); + + ASSERT(request_data.size() == sizeof(SwkbdRequestCommand) + 1); + + std::memcpy(&use_changed_string_v2, request_data.data() + sizeof(SwkbdRequestCommand), 1); +} + +void SoftwareKeyboard::RequestSetMovedCursorV2Flag(const std::vector& request_data) { + LOG_DEBUG(Service_AM, "Processing Request: SetMovedCursorV2Flag"); + + ASSERT(request_data.size() == sizeof(SwkbdRequestCommand) + 1); + + std::memcpy(&use_moved_cursor_v2, request_data.data() + sizeof(SwkbdRequestCommand), 1); +} + +// Inline Software Keyboard Replies + +void SoftwareKeyboard::ReplyFinishedInitialize() { + LOG_DEBUG(Service_AM, "Sending Reply: FinishedInitialize"); + + std::vector reply(REPLY_BASE_SIZE + 1); + + SetReplyBase(reply, swkbd_state, SwkbdReplyType::FinishedInitialize); + + broker.PushInteractiveDataFromApplet(std::make_shared(system, std::move(reply))); +} + +void SoftwareKeyboard::ReplyDefault() { + LOG_DEBUG(Service_AM, "Sending Reply: Default"); + + std::vector reply(REPLY_BASE_SIZE); + + SetReplyBase(reply, swkbd_state, SwkbdReplyType::Default); + + broker.PushInteractiveDataFromApplet(std::make_shared(system, std::move(reply))); +} + +void SoftwareKeyboard::ReplyChangedString() { + LOG_DEBUG(Service_AM, "Sending Reply: ChangedString"); + + std::vector reply(REPLY_BASE_SIZE + REPLY_UTF16_SIZE + sizeof(SwkbdChangedStringArg)); + + SetReplyBase(reply, swkbd_state, SwkbdReplyType::ChangedString); + + const SwkbdChangedStringArg changed_string_arg{ + .text_length{static_cast(current_text.size())}, + .dictionary_start_cursor_position{-1}, + .dictionary_end_cursor_position{-1}, + .cursor_position{current_cursor_position}, + }; + + std::memcpy(reply.data() + REPLY_BASE_SIZE, current_text.data(), + current_text.size() * sizeof(char16_t)); + std::memcpy(reply.data() + REPLY_BASE_SIZE + REPLY_UTF16_SIZE, &changed_string_arg, + sizeof(SwkbdChangedStringArg)); + + broker.PushInteractiveDataFromApplet(std::make_shared(system, std::move(reply))); +} + +void SoftwareKeyboard::ReplyMovedCursor() { + LOG_DEBUG(Service_AM, "Sending Reply: MovedCursor"); + + std::vector reply(REPLY_BASE_SIZE + REPLY_UTF16_SIZE + sizeof(SwkbdMovedCursorArg)); + + SetReplyBase(reply, swkbd_state, SwkbdReplyType::MovedCursor); + + const SwkbdMovedCursorArg moved_cursor_arg{ + .text_length{static_cast(current_text.size())}, + .cursor_position{current_cursor_position}, + }; + + std::memcpy(reply.data() + REPLY_BASE_SIZE, current_text.data(), + current_text.size() * sizeof(char16_t)); + std::memcpy(reply.data() + REPLY_BASE_SIZE + REPLY_UTF16_SIZE, &moved_cursor_arg, + sizeof(SwkbdMovedCursorArg)); + + broker.PushInteractiveDataFromApplet(std::make_shared(system, std::move(reply))); +} + +void SoftwareKeyboard::ReplyMovedTab() { + LOG_DEBUG(Service_AM, "Sending Reply: MovedTab"); + + std::vector reply(REPLY_BASE_SIZE + REPLY_UTF16_SIZE + sizeof(SwkbdMovedTabArg)); + + SetReplyBase(reply, swkbd_state, SwkbdReplyType::MovedTab); + + const SwkbdMovedTabArg moved_tab_arg{ + .text_length{static_cast(current_text.size())}, + .cursor_position{current_cursor_position}, + }; + + std::memcpy(reply.data() + REPLY_BASE_SIZE, current_text.data(), + current_text.size() * sizeof(char16_t)); + std::memcpy(reply.data() + REPLY_BASE_SIZE + REPLY_UTF16_SIZE, &moved_tab_arg, + sizeof(SwkbdMovedTabArg)); + + broker.PushInteractiveDataFromApplet(std::make_shared(system, std::move(reply))); +} + +void SoftwareKeyboard::ReplyDecidedEnter() { + LOG_DEBUG(Service_AM, "Sending Reply: DecidedEnter"); + + std::vector reply(REPLY_BASE_SIZE + REPLY_UTF16_SIZE + sizeof(SwkbdDecidedEnterArg)); + + SetReplyBase(reply, swkbd_state, SwkbdReplyType::DecidedEnter); + + const SwkbdDecidedEnterArg decided_enter_arg{ + .text_length{static_cast(current_text.size())}, + }; + + std::memcpy(reply.data() + REPLY_BASE_SIZE, current_text.data(), + current_text.size() * sizeof(char16_t)); + std::memcpy(reply.data() + REPLY_BASE_SIZE + REPLY_UTF16_SIZE, &decided_enter_arg, + sizeof(SwkbdDecidedEnterArg)); + + broker.PushInteractiveDataFromApplet(std::make_shared(system, std::move(reply))); + + HideInlineKeyboard(); +} + +void SoftwareKeyboard::ReplyDecidedCancel() { + LOG_DEBUG(Service_AM, "Sending Reply: DecidedCancel"); + + std::vector reply(REPLY_BASE_SIZE); + + SetReplyBase(reply, swkbd_state, SwkbdReplyType::DecidedCancel); + + broker.PushInteractiveDataFromApplet(std::make_shared(system, std::move(reply))); + + HideInlineKeyboard(); +} + +void SoftwareKeyboard::ReplyChangedStringUtf8() { + LOG_DEBUG(Service_AM, "Sending Reply: ChangedStringUtf8"); + + std::vector reply(REPLY_BASE_SIZE + REPLY_UTF8_SIZE + sizeof(SwkbdChangedStringArg)); + + SetReplyBase(reply, swkbd_state, SwkbdReplyType::ChangedStringUtf8); + + std::string utf8_current_text = Common::UTF16ToUTF8(current_text); + + const SwkbdChangedStringArg changed_string_arg{ + .text_length{static_cast(current_text.size())}, + .dictionary_start_cursor_position{-1}, + .dictionary_end_cursor_position{-1}, + .cursor_position{current_cursor_position}, + }; + + std::memcpy(reply.data() + REPLY_BASE_SIZE, utf8_current_text.data(), utf8_current_text.size()); + std::memcpy(reply.data() + REPLY_BASE_SIZE + REPLY_UTF8_SIZE, &changed_string_arg, + sizeof(SwkbdChangedStringArg)); + + broker.PushInteractiveDataFromApplet(std::make_shared(system, std::move(reply))); +} + +void SoftwareKeyboard::ReplyMovedCursorUtf8() { + LOG_DEBUG(Service_AM, "Sending Reply: MovedCursorUtf8"); + + std::vector reply(REPLY_BASE_SIZE + REPLY_UTF8_SIZE + sizeof(SwkbdMovedCursorArg)); + + SetReplyBase(reply, swkbd_state, SwkbdReplyType::MovedCursorUtf8); + + std::string utf8_current_text = Common::UTF16ToUTF8(current_text); + + const SwkbdMovedCursorArg moved_cursor_arg{ + .text_length{static_cast(current_text.size())}, + .cursor_position{current_cursor_position}, + }; + + std::memcpy(reply.data() + REPLY_BASE_SIZE, utf8_current_text.data(), utf8_current_text.size()); + std::memcpy(reply.data() + REPLY_BASE_SIZE + REPLY_UTF8_SIZE, &moved_cursor_arg, + sizeof(SwkbdMovedCursorArg)); + + broker.PushInteractiveDataFromApplet(std::make_shared(system, std::move(reply))); +} + +void SoftwareKeyboard::ReplyDecidedEnterUtf8() { + LOG_DEBUG(Service_AM, "Sending Reply: DecidedEnterUtf8"); + + std::vector reply(REPLY_BASE_SIZE + REPLY_UTF8_SIZE + sizeof(SwkbdDecidedEnterArg)); + + SetReplyBase(reply, swkbd_state, SwkbdReplyType::DecidedEnterUtf8); + + std::string utf8_current_text = Common::UTF16ToUTF8(current_text); + + const SwkbdDecidedEnterArg decided_enter_arg{ + .text_length{static_cast(current_text.size())}, + }; + + std::memcpy(reply.data() + REPLY_BASE_SIZE, utf8_current_text.data(), utf8_current_text.size()); + std::memcpy(reply.data() + REPLY_BASE_SIZE + REPLY_UTF8_SIZE, &decided_enter_arg, + sizeof(SwkbdDecidedEnterArg)); + + broker.PushInteractiveDataFromApplet(std::make_shared(system, std::move(reply))); + + HideInlineKeyboard(); +} + +void SoftwareKeyboard::ReplyUnsetCustomizeDic() { + LOG_DEBUG(Service_AM, "Sending Reply: UnsetCustomizeDic"); + + std::vector reply(REPLY_BASE_SIZE); + + SetReplyBase(reply, swkbd_state, SwkbdReplyType::UnsetCustomizeDic); + + broker.PushInteractiveDataFromApplet(std::make_shared(system, std::move(reply))); +} + +void SoftwareKeyboard::ReplyReleasedUserWordInfo() { + LOG_DEBUG(Service_AM, "Sending Reply: ReleasedUserWordInfo"); + + std::vector reply(REPLY_BASE_SIZE); + + SetReplyBase(reply, swkbd_state, SwkbdReplyType::ReleasedUserWordInfo); + + broker.PushInteractiveDataFromApplet(std::make_shared(system, std::move(reply))); +} + +void SoftwareKeyboard::ReplyUnsetCustomizedDictionaries() { + LOG_DEBUG(Service_AM, "Sending Reply: UnsetCustomizedDictionaries"); + + std::vector reply(REPLY_BASE_SIZE); + + SetReplyBase(reply, swkbd_state, SwkbdReplyType::UnsetCustomizedDictionaries); + + broker.PushInteractiveDataFromApplet(std::make_shared(system, std::move(reply))); +} + +void SoftwareKeyboard::ReplyChangedStringV2() { + LOG_DEBUG(Service_AM, "Sending Reply: ChangedStringV2"); + + std::vector reply(REPLY_BASE_SIZE + REPLY_UTF16_SIZE + sizeof(SwkbdChangedStringArg) + 1); + + SetReplyBase(reply, swkbd_state, SwkbdReplyType::ChangedStringV2); + + const SwkbdChangedStringArg changed_string_arg{ + .text_length{static_cast(current_text.size())}, + .dictionary_start_cursor_position{-1}, + .dictionary_end_cursor_position{-1}, + .cursor_position{current_cursor_position}, + }; + + constexpr u8 flag = 0; + + std::memcpy(reply.data() + REPLY_BASE_SIZE, current_text.data(), + current_text.size() * sizeof(char16_t)); + std::memcpy(reply.data() + REPLY_BASE_SIZE + REPLY_UTF16_SIZE, &changed_string_arg, + sizeof(SwkbdChangedStringArg)); + std::memcpy(reply.data() + REPLY_BASE_SIZE + REPLY_UTF16_SIZE + sizeof(SwkbdChangedStringArg), + &flag, 1); + + broker.PushInteractiveDataFromApplet(std::make_shared(system, std::move(reply))); +} + +void SoftwareKeyboard::ReplyMovedCursorV2() { + LOG_DEBUG(Service_AM, "Sending Reply: MovedCursorV2"); + + std::vector reply(REPLY_BASE_SIZE + REPLY_UTF16_SIZE + sizeof(SwkbdMovedCursorArg) + 1); + + SetReplyBase(reply, swkbd_state, SwkbdReplyType::MovedCursorV2); + + const SwkbdMovedCursorArg moved_cursor_arg{ + .text_length{static_cast(current_text.size())}, + .cursor_position{current_cursor_position}, + }; + + constexpr u8 flag = 0; + + std::memcpy(reply.data() + REPLY_BASE_SIZE, current_text.data(), + current_text.size() * sizeof(char16_t)); + std::memcpy(reply.data() + REPLY_BASE_SIZE + REPLY_UTF16_SIZE, &moved_cursor_arg, + sizeof(SwkbdMovedCursorArg)); + std::memcpy(reply.data() + REPLY_BASE_SIZE + REPLY_UTF16_SIZE + sizeof(SwkbdMovedCursorArg), + &flag, 1); + + broker.PushInteractiveDataFromApplet(std::make_shared(system, std::move(reply))); +} + +void SoftwareKeyboard::ReplyChangedStringUtf8V2() { + LOG_DEBUG(Service_AM, "Sending Reply: ChangedStringUtf8V2"); + + std::vector reply(REPLY_BASE_SIZE + REPLY_UTF8_SIZE + sizeof(SwkbdChangedStringArg) + 1); + + SetReplyBase(reply, swkbd_state, SwkbdReplyType::ChangedStringUtf8V2); + + std::string utf8_current_text = Common::UTF16ToUTF8(current_text); + + const SwkbdChangedStringArg changed_string_arg{ + .text_length{static_cast(current_text.size())}, + .dictionary_start_cursor_position{-1}, + .dictionary_end_cursor_position{-1}, + .cursor_position{current_cursor_position}, + }; + + constexpr u8 flag = 0; + + std::memcpy(reply.data() + REPLY_BASE_SIZE, utf8_current_text.data(), utf8_current_text.size()); + std::memcpy(reply.data() + REPLY_BASE_SIZE + REPLY_UTF8_SIZE, &changed_string_arg, + sizeof(SwkbdChangedStringArg)); + std::memcpy(reply.data() + REPLY_BASE_SIZE + REPLY_UTF8_SIZE + sizeof(SwkbdChangedStringArg), + &flag, 1); + + broker.PushInteractiveDataFromApplet(std::make_shared(system, std::move(reply))); +} + +void SoftwareKeyboard::ReplyMovedCursorUtf8V2() { + LOG_DEBUG(Service_AM, "Sending Reply: MovedCursorUtf8V2"); + + std::vector reply(REPLY_BASE_SIZE + REPLY_UTF8_SIZE + sizeof(SwkbdMovedCursorArg) + 1); + + SetReplyBase(reply, swkbd_state, SwkbdReplyType::MovedCursorUtf8V2); + + std::string utf8_current_text = Common::UTF16ToUTF8(current_text); + + const SwkbdMovedCursorArg moved_cursor_arg{ + .text_length{static_cast(current_text.size())}, + .cursor_position{current_cursor_position}, + }; + + constexpr u8 flag = 0; + + std::memcpy(reply.data() + REPLY_BASE_SIZE, utf8_current_text.data(), utf8_current_text.size()); + std::memcpy(reply.data() + REPLY_BASE_SIZE + REPLY_UTF8_SIZE, &moved_cursor_arg, + sizeof(SwkbdMovedCursorArg)); + std::memcpy(reply.data() + REPLY_BASE_SIZE + REPLY_UTF8_SIZE + sizeof(SwkbdMovedCursorArg), + &flag, 1); + + broker.PushInteractiveDataFromApplet(std::make_shared(system, std::move(reply))); +} } // namespace Service::AM::Applets diff --git a/src/core/hle/service/am/applets/software_keyboard.h b/src/core/hle/service/am/applets/software_keyboard.h index c161ec9ac8..85aeb4eb1b 100644 --- a/src/core/hle/service/am/applets/software_keyboard.h +++ b/src/core/hle/service/am/applets/software_keyboard.h @@ -1,4 +1,4 @@ -// Copyright 2018 yuzu emulator team +// Copyright 2021 yuzu Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -8,6 +8,7 @@ #include "common/common_types.h" #include "core/hle/result.h" #include "core/hle/service/am/applets/applets.h" +#include "core/hle/service/am/applets/software_keyboard_types.h" namespace Core { class System; @@ -17,8 +18,8 @@ namespace Service::AM::Applets { class SoftwareKeyboard final : public Applet { public: - explicit SoftwareKeyboard(Core::System& system_, - const Core::Frontend::SoftwareKeyboardApplet& frontend_); + explicit SoftwareKeyboard(Core::System& system_, LibraryAppletMode applet_mode_, + Core::Frontend::SoftwareKeyboardApplet& frontend_); ~SoftwareKeyboard() override; void Initialize() override; @@ -28,10 +29,139 @@ public: void ExecuteInteractive() override; void Execute() override; -private: - const Core::Frontend::SoftwareKeyboardApplet& frontend; + /** + * Submits the input text to the application. + * If text checking is enabled, the application will verify the input text. + * If use_utf8 is enabled, the input text will be converted to UTF-8 prior to being submitted. + * This should only be used by the normal software keyboard. + * + * @param result SwkbdResult enum + * @param submitted_text UTF-16 encoded string + */ + void SubmitTextNormal(SwkbdResult result, std::u16string submitted_text); + /** + * Submits the input text to the application. + * If utf8_mode is enabled, the input text will be converted to UTF-8 prior to being submitted. + * This should only be used by the inline software keyboard. + * + * @param reply_type SwkbdReplyType enum + * @param submitted_text UTF-16 encoded string + * @param cursor_position The current position of the text cursor + */ + void SubmitTextInline(SwkbdReplyType reply_type, std::u16string submitted_text, + s32 cursor_position); + +private: + /// Initializes the normal software keyboard. + void InitializeForeground(); + + /// Initializes the inline software keyboard. + void InitializeBackground(LibraryAppletMode applet_mode); + + /// Processes the text check sent by the application. + void ProcessTextCheck(); + + /// Processes the inline software keyboard request command sent by the application. + void ProcessInlineKeyboardRequest(); + + /// Submits the input text and exits the applet. + void SubmitNormalOutputAndExit(SwkbdResult result, std::u16string submitted_text); + + /// Submits the input text for text checking. + void SubmitForTextCheck(std::u16string submitted_text); + + /// Sends a reply to the application after processing a request command. + void SendReply(SwkbdReplyType reply_type); + + /// Changes the inline keyboard state. + void ChangeState(SwkbdState state); + + /** + * Signals the frontend to initialize the software keyboard with common parameters. + * This initializes either the normal software keyboard or the inline software keyboard + * depending on the state of is_background. + * Note that this does not cause the keyboard to appear. + * Use the respective Show*Keyboard() functions to cause the respective keyboards to appear. + */ + void InitializeFrontendKeyboard(); + + /// Signals the frontend to show the normal software keyboard. + void ShowNormalKeyboard(); + + /// Signals the frontend to show the text check dialog. + void ShowTextCheckDialog(SwkbdTextCheckResult text_check_result, + std::u16string text_check_message); + + /// Signals the frontend to show the inline software keyboard. + void ShowInlineKeyboard(); + + /// Signals the frontend to hide the inline software keyboard. + void HideInlineKeyboard(); + + /// Signals the frontend that the current inline keyboard text has changed. + void InlineTextChanged(); + + /// Signals both the frontend and application that the software keyboard is exiting. + void ExitKeyboard(); + + // Inline Software Keyboard Requests + + void RequestFinalize(const std::vector& request_data); + void RequestSetUserWordInfo(const std::vector& request_data); + void RequestSetCustomizeDic(const std::vector& request_data); + void RequestCalc(const std::vector& request_data); + void RequestSetCustomizedDictionaries(const std::vector& request_data); + void RequestUnsetCustomizedDictionaries(const std::vector& request_data); + void RequestSetChangedStringV2Flag(const std::vector& request_data); + void RequestSetMovedCursorV2Flag(const std::vector& request_data); + + // Inline Software Keyboard Replies + + void ReplyFinishedInitialize(); + void ReplyDefault(); + void ReplyChangedString(); + void ReplyMovedCursor(); + void ReplyMovedTab(); + void ReplyDecidedEnter(); + void ReplyDecidedCancel(); + void ReplyChangedStringUtf8(); + void ReplyMovedCursorUtf8(); + void ReplyDecidedEnterUtf8(); + void ReplyUnsetCustomizeDic(); + void ReplyReleasedUserWordInfo(); + void ReplyUnsetCustomizedDictionaries(); + void ReplyChangedStringV2(); + void ReplyMovedCursorV2(); + void ReplyChangedStringUtf8V2(); + void ReplyMovedCursorUtf8V2(); + + LibraryAppletMode applet_mode; + Core::Frontend::SoftwareKeyboardApplet& frontend; Core::System& system; + + SwkbdAppletVersion swkbd_applet_version; + + SwkbdConfigCommon swkbd_config_common; + SwkbdConfigOld swkbd_config_old; + SwkbdConfigOld2 swkbd_config_old2; + SwkbdConfigNew swkbd_config_new; + std::u16string initial_text; + + SwkbdState swkbd_state{SwkbdState::NotInitialized}; + SwkbdInitializeArg swkbd_initialize_arg; + SwkbdCalcArg swkbd_calc_arg; + bool use_changed_string_v2{false}; + bool use_moved_cursor_v2{false}; + bool inline_use_utf8{false}; + s32 current_cursor_position{}; + + std::u16string current_text; + + bool is_background{false}; + + bool complete{false}; + ResultCode status{RESULT_SUCCESS}; }; } // namespace Service::AM::Applets diff --git a/src/core/hle/service/am/applets/software_keyboard_types.h b/src/core/hle/service/am/applets/software_keyboard_types.h new file mode 100644 index 0000000000..21aa8e8005 --- /dev/null +++ b/src/core/hle/service/am/applets/software_keyboard_types.h @@ -0,0 +1,295 @@ +// Copyright 2021 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include + +#include "common/bit_field.h" +#include "common/common_funcs.h" +#include "common/common_types.h" +#include "common/swap.h" + +namespace Service::AM::Applets { + +constexpr std::size_t MAX_OK_TEXT_LENGTH = 8; +constexpr std::size_t MAX_HEADER_TEXT_LENGTH = 64; +constexpr std::size_t MAX_SUB_TEXT_LENGTH = 128; +constexpr std::size_t MAX_GUIDE_TEXT_LENGTH = 256; +constexpr std::size_t STRING_BUFFER_SIZE = 0x7D4; + +enum class SwkbdAppletVersion : u32_le { + Version5 = 0x5, // 1.0.0 + Version65542 = 0x10006, // 2.0.0 - 2.3.0 + Version196615 = 0x30007, // 3.0.0 - 3.0.2 + Version262152 = 0x40008, // 4.0.0 - 4.1.0 + Version327689 = 0x50009, // 5.0.0 - 5.1.0 + Version393227 = 0x6000B, // 6.0.0 - 7.0.1 + Version524301 = 0x8000D, // 8.0.0+ +}; + +enum class SwkbdType : u32 { + Normal, + NumberPad, + Qwerty, + Unknown3, + Latin, + SimplifiedChinese, + TraditionalChinese, + Korean, +}; + +enum class SwkbdInitialCursorPosition : u32 { + Start, + End, +}; + +enum class SwkbdPasswordMode : u32 { + Disabled, + Enabled, +}; + +enum class SwkbdTextDrawType : u32 { + Line, + Box, + DownloadCode, +}; + +enum class SwkbdResult : u32 { + Ok, + Cancel, +}; + +enum class SwkbdTextCheckResult : u32 { + Success, + Failure, + Confirm, + Silent, +}; + +enum class SwkbdState : u32 { + NotInitialized = 0x0, + InitializedIsHidden = 0x1, + InitializedIsAppearing = 0x2, + InitializedIsShown = 0x3, + InitializedIsDisappearing = 0x4, +}; + +enum class SwkbdRequestCommand : u32 { + Finalize = 0x4, + SetUserWordInfo = 0x6, + SetCustomizeDic = 0x7, + Calc = 0xA, + SetCustomizedDictionaries = 0xB, + UnsetCustomizedDictionaries = 0xC, + SetChangedStringV2Flag = 0xD, + SetMovedCursorV2Flag = 0xE, +}; + +enum class SwkbdReplyType : u32 { + FinishedInitialize = 0x0, + Default = 0x1, + ChangedString = 0x2, + MovedCursor = 0x3, + MovedTab = 0x4, + DecidedEnter = 0x5, + DecidedCancel = 0x6, + ChangedStringUtf8 = 0x7, + MovedCursorUtf8 = 0x8, + DecidedEnterUtf8 = 0x9, + UnsetCustomizeDic = 0xA, + ReleasedUserWordInfo = 0xB, + UnsetCustomizedDictionaries = 0xC, + ChangedStringV2 = 0xD, + MovedCursorV2 = 0xE, + ChangedStringUtf8V2 = 0xF, + MovedCursorUtf8V2 = 0x10, +}; + +struct SwkbdKeyDisableFlags { + union { + u32 raw{}; + + BitField<1, 1, u32> space; + BitField<2, 1, u32> at; + BitField<3, 1, u32> percent; + BitField<4, 1, u32> slash; + BitField<5, 1, u32> backslash; + BitField<6, 1, u32> numbers; + BitField<7, 1, u32> download_code; + BitField<8, 1, u32> username; + }; +}; +static_assert(sizeof(SwkbdKeyDisableFlags) == 0x4, "SwkbdKeyDisableFlags has incorrect size."); + +struct SwkbdConfigCommon { + SwkbdType type{}; + std::array ok_text{}; + char16_t left_optional_symbol_key{}; + char16_t right_optional_symbol_key{}; + bool use_prediction{}; + INSERT_PADDING_BYTES(1); + SwkbdKeyDisableFlags key_disable_flags{}; + SwkbdInitialCursorPosition initial_cursor_position{}; + std::array header_text{}; + std::array sub_text{}; + std::array guide_text{}; + u32 max_text_length{}; + u32 min_text_length{}; + SwkbdPasswordMode password_mode{}; + SwkbdTextDrawType text_draw_type{}; + bool enable_return_button{}; + bool use_utf8{}; + bool use_blur_background{}; + INSERT_PADDING_BYTES(1); + u32 initial_string_offset{}; + u32 initial_string_length{}; + u32 user_dictionary_offset{}; + u32 user_dictionary_entries{}; + bool use_text_check{}; + INSERT_PADDING_BYTES(3); +}; +static_assert(sizeof(SwkbdConfigCommon) == 0x3D4, "SwkbdConfigCommon has incorrect size."); + +#pragma pack(push, 4) +// SwkbdAppletVersion 0x5, 0x10006 +struct SwkbdConfigOld { + INSERT_PADDING_WORDS(1); + VAddr text_check_callback{}; +}; +static_assert(sizeof(SwkbdConfigOld) == 0x3E0 - sizeof(SwkbdConfigCommon), + "SwkbdConfigOld has incorrect size."); + +// SwkbdAppletVersion 0x30007, 0x40008, 0x50009 +struct SwkbdConfigOld2 { + INSERT_PADDING_WORDS(1); + VAddr text_check_callback{}; + std::array text_grouping{}; +}; +static_assert(sizeof(SwkbdConfigOld2) == 0x400 - sizeof(SwkbdConfigCommon), + "SwkbdConfigOld2 has incorrect size."); + +// SwkbdAppletVersion 0x6000B, 0x8000D +struct SwkbdConfigNew { + std::array text_grouping{}; + std::array customized_dictionary_set_entries{}; + u8 total_customized_dictionary_set_entries{}; + bool disable_cancel_button{}; + INSERT_PADDING_BYTES(18); +}; +static_assert(sizeof(SwkbdConfigNew) == 0x4C8 - sizeof(SwkbdConfigCommon), + "SwkbdConfigNew has incorrect size."); +#pragma pack(pop) + +struct SwkbdTextCheck { + SwkbdTextCheckResult text_check_result{}; + std::array text_check_message{}; +}; +static_assert(sizeof(SwkbdTextCheck) == 0x7D8, "SwkbdTextCheck has incorrect size."); + +struct SwkbdCalcArgFlags { + union { + u64 raw{}; + + BitField<0, 1, u64> set_initialize_arg; + BitField<1, 1, u64> set_volume; + BitField<2, 1, u64> appear; + BitField<3, 1, u64> set_input_text; + BitField<4, 1, u64> set_cursor_position; + BitField<5, 1, u64> set_utf8_mode; + BitField<6, 1, u64> unset_customize_dic; + BitField<7, 1, u64> disappear; + BitField<8, 1, u64> unknown; + BitField<9, 1, u64> set_key_top_translate_scale; + BitField<10, 1, u64> unset_user_word_info; + BitField<11, 1, u64> set_disable_hardware_keyboard; + }; +}; +static_assert(sizeof(SwkbdCalcArgFlags) == 0x8, "SwkbdCalcArgFlags has incorrect size."); + +struct SwkbdInitializeArg { + u32 unknown{}; + bool library_applet_mode_flag{}; + bool is_above_hos_500{}; + INSERT_PADDING_BYTES(2); +}; +static_assert(sizeof(SwkbdInitializeArg) == 0x8, "SwkbdInitializeArg has incorrect size."); + +struct SwkbdAppearArg { + SwkbdType type{}; + std::array ok_text{}; + char16_t left_optional_symbol_key{}; + char16_t right_optional_symbol_key{}; + bool use_prediction{}; + bool disable_cancel_button{}; + SwkbdKeyDisableFlags key_disable_flags{}; + u32 max_text_length{}; + u32 min_text_length{}; + bool enable_return_button{}; + INSERT_PADDING_BYTES(3); + u32 flags{}; + INSERT_PADDING_WORDS(6); +}; +static_assert(sizeof(SwkbdAppearArg) == 0x48, "SwkbdAppearArg has incorrect size."); + +struct SwkbdCalcArg { + u32 unknown{}; + u16 calc_arg_size{}; + INSERT_PADDING_BYTES(2); + SwkbdCalcArgFlags flags{}; + SwkbdInitializeArg initialize_arg{}; + f32 volume{}; + s32 cursor_position{}; + SwkbdAppearArg appear_arg{}; + std::array input_text{}; + bool utf8_mode{}; + INSERT_PADDING_BYTES(1); + bool enable_backspace_button{}; + INSERT_PADDING_BYTES(3); + bool key_top_as_floating{}; + bool footer_scalable{}; + bool alpha_enabled_in_input_mode{}; + u8 input_mode_fade_type{}; + bool disable_touch{}; + bool disable_hardware_keyboard{}; + INSERT_PADDING_BYTES(8); + f32 key_top_scale_x{}; + f32 key_top_scale_y{}; + f32 key_top_translate_x{}; + f32 key_top_translate_y{}; + f32 key_top_bg_alpha{}; + f32 footer_bg_alpha{}; + f32 balloon_scale{}; + INSERT_PADDING_WORDS(4); + u8 se_group{}; + INSERT_PADDING_BYTES(3); +}; +static_assert(sizeof(SwkbdCalcArg) == 0x4A0, "SwkbdCalcArg has incorrect size."); + +struct SwkbdChangedStringArg { + u32 text_length{}; + s32 dictionary_start_cursor_position{}; + s32 dictionary_end_cursor_position{}; + s32 cursor_position{}; +}; +static_assert(sizeof(SwkbdChangedStringArg) == 0x10, "SwkbdChangedStringArg has incorrect size."); + +struct SwkbdMovedCursorArg { + u32 text_length{}; + s32 cursor_position{}; +}; +static_assert(sizeof(SwkbdMovedCursorArg) == 0x8, "SwkbdMovedCursorArg has incorrect size."); + +struct SwkbdMovedTabArg { + u32 text_length{}; + s32 cursor_position{}; +}; +static_assert(sizeof(SwkbdMovedTabArg) == 0x8, "SwkbdMovedTabArg has incorrect size."); + +struct SwkbdDecidedEnterArg { + u32 text_length{}; +}; +static_assert(sizeof(SwkbdDecidedEnterArg) == 0x4, "SwkbdDecidedEnterArg has incorrect size."); + +} // namespace Service::AM::Applets From 578e6c5a57bc29aed27e604ac0ede34f87bae86d Mon Sep 17 00:00:00 2001 From: Morph <39850852+Morph1984@users.noreply.github.com> Date: Tue, 16 Mar 2021 13:01:03 -0400 Subject: [PATCH 07/14] applets/swkbd: Implement the Default Software Keyboard frontend --- .../frontend/applets/software_keyboard.cpp | 140 +++++++++++++++++- src/core/frontend/applets/software_keyboard.h | 98 +++++++++++- 2 files changed, 236 insertions(+), 2 deletions(-) diff --git a/src/core/frontend/applets/software_keyboard.cpp b/src/core/frontend/applets/software_keyboard.cpp index 73e7a89b99..12c76c9ee0 100644 --- a/src/core/frontend/applets/software_keyboard.cpp +++ b/src/core/frontend/applets/software_keyboard.cpp @@ -1,11 +1,149 @@ -// Copyright 2018 yuzu emulator team +// Copyright 2021 yuzu Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. +#include + +#include "common/logging/log.h" +#include "common/string_util.h" #include "core/frontend/applets/software_keyboard.h" namespace Core::Frontend { SoftwareKeyboardApplet::~SoftwareKeyboardApplet() = default; +DefaultSoftwareKeyboardApplet::~DefaultSoftwareKeyboardApplet() = default; + +void DefaultSoftwareKeyboardApplet::InitializeKeyboard( + bool is_inline, KeyboardInitializeParameters initialize_parameters, + std::function submit_normal_callback_, + std::function + submit_inline_callback_) { + if (is_inline) { + LOG_WARNING( + Service_AM, + "(STUBBED) called, backend requested to initialize the inline software keyboard."); + + submit_inline_callback = std::move(submit_inline_callback_); + } else { + LOG_WARNING( + Service_AM, + "(STUBBED) called, backend requested to initialize the normal software keyboard."); + + submit_normal_callback = std::move(submit_normal_callback_); + } + + parameters = std::move(initialize_parameters); + + LOG_INFO(Service_AM, + "\nKeyboardInitializeParameters:" + "\nok_text={}" + "\nheader_text={}" + "\nsub_text={}" + "\nguide_text={}" + "\ninitial_text={}" + "\nmax_text_length={}" + "\nmin_text_length={}" + "\ninitial_cursor_position={}" + "\ntype={}" + "\npassword_mode={}" + "\ntext_draw_type={}" + "\nkey_disable_flags={}" + "\nuse_blur_background={}" + "\nenable_backspace_button={}" + "\nenable_return_button={}" + "\ndisable_cancel_button={}", + Common::UTF16ToUTF8(parameters.ok_text), Common::UTF16ToUTF8(parameters.header_text), + Common::UTF16ToUTF8(parameters.sub_text), Common::UTF16ToUTF8(parameters.guide_text), + Common::UTF16ToUTF8(parameters.initial_text), parameters.max_text_length, + parameters.min_text_length, parameters.initial_cursor_position, parameters.type, + parameters.password_mode, parameters.text_draw_type, parameters.key_disable_flags.raw, + parameters.use_blur_background, parameters.enable_backspace_button, + parameters.enable_return_button, parameters.disable_cancel_button); +} + +void DefaultSoftwareKeyboardApplet::ShowNormalKeyboard() const { + LOG_WARNING(Service_AM, + "(STUBBED) called, backend requested to show the normal software keyboard."); + + SubmitNormalText(u"yuzu"); +} + +void DefaultSoftwareKeyboardApplet::ShowTextCheckDialog( + Service::AM::Applets::SwkbdTextCheckResult text_check_result, + std::u16string text_check_message) const { + LOG_WARNING(Service_AM, "(STUBBED) called, backend requested to show the text check dialog."); +} + +void DefaultSoftwareKeyboardApplet::ShowInlineKeyboard( + InlineAppearParameters appear_parameters) const { + LOG_WARNING(Service_AM, + "(STUBBED) called, backend requested to show the inline software keyboard."); + + LOG_INFO(Service_AM, + "\nInlineAppearParameters:" + "\nmax_text_length={}" + "\nmin_text_length={}" + "\nkey_top_scale_x={}" + "\nkey_top_scale_y={}" + "\nkey_top_translate_x={}" + "\nkey_top_translate_y={}" + "\ntype={}" + "\nkey_disable_flags={}" + "\nkey_top_as_floating={}" + "\nenable_backspace_button={}" + "\nenable_return_button={}" + "\ndisable_cancel_button={}", + appear_parameters.max_text_length, appear_parameters.min_text_length, + appear_parameters.key_top_scale_x, appear_parameters.key_top_scale_y, + appear_parameters.key_top_translate_x, appear_parameters.key_top_translate_y, + appear_parameters.type, appear_parameters.key_disable_flags.raw, + appear_parameters.key_top_as_floating, appear_parameters.enable_backspace_button, + appear_parameters.enable_return_button, appear_parameters.disable_cancel_button); + + std::thread([this] { SubmitInlineText(u"yuzu"); }).detach(); +} + +void DefaultSoftwareKeyboardApplet::HideInlineKeyboard() const { + LOG_WARNING(Service_AM, + "(STUBBED) called, backend requested to hide the inline software keyboard."); +} + +void DefaultSoftwareKeyboardApplet::InlineTextChanged(InlineTextParameters text_parameters) const { + LOG_WARNING(Service_AM, + "(STUBBED) called, backend requested to change the inline keyboard text."); + + LOG_INFO(Service_AM, + "\nInlineTextParameters:" + "\ninput_text={}" + "\ncursor_position={}", + Common::UTF16ToUTF8(text_parameters.input_text), text_parameters.cursor_position); + + submit_inline_callback(Service::AM::Applets::SwkbdReplyType::ChangedString, + text_parameters.input_text, text_parameters.cursor_position); +} + +void DefaultSoftwareKeyboardApplet::ExitKeyboard() const { + LOG_WARNING(Service_AM, "(STUBBED) called, backend requested to exit the software keyboard."); +} + +void DefaultSoftwareKeyboardApplet::SubmitNormalText(std::u16string text) const { + submit_normal_callback(Service::AM::Applets::SwkbdResult::Ok, text); +} + +void DefaultSoftwareKeyboardApplet::SubmitInlineText(std::u16string_view text) const { + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + + for (std::size_t index = 0; index < text.size(); ++index) { + submit_inline_callback(Service::AM::Applets::SwkbdReplyType::ChangedString, + std::u16string(text.data(), text.data() + index + 1), + static_cast(index) + 1); + + std::this_thread::sleep_for(std::chrono::milliseconds(250)); + } + + submit_inline_callback(Service::AM::Applets::SwkbdReplyType::DecidedEnter, std::u16string(text), + static_cast(text.size())); +} + } // namespace Core::Frontend diff --git a/src/core/frontend/applets/software_keyboard.h b/src/core/frontend/applets/software_keyboard.h index 54528837ee..506eb35bb1 100644 --- a/src/core/frontend/applets/software_keyboard.h +++ b/src/core/frontend/applets/software_keyboard.h @@ -1,20 +1,116 @@ -// Copyright 2018 yuzu emulator team +// Copyright 2021 yuzu Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. #pragma once +#include +#include + #include "common/common_types.h" +#include "core/hle/service/am/applets/software_keyboard_types.h" + namespace Core::Frontend { +struct KeyboardInitializeParameters { + std::u16string ok_text; + std::u16string header_text; + std::u16string sub_text; + std::u16string guide_text; + std::u16string initial_text; + u32 max_text_length; + u32 min_text_length; + s32 initial_cursor_position; + Service::AM::Applets::SwkbdType type; + Service::AM::Applets::SwkbdPasswordMode password_mode; + Service::AM::Applets::SwkbdTextDrawType text_draw_type; + Service::AM::Applets::SwkbdKeyDisableFlags key_disable_flags; + bool use_blur_background; + bool enable_backspace_button; + bool enable_return_button; + bool disable_cancel_button; +}; + +struct InlineAppearParameters { + u32 max_text_length; + u32 min_text_length; + f32 key_top_scale_x; + f32 key_top_scale_y; + f32 key_top_translate_x; + f32 key_top_translate_y; + Service::AM::Applets::SwkbdType type; + Service::AM::Applets::SwkbdKeyDisableFlags key_disable_flags; + bool key_top_as_floating; + bool enable_backspace_button; + bool enable_return_button; + bool disable_cancel_button; +}; + +struct InlineTextParameters { + std::u16string input_text; + s32 cursor_position; +}; + class SoftwareKeyboardApplet { public: virtual ~SoftwareKeyboardApplet(); + + virtual void InitializeKeyboard( + bool is_inline, KeyboardInitializeParameters initialize_parameters, + std::function + submit_normal_callback_, + std::function + submit_inline_callback_) = 0; + + virtual void ShowNormalKeyboard() const = 0; + + virtual void ShowTextCheckDialog(Service::AM::Applets::SwkbdTextCheckResult text_check_result, + std::u16string text_check_message) const = 0; + + virtual void ShowInlineKeyboard(InlineAppearParameters appear_parameters) const = 0; + + virtual void HideInlineKeyboard() const = 0; + + virtual void InlineTextChanged(InlineTextParameters text_parameters) const = 0; + + virtual void ExitKeyboard() const = 0; }; class DefaultSoftwareKeyboardApplet final : public SoftwareKeyboardApplet { public: + ~DefaultSoftwareKeyboardApplet() override; + + void InitializeKeyboard( + bool is_inline, KeyboardInitializeParameters initialize_parameters, + std::function + submit_normal_callback_, + std::function + submit_inline_callback_) override; + + void ShowNormalKeyboard() const override; + + void ShowTextCheckDialog(Service::AM::Applets::SwkbdTextCheckResult text_check_result, + std::u16string text_check_message) const override; + + void ShowInlineKeyboard(InlineAppearParameters appear_parameters) const override; + + void HideInlineKeyboard() const override; + + void InlineTextChanged(InlineTextParameters text_parameters) const override; + + void ExitKeyboard() const override; + +private: + void SubmitNormalText(std::u16string text) const; + void SubmitInlineText(std::u16string_view text) const; + + KeyboardInitializeParameters parameters; + + mutable std::function + submit_normal_callback; + mutable std::function + submit_inline_callback; }; } // namespace Core::Frontend From e681723a4a5c62e2cecb9f1a62fa544a79fddd6d Mon Sep 17 00:00:00 2001 From: Its-Rei Date: Tue, 16 Mar 2021 14:24:19 -0400 Subject: [PATCH 08/14] icons: Add icons for the On-Screen Keyboard overlay --- dist/icons/overlay/arrow_left.png | Bin 0 -> 1490 bytes dist/icons/overlay/arrow_left_dark.png | Bin 0 -> 712 bytes dist/icons/overlay/arrow_right.png | Bin 0 -> 1394 bytes dist/icons/overlay/arrow_right_dark.png | Bin 0 -> 683 bytes dist/icons/overlay/button_A.png | Bin 0 -> 3494 bytes dist/icons/overlay/button_A_dark.png | Bin 0 -> 3167 bytes dist/icons/overlay/button_B.png | Bin 0 -> 3375 bytes dist/icons/overlay/button_B_dark.png | Bin 0 -> 2975 bytes dist/icons/overlay/button_L.png | Bin 0 -> 796 bytes dist/icons/overlay/button_L_dark.png | Bin 0 -> 745 bytes dist/icons/overlay/button_R.png | Bin 0 -> 1841 bytes dist/icons/overlay/button_R_dark.png | Bin 0 -> 1835 bytes dist/icons/overlay/button_X.png | Bin 0 -> 3968 bytes dist/icons/overlay/button_X_dark.png | Bin 0 -> 3530 bytes dist/icons/overlay/button_Y.png | Bin 0 -> 3337 bytes dist/icons/overlay/button_Y_dark.png | Bin 0 -> 2883 bytes dist/icons/overlay/button_minus.png | Bin 0 -> 2401 bytes dist/icons/overlay/button_minus_dark.png | Bin 0 -> 1969 bytes dist/icons/overlay/button_plus.png | Bin 0 -> 2497 bytes dist/icons/overlay/button_plus_dark.png | Bin 0 -> 2066 bytes dist/icons/overlay/button_press_stick.png | Bin 0 -> 5225 bytes .../icons/overlay/button_press_stick_dark.png | Bin 0 -> 3636 bytes dist/icons/overlay/controller_dual_joycon.png | Bin 0 -> 7312 bytes .../overlay/controller_dual_joycon_dark.png | Bin 0 -> 5889 bytes dist/icons/overlay/controller_handheld.png | Bin 0 -> 4645 bytes .../overlay/controller_handheld_dark.png | Bin 0 -> 3745 bytes dist/icons/overlay/controller_pro.png | Bin 0 -> 9493 bytes dist/icons/overlay/controller_pro_dark.png | Bin 0 -> 7488 bytes .../overlay/controller_single_joycon_left.png | Bin 0 -> 7489 bytes .../controller_single_joycon_left_a.png | Bin 0 -> 2609 bytes .../controller_single_joycon_left_a_dark.png | Bin 0 -> 2564 bytes .../controller_single_joycon_left_b.png | Bin 0 -> 2559 bytes .../controller_single_joycon_left_b_dark.png | Bin 0 -> 2383 bytes .../controller_single_joycon_left_dark.png | Bin 0 -> 6768 bytes .../controller_single_joycon_left_x.png | Bin 0 -> 2541 bytes .../controller_single_joycon_left_x_dark.png | Bin 0 -> 2392 bytes .../controller_single_joycon_left_y.png | Bin 0 -> 2641 bytes .../controller_single_joycon_left_y_dark.png | Bin 0 -> 2639 bytes .../controller_single_joycon_right.png | Bin 0 -> 7497 bytes .../controller_single_joycon_right_dark.png | Bin 0 -> 6729 bytes dist/icons/overlay/osk_button_B.png | Bin 0 -> 741 bytes dist/icons/overlay/osk_button_B_dark.png | Bin 0 -> 767 bytes .../overlay/osk_button_B_dark_disabled.png | Bin 0 -> 781 bytes dist/icons/overlay/osk_button_B_disabled.png | Bin 0 -> 791 bytes dist/icons/overlay/osk_button_Y.png | Bin 0 -> 726 bytes dist/icons/overlay/osk_button_Y_dark.png | Bin 0 -> 502 bytes .../overlay/osk_button_Y_dark_disabled.png | Bin 0 -> 694 bytes dist/icons/overlay/osk_button_Y_disabled.png | Bin 0 -> 699 bytes dist/icons/overlay/osk_button_backspace.png | Bin 0 -> 2919 bytes .../overlay/osk_button_backspace_dark.png | Bin 0 -> 2958 bytes dist/icons/overlay/osk_button_plus.png | Bin 0 -> 626 bytes dist/icons/overlay/osk_button_plus_dark.png | Bin 0 -> 676 bytes .../overlay/osk_button_plus_dark_disabled.png | Bin 0 -> 645 bytes .../overlay/osk_button_plus_disabled.png | Bin 0 -> 664 bytes dist/icons/overlay/osk_button_shift.png | Bin 0 -> 1876 bytes dist/icons/overlay/osk_button_shift_dark.png | Bin 0 -> 2003 bytes .../overlay/osk_button_shift_lock_off.png | Bin 0 -> 281 bytes .../overlay/osk_button_shift_lock_on.png | Bin 0 -> 274 bytes dist/icons/overlay/osk_button_shift_on.png | Bin 0 -> 1573 bytes .../overlay/osk_button_shift_on_dark.png | Bin 0 -> 1937 bytes dist/icons/overlay/overlay.qrc | 64 ++++++++++++++++++ 61 files changed, 64 insertions(+) create mode 100644 dist/icons/overlay/arrow_left.png create mode 100644 dist/icons/overlay/arrow_left_dark.png create mode 100644 dist/icons/overlay/arrow_right.png create mode 100644 dist/icons/overlay/arrow_right_dark.png create mode 100644 dist/icons/overlay/button_A.png create mode 100644 dist/icons/overlay/button_A_dark.png create mode 100644 dist/icons/overlay/button_B.png create mode 100644 dist/icons/overlay/button_B_dark.png create mode 100644 dist/icons/overlay/button_L.png create mode 100644 dist/icons/overlay/button_L_dark.png create mode 100644 dist/icons/overlay/button_R.png create mode 100644 dist/icons/overlay/button_R_dark.png create mode 100644 dist/icons/overlay/button_X.png create mode 100644 dist/icons/overlay/button_X_dark.png create mode 100644 dist/icons/overlay/button_Y.png create mode 100644 dist/icons/overlay/button_Y_dark.png create mode 100644 dist/icons/overlay/button_minus.png create mode 100644 dist/icons/overlay/button_minus_dark.png create mode 100644 dist/icons/overlay/button_plus.png create mode 100644 dist/icons/overlay/button_plus_dark.png create mode 100644 dist/icons/overlay/button_press_stick.png create mode 100644 dist/icons/overlay/button_press_stick_dark.png create mode 100644 dist/icons/overlay/controller_dual_joycon.png create mode 100644 dist/icons/overlay/controller_dual_joycon_dark.png create mode 100644 dist/icons/overlay/controller_handheld.png create mode 100644 dist/icons/overlay/controller_handheld_dark.png create mode 100644 dist/icons/overlay/controller_pro.png create mode 100644 dist/icons/overlay/controller_pro_dark.png create mode 100644 dist/icons/overlay/controller_single_joycon_left.png create mode 100644 dist/icons/overlay/controller_single_joycon_left_a.png create mode 100644 dist/icons/overlay/controller_single_joycon_left_a_dark.png create mode 100644 dist/icons/overlay/controller_single_joycon_left_b.png create mode 100644 dist/icons/overlay/controller_single_joycon_left_b_dark.png create mode 100644 dist/icons/overlay/controller_single_joycon_left_dark.png create mode 100644 dist/icons/overlay/controller_single_joycon_left_x.png create mode 100644 dist/icons/overlay/controller_single_joycon_left_x_dark.png create mode 100644 dist/icons/overlay/controller_single_joycon_left_y.png create mode 100644 dist/icons/overlay/controller_single_joycon_left_y_dark.png create mode 100644 dist/icons/overlay/controller_single_joycon_right.png create mode 100644 dist/icons/overlay/controller_single_joycon_right_dark.png create mode 100644 dist/icons/overlay/osk_button_B.png create mode 100644 dist/icons/overlay/osk_button_B_dark.png create mode 100644 dist/icons/overlay/osk_button_B_dark_disabled.png create mode 100644 dist/icons/overlay/osk_button_B_disabled.png create mode 100644 dist/icons/overlay/osk_button_Y.png create mode 100644 dist/icons/overlay/osk_button_Y_dark.png create mode 100644 dist/icons/overlay/osk_button_Y_dark_disabled.png create mode 100644 dist/icons/overlay/osk_button_Y_disabled.png create mode 100644 dist/icons/overlay/osk_button_backspace.png create mode 100644 dist/icons/overlay/osk_button_backspace_dark.png create mode 100644 dist/icons/overlay/osk_button_plus.png create mode 100644 dist/icons/overlay/osk_button_plus_dark.png create mode 100644 dist/icons/overlay/osk_button_plus_dark_disabled.png create mode 100644 dist/icons/overlay/osk_button_plus_disabled.png create mode 100644 dist/icons/overlay/osk_button_shift.png create mode 100644 dist/icons/overlay/osk_button_shift_dark.png create mode 100644 dist/icons/overlay/osk_button_shift_lock_off.png create mode 100644 dist/icons/overlay/osk_button_shift_lock_on.png create mode 100644 dist/icons/overlay/osk_button_shift_on.png create mode 100644 dist/icons/overlay/osk_button_shift_on_dark.png create mode 100644 dist/icons/overlay/overlay.qrc diff --git a/dist/icons/overlay/arrow_left.png b/dist/icons/overlay/arrow_left.png new file mode 100644 index 0000000000000000000000000000000000000000..a5d4fecfe603214504b78e3c27344d5414a50463 GIT binary patch literal 1490 zcmeAS@N?(olHy`uVBq!ia0y~yV2A)=4mJh`hQg@^CJYP=EX7WqAsieW95oy%9SjT% zoCO|{#S9F5M?jcysy3fA0|SF(iEBhjaDG}zd16s2LwR|*US?i)adKios$PCk`s{Z$ zQVa~N{+=$5Ar*7p-nGw>4wYg5@c#U|KrAJkjSaK$z|bz zrX%$N!J74K8W;3B{xuq1R9My|Y_?!+1IxkE zclQ_5fy)fbcnb1=>kqPYI zwYE)f4SUI*!nM0GEYo=B+D)tvB6i07yZU_FE-w*vi>Y2qmu>w2q(n*R^Q!JkiO2>$H#{ILA-qRE6ufBN}n<;L4O>ULp_h>TIFNl5C6W&E%wMHUCc5 z9b`Sk8YU;s@jjTv@R(ieqa6+w!h4#3H>K2S&OdK|v44ld2VtL{js60{F$y<0?;WfS z+_dj~VdH~_*oQ(F&zv}M;$+=%)+WY2UA+hYrF^EP#=7|wGS;il&^f(Jr=`MkQ=QrD zv-4tNV*cDdK98wC{{K3a%O?7a6_K0bJYx>b=Ty6FqAxg6=bib@oNtAqg^Ujxj-OXP zcEk0A|GG;%8n%O+(v`CJ(~gGn14krwGdu`A`78hR*I&t}PoJJG_xIn4W84Ybcig}F z`m1o^0oJPjGCn;U`zJnlH+$Z_Cq9>L*dLhAj8Z)EefI3x8xOHoMJV}Pw()mao|+|l z=W*d-)?G0mf$0xU9sSB;y(9m`m1DBv1v}Y{kJ+&(w0gT-&)#~gGi$3^H zm&kdQ)FvlCuGkv2w*2l)rHN6^lF9N42j6PRs^2(R8~mx#JZtN%#hmZ{XIdZZ+4z0p zgKtx6K{DQ-EX}jG-U?~DJ!hJ5PUN*nl^yezaNgLoX0ubrcGdX$;*1Q9NU>Wf=Y<{$ znR`CZdFxlu^xFRtL&B_Q)eQGIcZ!QD2QACo=k3C|&fk;qhw&S=5Wid7-m>~${-?C4 z{cxV!!44CK8{76Scx}aCBavnO%Jt2=HxK9h&yjGGJ>aZpwj|2s^88cNQs;^}&6!@c zRbW3u#gd?vS8VcqCz`zVeLP41#V)@I5qcdj6dD%qS~orCcHWN*@%NWMeP(~|XHC-^ z-*0zgUy9A_P&4(N?K$E5vbWzt%}+20JrR4k!+u7w-TKU^t`4~g755yKX35CP>aJb4 zPHXQH;vb5ha# zwmg%>kq^hCHe7dm`8H=GS-)Pp z{mt#C3+8;bjJ<9=!9`9&!eWuqruEy`7oDr`vflLm`t7pDHCKuRPy9WoH}QCg6W^Pr zH1}CuTYO?7iuypQim5wVYyo=W&b7E!CfVMPgmb?Z0$c z=6se9b?5t=Ef?a>Co2&kZ7Fo1Vb_U?jBMK`n)>eU?od-bb=+v?nS}=r9`sOl=aOK^ zxjoHIZBe!H%rgrgK72TH>(;Fvb%~x&8X9v`4ja5+jQ1;F{$Bb{x0->0fx*+&&t;ucLK6Uo C<;0)> literal 0 HcmV?d00001 diff --git a/dist/icons/overlay/arrow_left_dark.png b/dist/icons/overlay/arrow_left_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..f73672a591fcf7a160735609f510818cceaf9ab7 GIT binary patch literal 712 zcmeAS@N?(olHy`uVBq!ia0y~yV2A)=4mJh`hQg@^CJYP=EX7WqAsieW95oy%9SjT% zoCO|{#S9F5M?jcysy3fA0|SF(iEBhjaDG}zd16s2LwR|*US?i)adKios$PCk`s{Z$ zQVa}CuAVNAAr*7p-m&ZxaTI8M_(DoR!9sBQ1BL=NgWHUmV$%fqem^p7IQVX3b%FYx znLiCZ9GNdKQd+Luth4w2>uvMwre0ioN^7$p)3pZK2RYxSoeV9Q|7mfo;`$9uzgzC# zW<8gh|9YS54yQO**6$yMVie_<)yM2)SDb(D-W}fq29Hj~z7aG?dH-%v)9%LyYT4GL zzJI6mkd5t3Lp$G{=LaUfxf`o;=K$9`X7+d75Bq#}G>YE&eTa7j)AywX?qLP*In5Ow zvV}GN_S&H=oR`4*eO&?bsspvWGH37Ib!GkD2j(f?nJ9Ya_90#mkmhKWC&vjj} zAQH^tye_yw_09sV0{@i!fAeSS)-h~5^Cs!iG|g?_Kfl;pkSulPOoOj*o(J=W#I)&~ z+I_YjJhLyge@4;a9JAedX`kOH2Im?WYoB<%GkDp}`SzEerEQzLCAI7A->V`E&rXKqPfQjULlag{EM;t^#_)Lm9^Ya zP&DVb`>x*~EZK5r0awc0{LgKE8OPM0uvtzlov>R?(VWBfz2N)_(obBjB`2D4?sR$`{Ln`LHz2%!P9V&9{1}YW zVOelsRX~KO#s}6L4YzoGH8XcJ>|^-tb7q@#$ji9pLTg1rr_M4?Q@ix;s7e2lyA7EjF%egB*C#hVefsoJ*}X4j zXW`>xUbkxBil{fd{`#x>fa?PLod-XC`lKj6VJ-W+hj9tKP*TOeuOj>)dxpC^rlPCEb=lgcK zK516qs5`Xid+KKv<_me-7r%V@a`jxl;~A4&Q%-&V``1fTTkU35f`r2P*-FgEa~;c= zB+L}{Gsu`m6y5p$cU90UDNQA7W}ZAHYgV3jiq`Br(i7s3$o$-CGGQ)D8{?<&5R> zCm-0*6|*+%`R==UvNP_ijB^okJf~xD;OuPk@Ksk|JSR#Oba z`qL9m^lL8tcWc!GhJE^r-SisFJFo4E)|;;H@8fgCv984U2~$P)on57?qxGggPg0nF zhbQvY*Q!_bAJ0VW<30T0@;~P6e=FuJoWs}jAm_g$;~WQ^LwzQNpf)`zVY31jWMhd8Hd>JSfdHoBjLUy}j1_d-8Uq zM8{~TRZOs3*U`fx&Xu5G8KcpV{Maq9B(JdWWAWwd=fAePt-g9|wu;`9il*qb*X~)D zzdOU6KjR92Gq)wHoVebTg6@uPZKeA>-Q1S5A0!rti;3A)&nSMzX2BbC;^fqrwPCmS zZrW7DS#f$vUC53J&7AA5X6^p|=B6?Gp4=TNhjYKT`z~v|e5{p+`E~=hB^9)>SHBsY*3hhQLsh9bqJ>+Mahs^P5kro#o&}!`%e$WjJocWO zmj8aNvw?fZ1C0&ZJ03iEf6(@hMf}mA!>q^EDrDbTw|NI0Wo>VG{Kzfv%il+j9v$c0 zzBPFcLz$?!%#G$|g&Uz-Q|pd8#%!o*`n@AiWN&s*zO-Hh&m7j64Hex26N8uSI8Z5F zc<|Bed0s2)ZO-r3Rm#r`7k9nLzwJ@e(E|)oulL)Y|LyN$wpaSj;lCMoYL9kaJ+Q%a z6Zeh>3*GG=`KMcF zoNv9_si=7Q|0e&?t~vSFcTbMDnmhALs@ZFe2Vw;*F`0XxUYc51|8YCN+dP8@-|Tld zzwfni6n`L9*(LUXPnP-g+U;fH1!Jk;WPb?$SZtWTcjxRL?KKSLo4-EG zGg`Nyr#dUf@t9~@Od{)!+iT6sg*Rly1WJBCvZD#4>U-Vmvv*Ts5|4?x{rR@L@OW6m zYj%%gX&?bGF|F|Ljg?A&eAeqd<}zNlfhlJH79%qd)8Nmc1$vLUv=4m!eI(g!b^9Csqnf9#KUsI~j(suTm#^QC TU$bRkU|{fc^>bP0l+XkKOnfmn literal 0 HcmV?d00001 diff --git a/dist/icons/overlay/button_A.png b/dist/icons/overlay/button_A.png new file mode 100644 index 0000000000000000000000000000000000000000..fd90f8b42a3c5d2bbcd4a446bb71690e672a6707 GIT binary patch literal 3494 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 M)78&qol`;+0Py^Vf&c&j literal 0 HcmV?d00001 diff --git a/dist/icons/overlay/button_A_dark.png b/dist/icons/overlay/button_A_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..d6b5514fabac904da9231749555b1c2c2700d070 GIT binary patch literal 3167 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 literal 0 HcmV?d00001 diff --git a/dist/icons/overlay/button_B.png b/dist/icons/overlay/button_B.png new file mode 100644 index 0000000000000000000000000000000000000000..e8927addc4dd0ba42485d890925e4d0e157c437e GIT binary patch literal 3375 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 literal 0 HcmV?d00001 diff --git a/dist/icons/overlay/button_B_dark.png b/dist/icons/overlay/button_B_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..3acbeddcd62aeba7c749697312ce1f142fd245ee GIT binary patch literal 2975 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 ScQG(9FnGH9xvX literal 0 HcmV?d00001 diff --git a/dist/icons/overlay/button_X.png b/dist/icons/overlay/button_X.png new file mode 100644 index 0000000000000000000000000000000000000000..fe70fb6852e8bda84daace73b0b85fffb1281368 GIT binary patch literal 3968 zcmeAS@N?(olHy`uVBq!ia0y~yU?>4$4mJh`hLBle>I@7FEX7WqAsieW95oy%9SjT% zoCO|{#S9F5M?jcysy3fA0|SF(iEBhjaDG}zd16s2LwR|*US?i)adKios$PCk`s{Z$ zQVa}ye?46sLn`LHom*cJ5<2bp{_lJLWSp7RVj>{m!Fg=LlKO|fb9pWy=k0`m`>cjt7aXR*e!j)=3s0I!-~|N+83XH)?L4GqatcUeXI|c z)NJ+>?lt_6IPwP@xyI=1R=-uIOO-)x11!!B&uvL(~d(C|Ok13`xW67MRQ z{zy9XUVnY}{+&B_g!>rO<}5Iqt=noC#`K_&!Mw$KUZ8}^BHNyg^Q&y;883WOafstp ziPiI!tD<-kbQ!#NJbswMaUfsS^ViJNPqVmO+XeMkTz=W}`S)LY&N)*9ANHwx)G*Xp z8W?<-eC^t`^*wJi7O`%y89oO0&KJuHjtM%JtXfr-@9gXxyMa$-;>C=E z4-0gZ7exi1(e{|8@=|&E<;wI8e20qKZoMs2Z}`tNL89-UIzznYq$zKUcG_LOW$>iw z*4uB<3|AgqGWMK&@#VDCpS!<){dzV^sx>5F`DIJz(5q21#2xnhnc6W;tE*_*HxB>Pg>%tH+^-kj`HI6m}3&Zd3_9B z4MIb&#y*ymtZK88>5~@dd!WJ`Z$UMuN2$)I@IHtTik*6IDI zu?tjuQfV{K+}%23#)6eASAGm$=94L6DQr^xE$7YM-QpWJZjAZn(sJmK)0OMjySW&@ z->F4JiT^IYf z&+7gm3FpX2$)uzm>wg_rQB-8izP|41HhC$jL(k66cJ1P1FlJ|C`xs7Ai#EFI4 z4a<8z)IK=CxZ`eK-tQ?>L~Qc*B#;d67X#Y5`nSgZ|ueQ(~pdD{EDTjwuq^zWOYzt-@A)tuDSR7G`l^%eWV)<(5n zxe}81`*1t|;ipBcSEsWrN{y_0_DZ}p?rBdNR+au7)$-v9YYf;cpe=#O0Y0TwPf^euynHKDMFo@v%a6^`zwFz?hgbvhMZu_HlJTQ}e0=1uiBn zzr6For~j>-UM{O$e_ry6jg`Gu`#lyMkx!mK@6Y^vBf0_7G9OxZM|=AZ(qE7_w3tsUte9_@%7ro`CHG=Hs@a*w)WAt(D?ZEFPT-P zj@>wQdOx>d$-_gfn+2au<@o#e?}{~Ro@|?*DtX@S_Z!pj6O&XHu35tq7+BMp7q?zq zfA1He?-TL`BpG>+aJ>5b^Pgvn{N5|qu01-^DSYRf*=$~Z+mfdTHr4zry79IwPyXuF zs~P$E&$pR#g@*2D?~pE1R`brey)AdvJ7aDxE`#iAIt&g$K|=E?9&x_gmVR!|LFxQG z3oYH06&B?9+S?0lI8AlV^byRYzH8UQQqt0r&dKbvd1zVu>_FC5C3ExXyZgW2 zuaCdH#EoNyRqd}Y@h|56+I%y|TQYg8l9G^TQIXNA)vF5+^FDf=#bVql!KLE4%lZ2R z=@n;03rot%lAfQN+xBDKs#RSUhKvW^f7hR}rHSdlLg)4t`>#LVY(8J`=SSf}JJ*8} zm-RISmv(X-g=mz~j$ zXX&zKY-i8TnzUox!nJF8e}8{3E;2Fkj)qgfKI!cSkG}u@_^4Z-ZLe8aXlTy8J&{jt zMJ?W8$HLk$ZJOAguh*g-x7rpx@z9tyrNg0PfuQ}9xMfRSt8KaFpKre&Uw^kDgU7LB zfpAkx3(K15?S93(SFF&uQ+~ge%_W6LiG}wjla#H9xVW;W=8iclO|!2h+_SmZx{>!` zwqR48?t%7&I+2@L%J0{fUwE6Gl(eDnvD>?=t%)h0xTVC!+b>-Tx?7iHCLOje#&Ta` zg`iS~3;#}6SJxSKwYvo3Sy@@{_!k^%5pdk8UD7hkwOcH+O2tfWhrH`;R)vUxBWUL zao<-57vIUBcuKA2Mr=%C-EJ$maPi{i+xL<-F4(dqeMpUN2oH^*Li+;87EJAdEI+xvF! zmbNZ?b6|RW-Ac=;3pZ>CxFNcQ;g*Q&&)pMFn=1X<%VA%2^2`~RxVU>i+77Q+v*ypY zGk+d5^A~)2;`we{VPWAct_+DYoDO$(-Pyn`B`e$e=~I!f^V^S)j|WCqKmD~$H+tKH z+4=h(ek(jKTb|){)tu|tjrWX&+RyDp9=h|d4qt!GPe*V|XET9qAVU;l5v?4>+Y_7qHW}9rSe7p7f4t2q-tgH`5#p4Tp`+0aA`1SR*=~3kdd%pWW z1HR~N-qyc`PsP)zvU2CLP5E7m9$Z-HeCM0pe11_;(OF)*Q`6Ixt*w`wtO(!p+==yT z)!yHXAL2j1PB_{ndgrKqgLmooEnUnEy}i8_6(1h_XgjQ-sd@0qO2wPI-qzRJ-51rq z-@+wzW7&^zrP{tDXJ#4)1_Ufvd3K&{b;#3l=nNK%eL=G#da&@k7ta}E7;k^h3`+^uzTOW zf3x=RESB3F{@{|?QKyYjYiCVQ+V}U{?OCTb_8rw;zsKlR$`Ox(Yqc>c zKQxsHGTgj+^=flVOH0lJCs;a9^L&d#) zeOK3sSAfaCE42M%yAmNm(_{&U%~W$SazZg1`}ysH_#Nk`e-T-?|9&*h_f)4e}S zpFh1V_x6M-Q;z&idH(2Vcf__F$$QoBZSU3!2L@(mzYbq4%YSsAuDsDPrA2)gRUXVZ z{`lv?2)4HM?kPQgB@Wy_W@;kYe{aQ#6@~fxc*USX7RW~;hgZryQJg&HIHe z&aFDaa!5ymVMok5vtu`6um99NE5B&*;{A&?yjI7#CyI+OfST7=v;OKe{AW=p*?g9x zogqXpuq-xK_Wv}oWPwP|K875#+n-*i+<(XIvc)ZM%63;P-ji46i|yLId-mF}*Rv<7 z++;k%Uv9%z$a7$C*P;*0cNGd1roSj)N`GC(P<&Xnn?oY`c}S>kxX95Brs0C+ZG6jj z@7`@`XZP>=CY@-B%s+A$d~-Q5b3+qf`olx5nX6XWIl1sNp5Q3_&g)VV z5PEf10PpH#LroUhzRNG?IJ%q;2|b(G^VH$bT7UM2sAD(UUA{Obbow4Kc*(L?YWCS_ zDyNI21edN}wX4B!TAR0Z4$4mJh`hLBle>I@7FEX7WqAsieW95oy%9SjT% zoCO|{#S9F5M?jcysy3fA0|SF(iEBhjaDG}zd16s2LwR|*US?i)adKios$PCk`s{Z$ zQVa~d?w&4=Ar*7p&aEy8y(-$if2sL`BW&Fb4F{Vv76@)#%j2}Q!)c?dOLnWmTBd1I z^Q7MR{0_ICTNdRuvBJ4xv7^hyu%qUxM?VTKWPP<*cS`%yKq*ds#iC>{C!z5F7UEK$ z?>xU(`_7W{`RtvOt)H*k{O#P|yPt1WuidJsf&xlA?#q6V3f;@_g8d8ouVeSinJ+zF zzy8>NrVxhIkNZU(G&gAfyT$lMWa8qLZ}vyunzLZlE#4iB5_~-jRnd8iL@M|r_}2(e ztdIUSUnJKlAat+H4)z_wJ9Lhoea|$HeV))nS#dOA6|&X* zgN4DF_xmc2%sx;XXY+~0FCbKwS>%z;hwZ6-%)u^C_|4|dOT8Rvv?6QPs&kCjE_^%6 z@`A%=`MIrI=jtwRv~UXDYW;!hVC6g?)7cG%N<|ux?)$i<_Q$PL{;F)_n5=IQkCt z9D=ZTQ7?V2z}dJoKogpdV6h;@`0~Pn@*qg(-aK}-7OSne0Kx; zk7*q;CU+H6uLp$Y^6&A}SSGE<7+3Up&a6|j7r9#$&o`c&DDc5L`00)USsAPgXPEua+OS1c z*}p#^bZ&!ia&qfp_x_?cHw=@1Z`oo}_4QTi>Pm)?sm<)Y`BscCE-mGr=y4=We3R>w zoy+XL_ym-uJ>Q;OsIuaVgQn&8=K-OisVVwOHa6cA*?1%pR&%eZP1e15e9^IQk&%&S z=9-HOX2*YZT(G}GCjX7IzW>hs2S06y`!08PXZfSO$G)xdGST-cE!{fxTk=MU*4EZz z-zIvru*=mPST^GU&xt>qR`S~lhJ@-fFROg`yYKz|gQDRvi8=8>L5DuqSKRZueR`Un za+yt9&U{lgHcs94+R)hU(KUwkeHZ34r&m9Wa5+({F@4JxlgG95Dt}3Od3jCB zD!+X#cg_FHRms24obfrsFMQh>pR%c$X7%=X&ohyT&9amAq(G-P(U{`h6GXGtT>`{$S3zJ9iHL z`aR;YKhO5qx5as~uGgkUa&CRvWRg(!w>OM6^s3&0l-YK(m4rGc z{0`7dpPTwmUngqIiL0gp4^B)}w%PkCGx@jQTr0!EOD^HLDhEYMH@)^t{>>rFKPUTU z`s6RKulL`6zvJ!=yE2<&U57;1-HO;!AgJy)$6(uh!)&(abIa$gHkBYtHRmXHS%UUUoL;&W4LOzJE23IX^+sx#z9!$EEM@%v$hj z$(N-yW=m^-e|z%mnUwwOm0FdbKUq|MO4(c%8XBq-wd2GN`Ij$W`pmVudgGo_^A^8% zJ!h4_$aU|FDZF|$yzq2+zTq}~P0fcp(`U=rGACROUag_4Yx?cyV@>;KP3Oe73N2s(AEKz32O0jn7K&q^hUCwyQc~utEKW z@3cQEzHY~Pmi=OLpCRGoBh{LJKcBn(?&L5|Kl`Ps;ykZ;Bdg<8&A5jr zr#LO*kvE)^%|6TCG56-C);}5hRxdpGzUL3$^_XH`roEqg?yb6|`BHo5syc4Q@BJkg89A?g%8dhE z3mWa8&RgU^-%VM5wyIF)jxN@3lG;t%xRN$Xcu(IlL+Vn)tXWdm{ua(-8750 z?$?<^tlXN~+TI!Wr~0o;`M>{r^t*}bQ*H}vxzoh&e{)Y}@gc=I4~~32H`}~lf8UQo zam)8Ce9Ijb80c7MVxMUbish%%aexZ?wNG%>UDMSOfJ z7{*dMU1z3~-LDtQU0q#key(1%>d{4aIbVyVN~|)Q6kqsp+pP(XjPx|o&-#1%{kb**-B5G0=zNX($#Z6!FRrbPu6%p-de2+emb|-Xo=a$H zYtNL=l`xcGSi|w^_ao!dmFw22{WmOM*}OPU_Gtd|KhyYRtt8%mKFBW5Qh4Cu<0Y)3 z6O$bhrgc{p9i9Bs>b~ z>G`?2dwv}1UiWRLUGlG&%Ed=I1lKj4*3#075xwwXhWWubiRJH`u3fn@;d{os)feYT zx8FVSK(TrMmfUM=BIi`Et8{xZ?QC?+(pS|dq7(cB?*CtW_57N&`}=B-eUp?mJ~4CF z<%H_%+`D)A-H6>((YgAUr`&=YA|Wn(4<6rrch9yu`i$CY|8rMl&e`w(`0Moc-koPd z3nD8kEic-{`|rK&xE} zz6>fSU+%IsV~tI`INea+=0DT!vYFzwcF*S&&#>G4RA=j%Gd`P+r*F>)pPsVp#iS=Q zgY(yLO6f1%dvuz;u$|`5meAFArV7oexu5dbv1Q}qW0!sleP(S+-YCJ%Ew<}bKkGjo zrRDnN+myxTZF;lb({K5;aMSrw@zeiJ|DOIOcX97q<%t|ScfQkFD_Q-fn*Fx0$PJ zg`Vs`@cwn)2Ca;mnU9{D)tVi9=-jSZb7)V8(t*3e@BDVWyLZG>_~pI3HV3TAY@V%p z;3lLaI&pESd4-_LH-m&aIiH3MlUG`+*QB4H=c)Q^TmJofp}Xu4Sm*CQ-1gl}<<2t3 z&;xgVNw4MBQc^wOb-T_#aLc-m@Be@8oBeNx-nSbP*ZKBryXijnn1+VNf<1RObIf%T z{JG?x+xkNm&l#Tax3x`?)h?FpvIM#M)J8M+b6b|ND@!eY6Cv3xuDk70neD2=b1l3d zTml4d@`~K}799|fFvp1BuW!X!g_k`%WJ^KzO`vFU}pLuU`?r(Ia!xbB*>drZ zj$r1dB^g@RawGj6UP)|a>~dLhGu~B;4!Kab{C4Z*rx1imiIP`Zwju|x$xA5$w9Mk#@Tgl5%Z^{7j&PHOHi3G z>F3fgFCK}fVLl%m0~iE!I z9W_%{FyGIfvob4RCXhKgNze5Sqg&UB{gVnGRf|sfQTpii0k!?VA1O;WzMdp1%%q^< z@Mqhfst4f?543|LTpDWSiEX+XzBiKvUY)?_rn%&v8Rjm6uwe0v z#UWd#uW}3h+Oq!Aj>TS~;j1>QReB_^c-0DOackF1T~(U%umK{i7%8>x3ru2j3}UzSXjz zH1O)W0|5yYe@Z)_s(BSI`+to&`>~=~^JD~V_;xl@O1TaS?83{1OTtU B)&Bqh literal 0 HcmV?d00001 diff --git a/dist/icons/overlay/button_Y.png b/dist/icons/overlay/button_Y.png new file mode 100644 index 0000000000000000000000000000000000000000..ca0de569df161c6492d7e42ed06f291b13a75322 GIT binary patch literal 3337 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 literal 0 HcmV?d00001 diff --git a/dist/icons/overlay/button_Y_dark.png b/dist/icons/overlay/button_Y_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..0f3e4df25f8d8861e85fec40d0692a9b31ed9c08 GIT binary patch literal 2883 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 bTgiXr`Wg|#g}sgp3=9mOu6{1-oD!M<>Z+|( literal 0 HcmV?d00001 diff --git a/dist/icons/overlay/button_plus.png b/dist/icons/overlay/button_plus.png new file mode 100644 index 0000000000000000000000000000000000000000..4d8090d7dbecbb5ec5be0e945c29ce302efac58c GIT binary patch literal 2497 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 UMIu)i7#J8lUHx3vIVCg!02a;X_y7O^ literal 0 HcmV?d00001 diff --git a/dist/icons/overlay/button_press_stick.png b/dist/icons/overlay/button_press_stick.png new file mode 100644 index 0000000000000000000000000000000000000000..6d0254d50ee535c54ae1f5576d1fbb53d3f4b488 GIT binary patch literal 5225 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�i;a^e-!rf=0p9)GMT zE-vOSn$K;jF2PfFG|BKftM>1LX}S!34yjF@p_Ako(mgfvKL{D{JPu+If2x_jG;>zu z{EGDv>fws#i*$|S)9aOE4SkDMjpbSRk3Z7acyIVw@#ROcxi5CrnHcZptGnbOF!8m* ziBLtal^P8?mp3xY3;w;=*ZZg|C@Wb(V8-z?+5wH1Gf$n7N}IQO%CC6+#sG~i@(t`0 z)K|$0-c4Drn|lBADUauz9Ykr86AA)zMFNYPgs`O{njsU1!J#!S2@F+ zME7qWT`q62+}2nSH1E^B)U>m?Oq@?Hq`14giwlYh<(K)V)m_uw#wki?DUVXZe5zBL*9drC++Z_!cm?_gLxEHD+ za_ju2HM|-I7KX<6uUxrO;wkt%W5Tk`cA-G+hGfQ?od;?TKXqp4H~K8K^oo^;#fOL; z1&Mdv{{A4p=j zuds-D!d~Xg6(x)ntuy0Q7A;>oMSt;$UXNMJwhCOdW%zbpJIC4OzOnD-1uNFNy0%;{ zt+i|@?mBVl>>2wYue&{p?+#?;=6-cu95}awM`;#Y!mQND%N;8unQ!lu{K~NItqyy3W~TAwKDk{Yu6Ei=zZTzrA3v+^O0eV4EEe=EAP4%yW`a$-U2eXO{3g zvFv&0i)Wsb)od;2Og`(?Xj^2(cHldsj)Yqvzqy&&yr_%L2QPhoz3X2x!{O37zabh6!RYyYO7Oet!=|NcHFLv3xXaE;ylBQ8pYaU6yhH?3Le z)bWzxTk5ly49^yPSvKqY%%3JpuhdUpv|*ClvU%3!@8tge{ky;QyJp_vUi&ZKzt2x} zm(k@+zKDEB{@yFO6qThh)+sEv9N;G;h^rFm_KdVphngW&INw~ud{*VQOE zGy2TWi4SU(q7I)a_taEYzSto6J|{Xly7ukcx9vusPfA$u)fBbP7GVBhAh_1d_cLox z!Gdjq;T^)(5B@8q*-0=PXQnfpe7H&UK(@;kxdTy`P2$}}T~DvJYPH~Ob&B^?n{3=7 zU_7Ie&G@{4)0?Abb|)sx+stk^zxF-L2TsA-V`p|V{F&QfrF;67$|7C6`T3`p`Od!6 fw`sN1Klu$?e;72+{kNQffq}u()z4*}Q$iB}A(6KO literal 0 HcmV?d00001 diff --git a/dist/icons/overlay/button_press_stick_dark.png b/dist/icons/overlay/button_press_stick_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..757d0ab292972636084bb75a461820f0d632f4d9 GIT binary patch literal 3636 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 literal 0 HcmV?d00001 diff --git a/dist/icons/overlay/controller_dual_joycon.png b/dist/icons/overlay/controller_dual_joycon.png new file mode 100644 index 0000000000000000000000000000000000000000..8e8b5ad417f9498bf2d3a158fb960671bdaec021 GIT binary patch literal 7312 zcmeAS@N?(olHy`uVBq!ia0y~yV3Yu14mJh`hW5UfJq!#CEX7WqAsieW95oy%9SjT% zoCO|{#S9F5M?jcysy3fA0|SF(iEBhjaDG}zd16s2LwR|*US?i)adKios$PCk`s{Z$ zQVa|-yq+$OAr*7p-mRV!n_8*=;r{1$Gd&M5G&!;~C7eBJ5c&SrWD6-zK?zPln;4!4 z9!pP6+~%-x-mlE7#THw#y0?nyXRr#ouH;di%n_X@<5eaH z>IqD|r@&D+FZ56B&wcx2Lf@a)_*2HPKji9s<{j2ui+(fi=qXyrc70mfhIjAYxh@TQ z$;9xU@xw2Zlt9^_(ixXy9Mc5gwe`L1(?vj_6nm3>8d=;(4dMv7X*48o)mv;;sR5~T5 ztgK|PGqbb%7eDWL?Xk~$nm74%yb4~!sy6?;{)TIH><8vD;Av9;^U*Gk?PK07e8Z4 zUTeE3LgydPf#r;Ef~&o*9OyGVW|Oh@FMG<58;n=`XVr16*_zH6!TsPz#ifO67uX(5 z-uM3g-Me>dE3Tak*tSZ~;My&I<{hjI;y*IXS5+mg45>Qu{qNJr=T0|Q?l@2}?|S0e zzh({kjoasDzK>hI>ids<^?BDi1I)Cv4CXyQD?D*G!<^{>c2{bit$6kI+O7W7xg1N^ zzhT;3^Yc^fvU%B+)lEMGij5fTve)i4dF`SRn<^@C{AevoGt)t+;5dsd*o5)+@hu(XccYlMwvL z)$mjEnbX?m63J(dZ}w}NwD~yG7_h`}XbYW~~o?Tc%}jYzBYgCRwGxTlWl%jEc&xc?Rr>(=@QPwf(*7Rr@BNwy1mm znXf0!I;G8;mXq`5FT)Mhd7G}h{r#^CWbU4zgpjZPn|xY07(%A5b(rNL&7c#tCBv>^ zd+HqS)aT(0dEyfjpWj(w_$(kaw((?c?d{-mPfN~!wpyei zR_f)jez=|}Q769py?w1~dHMH6ot>SlT~*GWo)*q*A$TBG^VpIW2?l|Q#+&_Gg1&Dt zd^W>?XZ>-W*|+(5d2=m(JZSc3{4m!CLa;k$`3cEPg7Fb&gNKlmZb~`Hn0$GJ>LBWSg26N^emh;a?Kfj~S@I`f2Vt2v0znWS)2K(NBUy(KIb!pD^TP!>c zmYl}}Lig(M9In}UG&Q`{Ns;I92isLudJVaT `Jo!8Cx$eq@j=-zuRi|B;ch=WZ zPW3>-rqslsy7jBxcP@G{nPJPdlZ!NVd4~2ge+YTIa_g2k9e1u>y;_=K$sV1p@BV9d z)usa`jLlE3nax?X&%A3<^rJZ@U5ge5tt??`NPjeEJL_J>>8EX{oPVBPb3WtcrKRk@ z_th&m^e>xno$rp`a!!?RO^Z8Xq}m?N^*j@+BUV54{PXjr=l-&!zFY5GU9zM2-STe=~yEnFK^F&pic6+2d~aVrp2r5xekB$r1I?E!T^mw?OVTjG06KF z`7uYbKL``lw0gdcF+xE}J*nvT%)RS2cb+I@=o4P~D#eVUV0K4N!MVK}O75#(aS8sM z{~(!7SHF|#F+F^xBZE*Net%$4i?p?cr^1nKK zRsF`acvXCN^|`%*6W1rU#jjIazN*SkfaUcm%Wp3h_y03!*mQrvz3xe?=4~x|d&_sT z?2Y^P<7Z`k{ouh%`qvRbjrTTUQjI#BTbQ(*(_vm6ZPetog_LR3>NbU-2WKZ`4UQT%9|2?$eqXGbT=)xH{zPTEint z*+j}4Ez7dzzc!I%SiVeVv)+nViRX64|JwI|-ORp?*W{Ac?&bXWF8ho7+qZA|qq6TC zUS|ARQ`h+Z_iDx%k>r>xgTno9X3REDKlhU1pYF5D^pv%>Y-{iC|C$iG_imIm$AhrY z-X24J%jbN?$L6e9m99Lq_bVU!hve+HQVgp@OXqgvh)+B`w^(~_aXQn9tc&wsxxC2_ zwtlYn^6lHbbqNUxO7C`lcX6>>7gfA@Pud0M1@0~jR^9lRK9h}c4V!_E((TZ%20n3q zd$~7@cRpp9_CDk524yqB2@Z{d6J2MU=iA92*jjjgulD`#{o$eSl4JHR5B+*bqwC#` z!{7JKJpWwW;`zEGPuB6dh}Py>sQHY21JR{#8x(5$oN*@}TN1 zgJ>7K2fzQF{aN!bwD8RJ>({4$wt3IZU?+RLMt994z2a7cVGAWtL(n^s7YZ(+T&`56{}oXwyrxOmiXNz^t{2bIf-j)Sq{uSw^M%W z)~&6Vt9x`}b`+euv$NQman71m%b9;97YMJjkUamHuV>MVn_ldzUe(w=cADz3^?=W3 zHHLhnV>Op7*|XmkGsH1$_=lvlnz(A-diUP{V76J+=G) ze!FeXb2xnOk4N3VJ5F#a73nsFeA}kB=t;^~v#-hLb{e<0x69A{r*EEnYm4lGy$tWZ z*8cYj-79-QK6R3!;8mS&;~C3Czy65rKE19({4qZ)|wjD=aL$yz9hE zhCXI%g`n0;_ih%(nm+C^e0}qnq1pMa6Y0;-&i?+?H2c~fo6C-%#LFPPY~Q{cSFe^X zF?<#z_>=p}^~by+$1mA6ORi0uv+CTBZU@ny$L3fT+gVvy{HR&V+GK1cy+W)T^ z_N`qd`qt2It>#sKrRR5=wA$qavz6ASJB8Ydv^`|KDl+|LlY+#X7cU~%7OXlQo29!_ zOhV9d<*9|*$-YarP6_$CMx$$=(Djz)=4SoU($cRhU-cHA^*pQjHv46;?fK}4lMOX? z`xy>Maz4GxrPsOW#kn2obE6hp%1`EZ6M7qXi*;G7%er^3Uu%~)S3jP!o9%#|r`pvc z`^4K4ld8Sk15Ku#etM4e!<{wk!QQ7HlMqFFTWf0+mvDXvMJ%4 zZ|3#gAM-!h7Uh`WW8i%W#c`xZ!Ghjy{&p)x#;w;P&;RZ+eD-aI{Hdt?p33lr0TKKRfg+3*PuCq|W{^Aa{ByYV^E~zs$0Qz`O}g=3 z>g=)rjWyqG@|!z!ysXWxuFpE3FpsU^kX+YkzeO6iUd*wUJ8(?v@taqM4C-$*^o%t{ zxDr=}Sn)YTW(z1?3VE-%B1=uby^AA1^0MD5T?6syw~G|J7Nv)NWez=dvvbwINQpN8 z>BTu4BWk={^t6iaE!*W{bj)U|Ym8v$>*khOE~cz%9EUe7I(}ra#x8%Cx-i!@FW+@9 zTCvEoN#TcL!$GOXW^*b(KT8ctIPg3%LtIo`LU3i4Vg9PWsk*DAyq<5<*Iyr*8bVD{oRrjNr`Dn8|Aoo_O)%#cLx@ogxRZ!j*NQ#hoT$z6yex z&!?5XDGV*Ww&UzJw#BPviby=aBfY7^`|IjiOu?bASwd|$%_`h_Omt$@V>21Slj(xj zB|cBnc@*?_)v^O2x2+Z~o5rm)$@7lel2yx>%U+JE?pU?yzy-fmng;5X^RCC9Y&duB z++l_}v94=g-h2AH&b3;lw7guO>BG63ec78#B|1)6S5;Mg^EFE0i`R7AF}b}w#oyn5_xs=1eQI|defT74W5wM=t=!Mwyj`%k&noNPG~MWZ z%n#lg9?i8-`gHQ#xpRLnbv*t3ubOd>W^crmh%C#cw%H{mUlQ+s4}be`&7mLr>My-` z@#62QDo0(;cLytrpP#Ee@Oe-02Je5hwY8SnZ!FSo0dEdNEXV@m3hq{9rD#t@Fvd#1)ps(E1rbC)KHtxWGiqW?z~^wN1lduh8#u% z`2(9)+G{H*X*VnU$Xxn((xmgx!!4daI*~5pkT22p{kXw(xu5&~e`T?AVPHsp1b*u#GyKJi=F%=N6+_ul$iH(Z_f zTvpmO@XdS9$ZY#bA%zQ{O}M)1|F0Om>#{5d@B8TMF$lO`43KD(XMWJ_F(t6>?31lZ zMh|}PlYhQs-}`u`2gQZwJkP3sWl~o1e^#_p)Y;j&y0*oh=Wu*~fB*c3lYb{oKDTmZ zUhw;a4_8&q-}>$ER=+EkFBdm#{iYWBm!~Yh+-u9^E8IP+=GmK=d~vZnzAk!uUU$cz zN-oQ-hxAX?G3-lT8~gb8KhCLq-)z<&{{ENS-QE2<^N-0tE-y}-@-6Kde_P^@$zE(M zjvtD%-|l7T^Ve7&R@}3SFCzQB*u?1L$Bw;=*NxCmZnx${|;Zf)s^9OXz6Cdw3yKEfmfAXRo>6Bws`HbvT$is zg3FWet3Ac%{(7nGTs=c9qWPvY;|7TXNrH{9p2WLOs=t55>QY?u$`!Z&n}lx7(g^Qf zb5LNZ-c_c>tJbein`78^{htf3##`eV@-J84W7caxpZEY*Kt|xR%ns{_IQ@-%T_fJ3n41fD} z!PC>z-y0hl6){Kcc*+#G`1GsTCB}1$o98{Bd-w9?%XWXIK?dx5zy8*?+}np4&Yje- zSL$-S#ed89x~9_hp1!`kueJXLL+>tH#opV~qjNOnG`mt(L8RCW*|@c<)R}JD-Z|x_$?fy`rk2utkabsQo8{ih zjeiGf;S}$@vz+0|ox6P*W;zCEQx_XP+v2ovL)|jwE>O*SZu7N&EC`CyTeDk?IEvHQQ#%d!0hD4Od+LD6TP;paW*}OUiEA7k|L#5uf0_SD=%4= zZ!}E1ruOjM&Ywy}q7AK6k7a(DE*n}J93FoCtD``O%{7V7nxSh;{IwTtx>DL0cvasm zu*7}m@#=ZsH3ff`O|rbIEcmmozP^6ttc{KWHV=OPo4l$j;!dUD)!(X@iaF)^7krEt z6_~hCs!hAWDQ2gDZ}@xGwo}2OiFX}Cw;!3xaj`_lz}F!(KKQZ7omnkH2bnI*?8=tN zW;n7d-}EA%j=|Z)(ER-TQ0{L(YyL$rv>%(pFUTM^BdzerG~MWbYi2c`+v$H~E)Phh zuT!Xf_{%=tYq$RMK3LKcq?FW{sr0-11b@wa{cPK#-~VrzIy*n~YIeYL{-~hGt6$3-gxXd=Jk)ys z){er*FLI85`}*~1*HNW%MTTc9U!}f%`*wQOyz+j-w7skHSU*?^R<2&9ch<@=OSUvN zfnm)Y>+*N+*KFNC_w9kCjTI6V<|(^&Dt$Wl^1b9rvoFl2uQa^cdw^jwf1~Ozg^0!b zwH!j$zpCPG>yVPh()^TQNRCu@+`=AO`eN~~B!)dRN`+jiH|V}*Sy9IJN8a=6%hol* z3vNj=L~r&}%0Bx^FUVL>uO|1?G}3a7Uu+w)YqH*8nV)R z40Q}b%6}ZZF5|N@WL2vF++_z$XZ%uDIvn}jVXN`s!-t($6voeWJ8n8-m$6m;y*=%l z{F<`T87u^Ux?bxP@ZFqtc9yAr`dpT)=NKA_^AsN27#;Oqvg`BbHNp!j-e?$XPb}(x zH0_A@l3$Bf{hwFLC&JIna5U+oAp?KP9Bx5|KUQ5z>-L*;zRVB4KP6JkB{<~k?#Sm3 zA1jw0kVwvBGsyn_FYSowj9m+lNz`31ofyvLvoN57?Le>Ovq_Tc4qjmo-K)*PWOwkk zOIemhYR^t4-90zFXa26a#u>1uOv|9vNl|2?eEL+Mu!_zQ@wP=8yVPH+XvFQ_dPZle z*L0==$tRN}I{F1Pwh-;%Qm{>4?YcAV>;FAv zkn=H|ex`s|O?uU?mrtk1KfCEIJbU8-9v&W_juZcx4s4Iou)Syl5X*j10Z40zZ#`87>4eSNMkQKC-rfO*gfRpUJ_UU&Dd`c)pO?s!r=`*lR< zU+vJ;V6Cph`NxhOJJ+|U!@6r*!zL~t5!d8%JBt}?rWM%De4rYXwT;Q5^l%%ma-_P` zOO4eB7#u*2r_lcJtG=AiCUeFqEqzoOYHP`JIO$qvK-DQd15oXi&$x!=gNxD2fXP+& zEX#{TTy;O*VE*gq%F4#DNaNOw(@*_egx_+U^j_kU8+NsvhoAqy(Q6ltT2XNc1_>#t zTLy-P8+i^m?0K2eJl&&{v)3^H=}hm}t5zLgIIyeq^|lqSPOkd3R^szQHASWJvonpK z=e9aMGzX2hXRc2DaAobW&$ajeFI-i}d!X^-5?9_F>xO=I#{$-G8+HgJX&D@NBD+k1!!PJ6H=~W{^*xynl=9EcWBzdQ z=jZ49ExQ&?yZ?I5KZR{f#;%Jsc9or*W6ACMM2O-1ahrOj9Veo<8ZyL-O*C&y{L$Lj z*x0#7c+sz*%Ti~2*x1?QnF@YhTI#)i_KX=jB0}{UHYjvP6h4sjIXW-!YW?Y_Md6}c zt;aX}wY>^gVohrYHLQ&rPFGb||F29*c~Zh4&KNPVB*v@d@DuN@czIdby}6H%_3G|y z7YkLJ8vkWw@bjw-Vb4GR44>+?RG4v&n$mJ63!7e!PVe+&$vT-LbRs7 zdZp_3{EnK>Hn>zFO$M=^zC)Q4ORmf>lXi``q;NqabC?lq5ut1~d1;gUU zCuRn^vAGA@=ruGccdXd4fuo4wri|mwz_MmHL8e6lH`_LJA8VL&|If#Jd~+|n+_yb1 zbno|Te{0XZT-?5H)$@fr|CU^he*b$#!LYuE2qdi1j^=~lzZpad1x4Wb+L z4SK(Hn@mxjK5e`Ek*|`E^<6HR9q^rb=sa^C>pJ#zEO}iWKW7$gvyfk6S%Sr24(ir6z zZ|~~v%`JW4yu^A|MJb6^YC>3Iiu9yldv*&01dZq4PW5uBm>vchxq z6TcHx(WUZ*{$^G$O@m2l9#&F2#4T3zWJkqE3jW~wQ0y7~#P3*@`*l{gOHl_*4?J&B z-F@!FubUzd%sl5$n^JtM@oUFAJC;eynD;Yn(^r?xZse;vKbm`Np4s4yocV=$K(~E(P6Swdbuoe`U zzKmerD`eq)Y2r@djlZ_WCaCCo&bq!Sx%6d(GcV)u?e|P>F=Vbj;4zD3PJefI>GR@! zlO=T?i_N%p>iK)--Qn~5cpDz8$WF87T=1i^+ zC+~Phl)C8}O!~(BZH1+{w9kLd%*S^;BTVDM5>#}#=S)!)m-d<5a5*Ypdu^iI?&hEQ z2}O51Z#dkRtH^(+vZjVxbVgYECjQh@N7-(ttN5Oo+cGu#qV){^$-Ab^yqar!$;|AU z0ykIBlBFK3$8@Iu{}kb~xo1zaB}c*Ziqo0J3D56%MpU|q%_vVjedvv99_u9qVMdj; zKDVTAv0Z-O(CzuV#NPi_PHy_8ZTr@*u$*qI>f6;Jp4FEvDj~P^e)W<6+Yf%^`5^e< z_u@D9wMp5l?+35^zA=?GX!lv|te#9!$vT%?>e*t`4ksU&(qWNZIQzEyk-Z1<4$Rz| z_H4Unui=$jI;VFt@rW`0c0XeKSXM%?o}G`iF0e98|K{3b5n&4z6O3)Xr(TGW5H#OX zU65_FjrkO^C=(i1tsgOJ|VCn@SVz^R;`k`=lvk!|qDsl?hJ)`D*F?+~x z$wqd4pyv76jjOrlg-;K^E^%7R$Ys`zkala&^={=o9r^vCF$_wNZgyDcD=nLRuVik# z_q67RwsYqxy!5?d9rwI!Ew6#gty`PDnEF`Fx;va)m6YD~bo^~(ZhU^qC(l7v#0iV z5%vXk zXJS?3*T?QZBVxWhnrq(mx3X!+3!^9Nl`RLh}po5=xMY`;0xx}}i^!Qf0(6YwJO;28L%@$BE5zW=y8MppNr=#d|b-XxQE@{_{? ztyypEYT0qE{gj@;ZLPP(v9qtWSA4fh{x-q1AzF2BaUr`$l<%dTQ+BVvnB8z&<*kw1 zos$P#4?I*ow$(29Th=Si1M?1)d+&P}vCQDc-{^2gy=f-RTW0;*ejq03Px$cQyydG$W9?RIl-ndpgIEJC4qrKtf zft6cRQ+tC#8zW^cc7N^Kq@~Un9a8aEZ8rZKX~E6Mtv77#d!c-wcWQcSZ*Z#p?bycr zOvAa?b}7Ehc+i|0U*0h1cH-&QO;NTt8dmN~{Pw%yvx;u)v7=LFsntsUh>^T5oseEu z<>Ye5N$P-kcC@VQ&+p2){TIKjxqQ*%mc$OWAN@g_q;g!POL&u#^cGytoe8olcOT%nz3FQyPs5Vsp0=^mvTJuP2+7ud z(8cszH1}!(^Q!PN*O-Z060djd*m33I13&+a-i3R&HN~tsShe&(#9qD$CQI)&M}MAq zD&uPc^D)*m171e)Etg*Nsm!!CKF(od&g_^ufMEWP8kprXt=?QTY*) zI|45GTw?oHJ~_(PM=dvDzxoCjP~o!kfW(1kYxOq={S-WTM(Z(Ca@y3wM#Iggb{v=y znHrODW{r`aL0XukxPLmm}H%K=~tIXwz;n}0+T$dKsBvWqC&v;uu zDn?9Ha6jWVX1R5$+Q!E`K)8WmbM9_<&jTd2*WQ%H0S4XesHvebUnLgR{Wf z_2&)mXH5Iq>sW2Pq?C`hayAOQn_z@K;-qW%3fIZXm zDW$&I%XV>}c{j=I#S<3K$&GupPNfUThzd@hT6^)D#9759ofFd*uiAZJ50k~#uWd;& zYjeH*m30+2ocpVwq;)&kN-2(kk7b_R`v_?t%X@4-%=?rxFGa9>RdpT-$u1gR!^uMI} z!|#~b#kcxT?l!z;t~#<}|F)5-@amtJ3*Sh$^W z-@Q|-g$@5H);@hM__5-wq5ewCd0&}D%l9uaH20alrE!V%?heCQk(11%)ZZ*$!7EGr`To$)sFchR2BD{ijc zk*p`%(P8bGoqkKUIpbEl<@HAa`!{WC711@2f3x7}q2L3*xsF9%`K|nDquFCSE}47J zN@suT_>!?~N9te2jcpo133bAPvcAIo9ny{2Tt}o{9_x7VLQ3#sa)R6R!ry`Gm>&N$ z*fv3lb=ixAdq-K%zy9_o@9{V3iR*LFTK%jHoEZ;aFrj(HvE%H?zM8s}>f8~yS-bz35r?Z`hV za=j{KB2(~|Qm5^@;zdW_x>sM54Y-uGB2E|D24rP7=3E>c;`_$FH6n#cPoS1!tV zPBLS;ck$f9qd~7CcY8#wdN!l_;Zmt>9ZK>YiVw1-(oMe@ES<(3&%W#L0slpt!}x_g z*UH_B%)h$DaE7Yq-l(jFTN1v;zv@c=yZA-?oZQCZ{Ii+H0i)bP>K{$)zzvN+QC~P6VZ94XyN4WrROK7 zHLm06sVv{rVYwvflgb98%+jx?WarrDM7hL-KG@^saZ6G|dH#OJ{Qi#FUOTR>P^q2# z>3;`=cH%XW_Vu?3-13i!|F}*9mjd{x`yYc zuT)B39lkE`e|5uIe6-@p3_)zDj1L=X0j`1(o^t*Q8i-ol#ut zcJXmnhk7G_$Q*v2WBR8y9hf;OZ`u~;$*ZFf&E8j5{N4xylGFe#Z zQPrIjD|~`I`hD!Dmh8Bsaw1T3FYg?sm!FroF22tYwpu&zb$zPY-#qt=Vf@o4SUrxc zT())JKE0QDm*$>)yu{q!d(yRnT(_#bxnUl6S)Y}g&cD8HS^vQv)rRZa_nq+y{jMCe z{O-SY(cG&S);UTv8?KJM%bNDK={axGY1YmA!pj?PE`9d-jLza++-LG8nZ10%vQ&&W z^t-v_6GMs3r>?{%s4*;G`8SMTxbd>*TB%haQLZO5GQOKVV4pNi?zO_w&|T@qH+03% z{EAw))#EpVvC7-kSB%pbJ%v@@^8R76Q1C8G3+vkT{9w-(!2-q)Jxi=Tbj8D%wq^WX zKlAI(Ijm_nxDNQuEdHRdK9cp$lA9>?%pr-M(8rb!ydLO1IKRYyt#>R*D8&--wO@797S>ZIJ{iG|`D!P^&ZWLbady*}7z4#2j z4}+)I4TWbXNE$&yreYllm(Rbn~jxV`t^vc>NDYxxbesSzyfE9(`+LRz=D%ZE zBjd~_dJ)1Q7ctX`-QNf3jdw4fx99}8>QGfQ+&r9!cM(h;RG7v0O|1jBS=aVPy zOa7!WU4Q0x=#**H=`)KPf0n9Gn)&|Lr{5tiKlVEx-m~U_%MpFaI~yfVKi<``!=zj& zY~NASzjj$N4z7iXPPRkv5V z`u<4A4!v@ru(gHdWj!71nU-Il;O$rEvg$hT0revF#H-omuW!^J^kDdSN4DM@Mzz+6=$V1@=mzkNpy|@9*SU=fujUpfr!AjCEgll-V_vaEAQuj^D-V zNk2vO3?|&ub3LMX;9tdOGw)}N?vw6`+0M3>JiU4~ld+D{Ire)TF~P^auE{^kvdrbk z^eE|HHygB<9yris?i1X2xWWDOt~_6x6SwCs_v{wCFC%#G^&K&dwFeknT9h6|A7EQz z>hrg#`;Fthq&S9sTx;Y7+4vowCMW(f`~NX*BqC2 zmG0x%;c@BGlFq7XhTY8B*Crme_|svsOWkAczx7Od*}CF}+0ozJnLS@K&EB%ljP)Ap zwCvio7k;fWI=}P4v$c|^Y#lHCIy2px cq`&>&^|*$${YL{k0|Nttr>mdKI;Vst01cw`TmS$7 literal 0 HcmV?d00001 diff --git a/dist/icons/overlay/controller_handheld.png b/dist/icons/overlay/controller_handheld.png new file mode 100644 index 0000000000000000000000000000000000000000..deb375011c85a5f4d615c08e33fcbb97f8532944 GIT binary patch literal 4645 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 literal 0 HcmV?d00001 diff --git a/dist/icons/overlay/controller_handheld_dark.png b/dist/icons/overlay/controller_handheld_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..1f5317aa0cbdbf74b3f2ee9cf45267233d345742 GIT binary patch literal 3745 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 E01iw|b^rhX literal 0 HcmV?d00001 diff --git a/dist/icons/overlay/controller_single_joycon_left.png b/dist/icons/overlay/controller_single_joycon_left.png new file mode 100644 index 0000000000000000000000000000000000000000..340ddc71b95c6fa9aa4407148a7b601e540a9b2b GIT binary patch literal 7489 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 KxvXpP literal 0 HcmV?d00001 diff --git a/dist/icons/overlay/controller_single_joycon_left_a.png b/dist/icons/overlay/controller_single_joycon_left_a.png new file mode 100644 index 0000000000000000000000000000000000000000..e0f5c2ad4f73ca72ec810ffa7da8dc4c9d3f0688 GIT binary patch literal 2609 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 literal 0 HcmV?d00001 diff --git a/dist/icons/overlay/controller_single_joycon_left_b.png b/dist/icons/overlay/controller_single_joycon_left_b.png new file mode 100644 index 0000000000000000000000000000000000000000..7429450a33de84a48866309cb0ed7e65e33f7913 GIT binary patch literal 2559 zcmeAS@N?(olHy`uVBq!ia0y~yU?>4$4mJh`hLBle>I@7FEX7WqAsieW95oy%9SjT% zoCO|{#S9F5M?jcysy3fA0|SF(iEBhjaDG}zd16s2LwR|*US?i)adKios$PCk`s{Z$ zQVa~7HJ&bxAr*7p&W+EJ2^T%K-#B}{vbDkr&F-loeSw$OY~AMkbiyI&J}nguj|EM_ zk4*Yn4{j;+Zn?FbYon{Ejfqp!wNM@Jvar(2$KEe*Cyx@S>rT6;DER zr~ZB)m&pqQGM;>RczF8ih3m!ch_&_g_utQyI2O#%$HXzca(5`hd9GwfJ=X#2inXAUznGwX-`*cW#Sh{fW;>EqZyu9lfez@r#Q(kmEJS=RVy@|<|knWGC4wkp2S$+P# zV&crmKPxwG{P^$Qy?a-s`lE%~wloBt^x3@COqk(abX3%@wFh>nK3vUbTly+QzQNOz z*QI2`{{8=Zmays9Bxfj1%t(7y$uM7dT9K^`-}`I7rXDj7)-(Rk^(l?%fb6nNJ560B z`F$@goIH8*?X=Xr{h!2nwT`Yzl-S)TV_D=G<)OFl#f9kYdA!d3GZ_TZp0zJ=-LdfA zg$kQIfx!7&`eJ7MJh*b@$_ZQgVy;Nvx^}Hhzv1mAtC$tAxTU0ffVuS6pM)3J4o&a)sF}&Q z^T)vp4MHE97n(0+ttfDga?q#lx9 znu}p!Kn6>;^{uT5LKi!>aG2L5W;|PSa>tfFonya#|6cFWU2s^ujj16u(x2hN2E*4; z7BXSGm!B|iHg(A;J?b|ibr<#>q7e(oFOLyPWm*vC>1u$obURy{A&G~Gsm?)&Wk+#Yf9>hX&{-4 zZ!FH~cfM$6kUF(foI&`&jMT{e5;8J-;?DJ|yxOekyfsZqMW{2Ep>5_){sXb9`~EO9 zOm;B|dbg-CrPj;r3ja*Dj}><^va@f$wmA3iOxWqW5w9n_(R^prP~Q0>CaudlzH>Xn z%U>e7851%e^iKOFk_u)l`z2yxa>C?;TXZd#o({`QwtE??`_tRmK#Vp1an8*?%=ufc zslT5hog!7Sc&ptySt$@>CxRhuWg^!vZC&=p!}-495AELG-nD!8?%lS8VZVFeBvp@v{M?I~gN0E-Z6&wu`#M!)wI5Cooq2{@Y*N zAC8^9pm6zs^wqD0#v!qJ4q{SUGA~T%=f4Y8G@rLE>kMy%d$Sp@)F(%m=VlVC49yc{ zd2g9yoM?Y+k!~&lcI#d{X|RTiaq`#SgB>*M_2VqC@1Om8606B&#JlHW)ciedASY(5 z^*=cGv#_wRdS}LpU7J1`6@7Ve@nKNBNcYiw2@=QZ()Nivh#U}SGKAM6d4 zY5%kv%!5{5abMcEbgf-!Yuv>vS4wg+Gc}nWScgu2>gPF=?PA83o$J=E<3G^M@XcuR z^{4aC{&~ytp;W0#gu%+n>L1&K%F~=KmhrMEBV{gkIujDz<&tMXk@yAt3%9!WzkuP6L4)Q0@dKLVt(8+Wr+ugf&zb?zP z<9VPN@?P?(P{Z=@u&}z?kB?k0zjo67s`>N4`Zk~Db?esM?Rz|jal`UMQlCEhX^2=E z*xJSxB*yKsQ?-Ba?)`gvXP3aVI=<{mmZwQf71uVc$uLYimb8&w=D211t(lw*@tbmg z$GE#6FXxWch@HR5$vA6n=7pO#E2qpn9&n_6r&c1fgrC}E_3oV!N}m@hZ8Y0@!sNx9 zH!?}j7I8%~%lWEJJ}**TsB0`=BWNSqeN;|ikuk%TKYJGYy4;^Vcka}V6`x)9F5DQA zqb@j?{X=G^uH_w-Mf&H@ojd2vebVjgy8RJ4Vrq-R7o7J=$lYag_wLaK7O}MuvlAhDk_qo1@|pV+9kbU-FrZsUPAq8Krn@<{^D{W_T(3-ldW2?Nn_+Qth zGC67QE~lw2_+e`@yS26Tr-#ZX!G?VAY{mm;Y^HcEjhiC&s7;@TwV}1O_47mz72XHY z6J|%|C{#4LeV?tu$!?_+RM%)kT7zopr0KZ186951J literal 0 HcmV?d00001 diff --git a/dist/icons/overlay/controller_single_joycon_left_b_dark.png b/dist/icons/overlay/controller_single_joycon_left_b_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..3bd97a8eb695e6f9c2d7c0982970b76a900f7594 GIT binary patch literal 2383 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 literal 0 HcmV?d00001 diff --git a/dist/icons/overlay/controller_single_joycon_left_dark.png b/dist/icons/overlay/controller_single_joycon_left_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..24ed2c44c919f36ca9c4211ac1e4d03e70dc739c GIT binary patch literal 6768 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 G&;$S|2RXd} literal 0 HcmV?d00001 diff --git a/dist/icons/overlay/controller_single_joycon_left_x.png b/dist/icons/overlay/controller_single_joycon_left_x.png new file mode 100644 index 0000000000000000000000000000000000000000..4615172a33957d1bf3538fbbe23d17a1bba5d6e1 GIT binary patch literal 2541 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 literal 0 HcmV?d00001 diff --git a/dist/icons/overlay/controller_single_joycon_left_y_dark.png b/dist/icons/overlay/controller_single_joycon_left_y_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..fdf177c12d4b72f3ae51dc5a6c26c459df82169d GIT binary patch literal 2639 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 literal 0 HcmV?d00001 diff --git a/dist/icons/overlay/controller_single_joycon_right_dark.png b/dist/icons/overlay/controller_single_joycon_right_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..afa80e6ef0f54acd62711c65abab9b9e7ad204a9 GIT binary patch literal 6729 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 xKey)H@_$8l<{miT5Wnh2R|ieN39`V4 literal 0 HcmV?d00001 diff --git a/dist/icons/overlay/osk_button_B.png b/dist/icons/overlay/osk_button_B.png new file mode 100644 index 0000000000000000000000000000000000000000..f4a04117857cd2c50ba830151e6ec7006622164a GIT binary patch literal 741 zcmeAS@N?(olHy`uVBq!ia0y~yV2}i14mJh`h9fUqlNlHoSc;uILpV4%IBGajIv5xj zI14-?iy0XBj({-ZRBb+Kkd6}9h?3y^w370~qEv?R@^Zb*yzJuS#DY}4{G#;P?`)(P z7?_egT^vI^I^TxcW{U)h*rul)Wi|d>w&|c#l2r3Q7G*}mrdH35p|cVcBW`erDMdxH zDmrRM>o%_qY*KjSo~R@?t*J{hW~0ZZn=@m1rJuarcq3AFQsr!y4X0l|yZ8Cs&hmG= zzcc>6ar{5i=bqQEUpvox?mKZ##`fFCJ*IiCV~m)#C1LGc?dhkl)(cGx@tKq&bwHGX zy?RbIG!HrwX>iU@&43gH? zs;msZI@jxw#XjZ-QODw#KQJ}CRAXAe@4)iF#c$5dpp_+uH7A+<6gIM*utmc4V#d8j zx5b9H^0v=k(iFR8&DP_w(-%IdyeA^#>o+MS;!-XHFT;KY8;-*td}WT8Gy0^jaJ~P1 zIm4MJT@&U^c(Q7X{DG8b)eHxgEP4NA+im-IBA(9`!W&bRURs`&z*%$$l$iwL3%lkU>qqpXpy|Y3cSJ!{wgaCadh6a%*=+WVXE3Wab8ozI4Wz z_fo303YDh82N^wURk|mgW7yHLe>MX{n%B}xXXh!s{8r1EaK!aZqiXUQiIcwK2fdAR zf9{KC+^}W(i%DX&_t$?B`k~Ck;3%+0BKa7@nL`&B^F+P)vuLxUz?yeceBQ0Np0#!H wOFb!dOPx%Gxy#Qp%v+}ac>CO@@{iKDdcJ-8cfn*Fl9tvAZp9D0s-qTv@0W+130=%c|3_ zNy(|zZ{4oQIV}GbnmX#)dMzws^p<8#U7!^5cEPd&$*T3v>n0qDiDKPuclqvb+xx$N z-}}Aqy=}zhS^uVA(S5P&UD@$MwKH!FHZb2%dAWOL?Szv1XO=Z9{P^NOVd27s`)cg! zIW0I-s?TsYuKrr}w`Oe^cQ~)4q~y({wDPk#=d;2&_LRt^-}(M`{aznF6YY*gFV4j? zF}N+(*cGcGQWfeerSQ*XvF=CK2OF2HnYp$<*)l(`5>r! ztN$eygPWp4@|RV3k4$EqutieC^H)+@uA8y-6pn?J{JXTKdj0KK^kU*nU9Vr;wrnX` z8=|!|Ej_)ztV&gM(x)Yvd1>K4_x)$HxWH(aly=l_{Y=$op2y8Av$s25FXf)3$6WeA z{KJNzm3QRiIpRCX~v(`h3g{?uTK*oagj+R*WPOqh(cz)b?SUaC%V#$8? zCBA9jR>kO_m{W=2{O5HH=~0i2C;4sPlisjr=2yn#1BQ>LiG&+Qt_?f?wdKg&@0@Sfrf$`0 zPux+Tm$zLmTw=8wuh Wl$@J1&z^ySfx*+&&t;ucLK6U7rc^lq literal 0 HcmV?d00001 diff --git a/dist/icons/overlay/osk_button_B_dark_disabled.png b/dist/icons/overlay/osk_button_B_dark_disabled.png new file mode 100644 index 0000000000000000000000000000000000000000..93c102b1b977463f3ea2157c9c838b41ca2e83a9 GIT binary patch literal 781 zcmeAS@N?(olHy`uVBq!ia0y~yV2}i14mJh`h9fUqlNlHoSc;uILpV4%IBGajIv5xj zI14-?iy0XBj({-ZRBb+Kkd6}9h?3y^w370~qEv?R@^Zb*yzJuS#DY}4{G#;P?`)(P z7?`>|T^vI^I^Rw`?=6xjaoqmAwRRSVw^}vFr%5hf_@{VGaBP2*Ag=G-{!Jh*X_o6W ztCg)RTDu?L+WJAIQNTsf(bb+Wv*d+D$diSKnr|hVcTb%=clqHr*LIy*BUbXcdiL|5 z`_6x_d{#OCd8_T4hxLw!dVbdIllEBk%JP9h0mBW3Fqf2zpZENnIB}x>e-$Hh^Xp+6 zB5Dn%9kM4Et3C*`k(2jb8niUF+sEHuJt`_nj^TLuY{d_|V)XdajAm|?bzLQD!|wN| z)q(f)u^Dx@w{2g{kv-=6Tj7cA2mVa`DcXe#2+GR^@edy#M`gZi&_3 zQ>Ay;bog8eysdNEi)r#grK{Oet$qrcLU;I2Z`#$Mn0#i{DzhSXC0CAT3L?j5GH{%Ur;^nABRK8LN5Ur)j${x-|i zwmUgVdF?d;^3t)yxu7ADPmel~iVDccrPtL~$ZZo4R5x1tO3_v4V25Sr{`rNjzf!xs!=t@p3*+*TuSy5kuS>jCI467J zRE8suKVEcNSTMf^sk0ee~;nk iZMpS#H7j`cGu-vQpT6tfx~B{b3=E#GelF{r5}E*S309o| literal 0 HcmV?d00001 diff --git a/dist/icons/overlay/osk_button_B_disabled.png b/dist/icons/overlay/osk_button_B_disabled.png new file mode 100644 index 0000000000000000000000000000000000000000..5900982f69066b420e16fa4b9fa7ca1ef348efd7 GIT binary patch literal 791 zcmeAS@N?(olHy`uVBq!ia0y~yV2}i14mJh`h9fUqlNlHoSc;uILpV4%IBGajIv5xj zI14-?iy0XBj({-ZRBb+Kkd6}9h?3y^w370~qEv?R@^Zb*yzJuS#DY}4{G#;P?`)(P z7?`Gbx;Tb-biSQ>&^shhK zO9g~9wrtE*+*7&OKp;cW(e;{Ax0r5gS4_x`0O^%FZgR!`-GwDH?n&!rJm0?idCm8l z?~mtI>%B=5|IaNX`lWXNRf)+#S3fHiNmwwfS@Ei=x^!V}P-gU3} zR=-LWycwJxFStXv?O{TFeSP6^rG4+^O^(?x^u7Pmav(c9H(YIUqODYkX^ISKc8#BV;j15>Gq>nJp<}8CVDKt{Jz2X>c$wo>q}S3ZjA};T|1c}tYO-U zl&5|!DZZigD_%X`w{PFe&d$!w*KR$(8Y39lDQviEm%wZjHP4@XzZ$g<{H?LuZ$8y) zFN59H3--sZ2~M23BSqKc$=N@D{``9LCa3As?v-vib6Xx;>Ph?R?2%w)}(02lwi0 z2V~H6Y~bM5l!XRo09hKPA=G)%hc{gQXWQ u;>}q0$GFbmm`%{tInOKOw`s>6%-`+zfBRnp;k6753=E#GelF{r5}E*nG-6i( literal 0 HcmV?d00001 diff --git a/dist/icons/overlay/osk_button_Y.png b/dist/icons/overlay/osk_button_Y.png new file mode 100644 index 0000000000000000000000000000000000000000..b08b4e26b9cde9477f9541ccfeba2f14cfc7ce7b GIT binary patch literal 726 zcmeAS@N?(olHy`uVBq!ia0y~yV2}i14mJh`h9fUqlNlHoSc;uILpV4%IBGajIv5xj zI14-?iy0XBj({-ZRBb+Kkd6}9h?3y^w370~qEv?R@^Zb*yzJuS#DY}4{G#;P?`)(P z7?^@RT^vI^I^RyU%@zq1X`5gC_=xTywwRj@c}@pb&bhnf#vuo;jT<^$k0>1t<7(=S zuXoT6oNcm9Fq=8-qj86V%2ZF=b2-U8-M3OY6cSq{3!X=uQE5p{GY@=IbGrC`@x0>a z_bkQ#z25(Cs*3gti*qv!mn~c+-Vp5I>9Q@fG}vWYXz9hzd)C*pNXpCW^YHTCWnRbZ zUj3x?fYa`~`)+4#J;dy+rlxjk)i1Bm^DGrAuC zXwAL7x9lu~!_uHijx$R)N<=W#BuHqy`~06Fj@ft8W-F$isA;AWEC+wznQPH!>{i59 zZ5SH+(I($7bp1Q??h~@-KR;_ocStl}x5|9=)mIsMMexLFD zyCHY?i(T(_J+^38 z`lNF9X<(>-({G8=VS!~0|SGntDnm{r-UW|wG~L( literal 0 HcmV?d00001 diff --git a/dist/icons/overlay/osk_button_Y_dark.png b/dist/icons/overlay/osk_button_Y_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..1fba9ca93dd607194f860d9f908f0493ff7701a8 GIT binary patch literal 502 zcmeAS@N?(olHy`uVBq!ia0y~yV2}i14mJh`h9fUqlNlHoSc;uILpV4%IBGajIv5xj zI14-?iy0XBj({-ZRBb+Kkd6}9h?3y^w370~qEv?R@^Zb*yzJuS#DY}4{G#;P?`)(P z7#NE^T^vI^I^RyU^=5VyaedEb7{>O;W3lIA1&(h`Q>QNI~&9TD5Zm|XVn z%CWpgI|X(V<)Ur16Ha%qG)!JMbszKRZ+|a5-2cIGo~$Rs>1)Q>$1TsybY1zfxu!ya zH{rssUyauN+utUor2I>neoQs@=JQg|%W~^C`?)O({V`?plRs$U zlRM{Ivb?Q|oN~-Ve@-q?9Bf!V#TIx=gcf&?NMVYY24({`TnfFmu`aj zORgC6WXES|F;h%5GhLq9@0ebqS8Qyj`uB(aUruJ*Bu9<23=9kmp00i_>zopr0E*4Y AQ2+n{ literal 0 HcmV?d00001 diff --git a/dist/icons/overlay/osk_button_Y_dark_disabled.png b/dist/icons/overlay/osk_button_Y_dark_disabled.png new file mode 100644 index 0000000000000000000000000000000000000000..6ce53f9e436caa8b4510b3fa44b242787198b271 GIT binary patch literal 694 zcmeAS@N?(olHy`uVBq!ia0y~yV2}i14mJh`h9fUqlNlHoSc;uILpV4%IBGajIv5xj zI14-?iy0XBj({-ZRBb+Kkd6}9h?3y^w370~qEv?R@^Zb*yzJuS#DY}4{G#;P?`)(P z7?_MbT^vI^I^RyU&j=0_Y2RP$v^Dluhba42y;7k$p;jKOiXj^wU067o@!-;X-!o2T z@G3>=u3z|4NpCLK(M3%WNA&Lcl)G|fF?&rJq z@9%K4TV41+X$!ZWj?Nq=wKZGA8x#{5ZZK|hNzv_E^z`M+m*;;s>FDZyefRF&J$ALm z!;!4YS|639vjqa8~U9 zTm_C}Yqx4UxMix(g`yKmpAzco{<#RSUF_>l_jO2Kk0~9kIpx_U&sw@X^e2ai+FpQBSP?RIlqb_x~?hHNE!! zeT}JJhp*j==KKHB(>6!w$O%nc?&)W3`C6mbuk2-t%<=7vn{K}0 zec;n|J<`2#?GzW6Bi?rXOMW)=`*n1zDfM_4wtnsejjm}wFGu_i)$MyMdOabnw)Ste z-t^B6iS5s$^`>_>b{~jrSD!O$R@K8MjfUry#s`)iE*7nDTby}&<)T&Pu?@B!cJtR? zef`yN!KAdj#pSE4rj=Q~b7bIZJ+y6AUqrV2<)1xS2b4qiru6*0m8q5+f6Rb~ee&*% wDQ@cWtYvGq%DX)2U8N_T9F?E3ypEZB`iA|DT>FkNFfcH9y85}Sb4q9e04~5WRR910 literal 0 HcmV?d00001 diff --git a/dist/icons/overlay/osk_button_Y_disabled.png b/dist/icons/overlay/osk_button_Y_disabled.png new file mode 100644 index 0000000000000000000000000000000000000000..25db07f665add24f59d5a64735be51b3eaf3d363 GIT binary patch literal 699 zcmeAS@N?(olHy`uVBq!ia0y~yV2}i14mJh`h9fUqlNlHoSc;uILpV4%IBGajIv5xj zI14-?iy0XBj({-ZRBb+Kkd6}9h?3y^w370~qEv?R@^Zb*yzJuS#DY}4{G#;P?`)(P z7?>Nqceizy?3I<%t{hZ+deWM&^2UT``>r%a?|;1WezE=Y ze^%msB}@LRz7*wYO1L2r9CEc#(2`k(eOqX0u*)@><12lAeQT{nQVI(VAMbiMXVtlm z6B}Fhh;0yPd-&$dmy*{9CMa6E>T7rL7RHwIN%SU9?AXH+*6=>|-~7Uw)^F1odr~*wbnl#?z|j_3y3Bals@L~^r-xT_jT>5 zr?aiMPf6cZfRuUifuNhfc`qtBmJAn@RP$NmU3we=~Vi)N{iF z+j0d?FqShMGT6g8$6MosRe#{svn(735@Is9^}p{HRZ_~Xoy4yaTAST!(HETjK2bJo zZHU&^oYo_ExBR|W{wil}*lxx-+a*K3Mh1oZF}5UHyx#Rr^;?wpa)f9d6yl!XC5CZ_zVeqMY3ewNwn%7l6U(ys(vU4Az0bCEqmMBTBf zy>=q5frfda$_F@a**{KCK4TrawsPL}y9?L;Vm|Qh{00s6?G+3R3=E#GelF{r5}E+H ClsLlx literal 0 HcmV?d00001 diff --git a/dist/icons/overlay/osk_button_backspace.png b/dist/icons/overlay/osk_button_backspace.png new file mode 100644 index 0000000000000000000000000000000000000000..4ad284720ded558efda48766f900bc4426e3cd63 GIT binary patch literal 2919 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 literal 0 HcmV?d00001 diff --git a/dist/icons/overlay/osk_button_backspace_dark.png b/dist/icons/overlay/osk_button_backspace_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..19ac8847e42b79bc89d6219359f942926b248980 GIT binary patch literal 2958 zcmeAS@N?(olHy`uVBq!ia0y~yV3@$bz!1a1#=yYPm}67Uz`(##?Bp53!NI{%!;#X# zz`(#+;1OBOz`%C|gc+x5^GP!>Fi4iTMwA5SrEaktG3V{v+@6?F$>aae%a)}t%;8=#Mch$p(?Xu-cV15OY{`le+mH~w zWU0X(SAi=U5(lPdd{BMJt;W^WrD(DsVFO32E0^w;)~zDZOA=%zo!gs!Jd*ob$DUNz zEvjW@Y40Dm-@VxSF7Nxj;&-QW-oH=2{CiIQru}n_jh|aS7mAvav~kAD5UuY_KVt4g z@}+6KV{m-&@}*@;LBWSQhVY|vpC&AS$|B^nMfhSAgZR<8s;nCx@jYmFT38?flgQp0eb(L(Q?jwZWWAk)olu?!d3AG`ob_a4zn{8>{s@p+{`xxWGlX z7bicPHOvc{_o6dYLGj2_208W%3j?3|p8tHVG^43APF_yV zZh4H}{ydE^F9#*j6WiX-p81n~R;pL8TXNyjpvuc0D@1e`{gU!`>ad>iGuz5_MrHNd zu-ysQ*Tw!f6V-9;*GN+ka{ANLvE>Ql2GIkXc0AP-;oAI2Dw^ZszJiB`oF7Zo+c=4? zh|o#Px&2R7@a}@hS1b2cf7c84dTT1Aqp&Skf8Eur^_+@!vR`ksIxXz&kh{4#{d}5Y z%enkCH%?y#MVIQOL6t2t?N>);<(kco-CF(qU9DRa>yd45kDl2hcp%W_OT-i*QLff) zQER_xG)!d>6TW!abE1YwF4*Y3emdi`-JP1L}gmfzu@W+ zp<0i5YMm2L{7B2Pn?K*s#^#TT;M~=bSzBN4GH3YU^Mvb^#iyRsj+y#_Ub!5}Z#bVw zo;FL1%PY$KzI$d?3EQi8@9YkJ-eVVAIpe3<_0s)4DW{oREQEa>`uS&$ z+v1I)2b|_wovE1}^eVb7i8YPulZm0;MUkhOLaSZeScIHpURT}|W4J4oI%DSkdr6;6 z4Amz3SI#=3HgTK7&X{*9O1JnNvZf>|?ef(8Dty;w7t4g@o!eX(pJobmyF@Xaop1kt zijUf6S%>LvGZWMHT`QckyJXXhrkU&8>nmq%5e{6Rd)xc3okmN>{q6bj`b{SZG32xvbfyfxj{5cHFTuvl&XVdcNx!&wp-lj;~j0qTn=_%dT<>UD1+3 zr@R?$IC}r3GUP{2+qxu1Z?}|b(!nS4dloNFE}r!C>V z?mu~Ewfb-S;02d5OwN_Xe$SUO6>3)OY?@?I*}(Zk!q&3T`pYfbI7um~sF<~3_b;rN zXpuBYW8x=<-`p-ir{tw9EI(nKHoA09W&g3jL1pP5&4&DpYky+* z#>GczP5rktQvITfO5mcWjBc!2V%?IOPiqIJdm?VWG# z%+&k;Tk5^Vx!5x`tKHU4HlBHAvs1@no+nwHPLq~dc>WB#qP7ynXr7^N8sowa$onYMoOv?wq}H@7}${w0%O#>m#{X1lLN)$n4?g z+;t{q`iv);lRP@ra;^q=@m_uX)tli@;NpK*gHBnS&hzJClRg!9slu*M$oJOk;I(1j zd3bqsSudV1}tytP_%)aILSl3YqsG`RR~=$IK87|b}Ca*p|fcyHl8^_Cy2 zj(y&kxQo&5RdITv@zT7*bFIt&$sQ15$Y+)9=%~xNz3p9F?vj?$sw68#wSEl{`9BIOjgo4yJ>*9ny?G zr)j3@J@~vZaP`$XR@=%fmo1n4?fqAVtTO(bdgjlPV>{luT+v?oXNMc_eHOb-k9WPZ zO8aNS5YK+`n1Qsz{TH`q3Ns6?b^D-N)iJZX?{Utj-wT4WRxDn;m}BqE7pbQ=m1Wty zmtM=g%JAZZEuxJH_uRNa5+{rgV{@@+ooZmx7pO7`)mdMZk;y9`949? z9sHd=<_CUH(J-rID8ByZ(~G_5vQ4w*ysW&pYu&naYZm(4@^M^r!O!0(a8bll#xrpy z?|Sd)?+9FcY*JreUwhZlq@;W4_MI*Z<@Z`PxNJPlEU|Cpz7Vae*4Ebgj2nCxpIPPbJhhu`@QaU-SH4L;huYnd;4at z@BDS=+6;M%ey(7-$KpP$sRx@_-LJp?n#PE` zpF7i1`ODmpiP6DszWu?^d&C*$h-_dHzF6A#_)fM__^BgDer=n_AFMY$US3Wvj_JYM z2Gx$4;oh41#xu2#$}HS(yftX$pP4hyvww&?ASBjc>KG?KLC9|6o^R2bTZIo4F*w@) z*lPGW8Pv)+7@^75@n-MGb+7lGI1`@m#HdX{=2_*vw#$Cjxs4}dG^*dC>!LzXBg28hIqfrky7daVSI@hy-tuF`#sJO6&KL!T0I$2g zF55GvSO_}IpYll}cxF(OV7CLq1B-KO8R|s7R4Q4q9(eZd-8HPYYc^YTaMCOzukk*>g?%=`51 z+qdG(6^ie4H2--V;kmLTK;z7+t6Arr-~aurDL6@^ks;yUp339fw{Fd4FSu~;*DWuV zBu;7Xtx>hj9n%CZZmBwbzC^W9+Ph^+P2JgTe~DWM4fnOkLg literal 0 HcmV?d00001 diff --git a/dist/icons/overlay/osk_button_plus.png b/dist/icons/overlay/osk_button_plus.png new file mode 100644 index 0000000000000000000000000000000000000000..5baa5201e64079e4e89874a182ab789b75997fdc GIT binary patch literal 626 zcmeAS@N?(olHy`uVBq!ia0y~yV2}i14mJh`h9fUqlNlHoSc;uILpV4%IBGajIv5xj zI14-?iy0XBj({-ZRBb+Kkd6}9h?3y^w370~qEv?R@^Zb*yzJuS#DY}4{G#;P?`)(P z7#QDsx;Tb-biSQxm@VQca{T}QnF|_|^Ton+_|B=d$L?y_*fV$I4!a+Me|W@_7SDZR z%%T&@Gc8QYk(GO+hlj%zwR>}o^B#8XsTSK_9y9ICy}1YV{%1YgYySRwTKaR|f_24z z^*@?CPB7S!)N@m*GTTCC-Gnn9-xy?8zFODuCMELQ)A25j4Q~q1z zI)8FLU~drk7|*YGXLa)inZs4ya{o-(b?OR z{G2~MlVnWJJtpJ*)ON-;&4zOokLLVlV>p}kxtJp{RVz=vLDt`+OyHu>$u&vIvzEDE zb(mDo;Pd9*Z{fPowa+XWU#`r`zV%{nymonu&3XH+x8KIEs#0v(V0CuwlUJ^asikYX z{=TiakYN(WSo7uHc8`7Cs!RnE#S)=+*GBH>nCGsjw5d5z->RESuu`UVig<>#RKgkU z_kSCnv`d{>ylb6L!ll}HrF&7YwmhG6Tj8g5lm^2Doi$ry8LGQe!mnPxb}LUdfr+s} z#j|g2u{48R(l<#_2Dv4fRmz@|-f!-*{F?ga`|r1##B(RTS@CM=Cp%e&iokt6@#(7a bul4Kp?J9K-FrLW3z`)??>gTe~DWM4f%~A^@ literal 0 HcmV?d00001 diff --git a/dist/icons/overlay/osk_button_plus_dark.png b/dist/icons/overlay/osk_button_plus_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..4cadb438b791e44f51be3e15285e50a3794cda23 GIT binary patch literal 676 zcmeAS@N?(olHy`uVBq!ia0y~yV2}i14mJh`h9fUqlNlHoSc;uILpV4%IBGajIv5xj zI14-?iy0XBj({-ZRBb+Kkd6}9h?3y^w370~qEv?R@^Zb*yzJuS#DY}4{G#;P?`)(P z7?>12T^vI^I^Rw;%nl9|IbQ$#+H^I=kPVLnayVzay<4_1A@=A|s}y~{UEQ-EJWiwF>xVh)(nky(=)$G*p+(@x#;k3Oi%;W}ZxW$86sCaz>@> zgOBIVoxAI{_@bpjN@3y0o>lt-u5z=-Dt@>o5xgQq>;7XOwnegEs`fr_XJOF!UbQ#< zXN_Inb-^Y7JEzIc%YDH9;qt`&GrX3*JL*((f4;g^bEnWmZ%2VSL046$yNBjSWX|*%CeJS~FK>?v{mbCP zdu(yBnq%7Wq@cB7=Er)U7X1`A=!wogUTzfkoPWvvlQ%Ce*jnc4BBC()a?#7obD_*fpknP?d9Lf;2_8OBw#k}cn%XYtyZEf&h*CIG^>x|C!i{9!k zq1`U!7Xz;_&f7JKDPe6b$DOi@ZH=~z?*DzF(!9!U#nr6xmpbP0l+XkK_&hI( literal 0 HcmV?d00001 diff --git a/dist/icons/overlay/osk_button_plus_dark_disabled.png b/dist/icons/overlay/osk_button_plus_dark_disabled.png new file mode 100644 index 0000000000000000000000000000000000000000..b8eb8dc3db923577c9b16b56e8896444c74e3a93 GIT binary patch literal 645 zcmeAS@N?(olHy`uVBq!ia0y~yV2}i14mJh`h9fUqlNlHoSc;uILpV4%IBGajIv5xj zI14-?iy0XBj({-ZRBb+Kkd6}9h?3y^w370~qEv?R@^Zb*yzJuS#DY}4{G#;P?`)(P z7?_wmT^vI^I^Ry+pB)@1a@_v=9ye}A`M^SrD@WJe<$84^@0wzD;mIjWZ9X-#`_;0`c=LK#9XWCOsl^j^!zcTeh;6SsR=w|g-MQ0G=N(X; z{zw1Jnxj!`%cO4vUTr@h`+)5NThy@`i|h8^*AVND|L+;J{`z$5x#x@%#CJ$-Ue9jx zy|!L|^2sk^)25t$8r|vgNVTB+B46WVjjl_H67$YY<=OwhwBWr=lc2BQO`fI)+qD%K z4$nWKYQbD5m2B1bt2X=XAatwPDMZFF*gIn#rs^xx2l+U3%Vg&4#VtoEHZC2-TXpZb{I}uYqo% z%a^-a`(2tft>Z*=DC>dGdrBYdeg9i~)vt4<0v~RM3M8KiJag>xhf|s_zyIcC+x$Z5 z%bKnFF|N!3Ys2^(E=;s!USsGMa_LG^J?V#m$L1d!3-oc1T zCsV2dMV2y=IU?pSQe)9k3H%_po|FjB0W?44bC+mPFVAKdI?p>FFLyR{>#0^`<~a_d%CY$ zFXn~QfA6BO3*Y~`Ci}#NzVAHY`#|bK$W>)Q%L19@(@sC#_fI&byuAEe>0RB>y8;uv z4NDn6=9g=H*4P!VBi8@ehwYcP!Lc|0RT&bFN^A~a z8S*IXZu^dPyII#n*r+?O|HwSKe%a-h%Qp(#|1KU{dN@sBqUrh1msahPx%@nFZ(RHa zCzm6$H=W#^q1Ux&k0{d}OLYc@(7oBGRoR{=oY7aB`Otgy)n9xU-j?lO60|aS{i-^a zITLsBh^fAi-xBD#Fu-D#$+Xi?1uS~QS6@9j)#saJvq|I6iwoZWKHc|Nqv7=~`As+9 ztan>Hk>_wh=8~5;9K|Ls6E%_$O%_N#6CKK`z|o}opmzQB+2L2qx49iik3Q6K!qMm9 znV_rF!#W;YnEu-Q!iGzz^GI=Y5W}jgulPQkcx1^~^`-v1cDiIpX?1Pw-;KOU42KNv zu<+^5JAEKjnxW$I%aVCF7(P4~xyKy#m`m-8)l46|6KR|6ANi#H2+$Ghzy0=GZbJLZ zYLOMM%68w4o6W{>`fSAd>*9+vPVu|BvhDEupY|uEr*i2k)AOJA`DWW)$-gr3z_m*e T6Rt5ZFfe$!`njxgN@xNALNY4R literal 0 HcmV?d00001 diff --git a/dist/icons/overlay/osk_button_shift.png b/dist/icons/overlay/osk_button_shift.png new file mode 100644 index 0000000000000000000000000000000000000000..f03e306972c4d5d598faf4203d6c6c6717268a1e GIT binary patch literal 1876 zcmeAS@N?(olHy`uVBq!ia0y~yV3@$bz)-`%#=yYPcEHk+fq{Xg*vT`5gM)*kh9jke zfq{Xuz$3Dlfr0M`2s2LA=96Y%V2~_vjVKAuPb(=;EJ|f4FE7{2%*!rLPAo{(%P&fw z{mw>;fr0I!r;B4q#hkZyD_4kxN*w?A{%ZB3n_n-uKn>W~Qgz`~CaC z&&xCKTYkQC{rgHO9%G`4pd)XnZi=z-OWLTqnrraKrpYUfo>Q4aMs^mzexw zeE+fjqsm+51c$zzOIV~DerLwpct$gPIiWjO%S#MA60UB3mo)3mlkYe*|+4^BhOnw2@~pLG$*MgC~g(^za+Lz#(CFD=}!H5 zOYT0K7sZvR_3e@8Es^JMCT%{z7ejqJxuV6Uu0p^r&8_#M9-|{OIF>^1StGTCsO$E~}2>K8|(F>&*ZBUf+0d=Jz0@i!6Iw{v55Bdg1xQ>CcRZ7VDX2 zr`--cdC2!j{1^Ymv(d7v?LRDhSg}3*s^`gLu1DhE#5cxTTP*v^_M~;?_aLJ}lf*2= zXG;IMW$qu{*l#FTos+e9icf^Ss@}RsGSemc{&R2W-Z_0z=f4B&N8;~TXIWf7+xEGp zX_+9bVY~#V%vP> z4#$c4Pk7aTOV3OSXj|E$vpnd?0-wnD`fx`b2Pq*=Y(EV7y z)wO2h`Ay;J_pqJ2?0wOfDXF4$iEN@!Q>4@bRUkJzpqm3{Z6KSu9z zhy0yQHu*MldczN{-|AcJA0u>J$euM|8|yqB?wGagejoZ{a&|NB7yKU{c}(@$GJ*T| z?HeZko7kPnkSO1EI>cfpZ?<6W!cT0 z@_UwEIqr0SBlleO+->zwk}PKba9q88EnC*ztcAJO*X@h;xe0bDq-}Urx_NSzdG*T` zYgzfe_0Q>7ys=C(ZKwJ-*zguQ~v&<&# z%r$29U+(9Qy}PkYQ|-6AUQg$d{@Y#&{{K(B+~gO0=6ipP#ph1>{i-d`H+ipA|G@Hb zZvD(6|KKG{IV&1}6xN7kl=#nWvOH2=rMY2i^`l2p95s%AD$afE&q z>T2ql%0BeR%#AxR`$+ja#Y1arUPdt&$bWpiX9kmBxd}tL@cotLjr-oMJiXfdgV@K% ze-&7NC-3d~ac&Jm*{A-PD(8*9{7lfe7*nTk^c#i{BxejS0-08>=*tY zUeH+oWS4Q{kB{-3Z=TNEaa8O;x+qiG)n`B5*F}Hs*w44qr~C`cJd>8VmSviku5j4+ z|M^Sm8+^x@rhPK{`v)XFL31C_dI{zYQfX!dgH{S zh9|E@{w43*(?EaC?{E)TnhX3TFEut>hmWV!+d!&79b+7m0&3?xw{;vql3O(x^_POus z(0A3d&c$n1HobfY$h{q5;bq*m@d_`P~Ndyorm^Who1k!Nra&% a>leEn=Jnx{J;=boz~JfX=d#Wzp$Pzo>4P5t literal 0 HcmV?d00001 diff --git a/dist/icons/overlay/osk_button_shift_dark.png b/dist/icons/overlay/osk_button_shift_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..c9cc5cd9afe9e8d81c1d8ad711329d715d5df606 GIT binary patch literal 2003 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 literal 0 HcmV?d00001 diff --git a/dist/icons/overlay/osk_button_shift_lock_on.png b/dist/icons/overlay/osk_button_shift_lock_on.png new file mode 100644 index 0000000000000000000000000000000000000000..09077ab015f84fdee0e2303dcfeb2eb1e7b8480a GIT binary patch literal 274 zcmeAS@N?(olHy`uVBq!ia0y~yU=RRd4mJh`2Kmqb6B!s7Sc;uILpV4%IBGajIv5xj zI14-?iy0XBj({-ZRBb+K1_lPn64!{5;QX|b^2DN4hVt@qz0ADq;^f4FRK5J7^x5xh zq!<_&`aNA7Lo9mVPEh1K6d>Soe~r6Hd~c4gu-}21J6K-YK49s{ax%P8rK9+B9v_=) zMjii!jrr-HHf>1Z%(yo7chIt<_iCf(idJ9Y`N{ZwsoVX=ul$vkA+BK;x743vaJ|vK zrt#i&mNjYxr=(II{b_iyOz^p+|DIfvKJf)V6#d&@OR}AP5TIJfBs77=y*+zNZ4HB9 Xxi;^wy*7mm3=9mOu6{1-oD!M<90Fot literal 0 HcmV?d00001 diff --git a/dist/icons/overlay/osk_button_shift_on.png b/dist/icons/overlay/osk_button_shift_on.png new file mode 100644 index 0000000000000000000000000000000000000000..e7c1187f90e33cf6746cb02ac884205101f0ab40 GIT binary patch literal 1573 zcmeAS@N?(olHy`uVBq!ia0y~yV3@$bz)-`%#=yYPcEHk+fq{Xg*vT`5gM)*kh9jke zfq{Xuz$3Dlfr0M`2s2LA=96Y%V2~_vjVKAuPb(=;EJ|f4FE7{2%*!rLPAo{(%P&fw z{mw>;fq`|Ar;B4q#hkZyBUglm${hdrey`Td;K;&!v(>#~F6LU3lxA^cHgKKHSkl<^ zY>`4$=7J1`Mccc&*cJu3%r>a)STd7MLt0+zvRBY-7vrtfuM_9?E_*(2^ZB^!_y41Q z|FQdUnEm~RZ&CJpet-BrkN46k)dN$nbx$ynWzJzRV>&Lq-c@L``39jM+b3Q<5OP`6 z)94uE{7pM;BKI+|NfNnliT7>%2J`Za~tzd#?RC7Zrt~3y0)PttIo2|pHd?4{E)h|RBmBK zyR?jpp|k7W>2T8pRO@^w_@pTjr}%}@7BDSwL~d>V!n@{mh*u%Uh90k$|W}R zy*f2HHTH3}_n|VzeT#p6O8Lq7LjSMUQjx_C<{P#Bru;%o$1_hv8gn1+d2CKJwofFnKZI}1hw`v{hn6#^--$L2A1EGwww;n zxFa|>?t$u?(-Y0P8w8iWX%Q*n|F-mnN%{0m!k0E~URefq&ra1h?Ij1Yd<3;#H!3&1 z?|O2HU+3^XmVG}tZ`gjg=OMjszpr5QJ=e#5Ubbz27q*|f>{#y6R?7UXJvM9|{|^64 zM?8;MF5?jGwe}MEJwqv^j`ky{ibL{-`|HvC(XaOLwWnf z1JyU=qjG+7+_;w^*vLq6!u&W#`5UAuGgO0|!mR`Y?CtlxKR z`^H{({9i%P$@vGiI9bO!3m2SUxWR79l|_Z@c|IStzO_%x61(*DaUc5z`<5%k^~ZO| z35zG}|5-x-Q?Hv(u?Q2CFLEgE8eYY{@iAA%ABU(z09wB-%jIUTK?2(CW}nNF%hvy zW|=vUYZfgtY|Y)rsJ6{^sYfqE+~GRm-L~Bg6LZh4+F`e6u z`z5_Lu3_Z6`7Ffx!RO)vrFY9B-!GfG?R{bXy$!ogsEgOyeC4>~yZO}QeJ2lm7Lh3z z+1pviVX^;7u-&{Cd9Ao$XXy^b^qglA`U|F3-negYY*OXC>d0A({x1z> z31~brnM)-uMB(EircQBDk3B0K9=Wm#>1$1>2y8skO$(`i%m)+>yw7aqN@QSQVDNPH Kb6Mw<&;$TXKH#?i literal 0 HcmV?d00001 diff --git a/dist/icons/overlay/osk_button_shift_on_dark.png b/dist/icons/overlay/osk_button_shift_on_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..58e0d9cf41efc02d579aa2bfde0104e37d6849ca GIT binary patch literal 1937 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 + + arrow_left.png + arrow_left_dark.png + arrow_right.png + arrow_right_dark.png + button_minus.png + button_minus_dark.png + button_plus.png + button_plus_dark.png + button_A.png + button_A_dark.png + button_B.png + button_B_dark.png + button_X.png + button_X_dark.png + button_Y.png + button_Y_dark.png + button_L.png + button_L_dark.png + button_R.png + button_R_dark.png + button_press_stick.png + button_press_stick_dark.png + osk_button_B.png + osk_button_B_disabled.png + osk_button_B_dark.png + osk_button_B_dark_disabled.png + osk_button_Y.png + osk_button_Y_disabled.png + osk_button_Y_dark.png + osk_button_Y_dark_disabled.png + osk_button_backspace.png + osk_button_backspace_dark.png + osk_button_plus.png + osk_button_plus_disabled.png + osk_button_plus_dark.png + osk_button_plus_dark_disabled.png + osk_button_shift.png + osk_button_shift_dark.png + osk_button_shift_on.png + osk_button_shift_on_dark.png + osk_button_shift_lock_on.png + osk_button_shift_lock_off.png + controller_dual_joycon.png + controller_dual_joycon_dark.png + controller_pro.png + controller_pro_dark.png + controller_handheld.png + controller_handheld_dark.png + controller_single_joycon_left.png + controller_single_joycon_left_dark.png + controller_single_joycon_right.png + controller_single_joycon_right_dark.png + controller_single_joycon_left_a.png + controller_single_joycon_left_a_dark.png + controller_single_joycon_left_b.png + controller_single_joycon_left_b_dark.png + controller_single_joycon_left_x.png + controller_single_joycon_left_x_dark.png + controller_single_joycon_left_y.png + controller_single_joycon_left_y_dark.png + + From f6e6913f8ff5f533e69a5831a81ca8f15f709baf Mon Sep 17 00:00:00 2001 From: Its-Rei Date: Sat, 20 Mar 2021 07:53:00 -0400 Subject: [PATCH 09/14] qt_themes: Add styles for the On-Screen Keyboard and OverlayDialog --- dist/qt_themes/default/style.qss | 377 +++++++++++++++ dist/qt_themes/qdarkstyle/style.qss | 399 +++++++++++++++- .../qdarkstyle_midnight_blue/style.qss | 439 +++++++++++++++++- 3 files changed, 1193 insertions(+), 22 deletions(-) diff --git a/dist/qt_themes/default/style.qss b/dist/qt_themes/default/style.qss index 836dd25ca4..3bc92b69d5 100644 --- a/dist/qt_themes/default/style.qss +++ b/dist/qt_themes/default/style.qss @@ -281,3 +281,380 @@ QWidget#controllerPlayer7, QWidget#controllerPlayer8 { background: transparent; } + +QDialog#QtSoftwareKeyboardDialog, +QStackedWidget#topOSK { + background: rgba(51, 51, 51, .9); +} + + +QDialog#OverlayDialog, +QStackedWidget#stackedDialog { + background: rgba(51, 51, 51, .7); +} + +QWidget#boxOSK, +QWidget#lineOSK, +QWidget#richDialog, +QWidget#lineDialog { + background: transparent; +} + +QStackedWidget#bottomOSK, +QWidget#contentDialog, +QWidget#contentRichDialog { + background: rgba(240, 240, 240, 1); +} + +QWidget#contentDialog, +QWidget#contentRichDialog { + margin: 5px; + border-radius: 6px; +} + +QWidget#buttonsDialog, +QWidget#buttonsRichDialog { + margin: 5px; + border-top: 2px solid rgba(44, 44, 44, 1); +} + +QWidget#legendOSKnum { + border-top: 1px solid rgba(44, 44, 44, 1); +} + +QStackedWidget#stackedDialog QTextBrowser QScrollBar::vertical { + background: #cdcdcd; + width: 15px; + margin: 15px 3px 15px 3px; + border: 1px transparent; + border-radius: 4px; +} + +QStackedWidget#stackedDialog QTextBrowser QScrollBar::horizoncal { + background: #cdcdcd; + height: 15px; + margin: 3px 15px 3px 15px; + border: 1px transparent; + border-radius: 4px; +} + +QStackedWidget#stackedDialog QTextBrowser QScrollBar::handle { + background: #fff; + border-radius: 4px; + min-height: 5px; + min-width: 5px; +} + +QStackedWidget#stackedDialog QTextBrowser QScrollBar::add-line, +QStackedWidget#stackedDialog QTextBrowser QScrollBar::sub-line, +QStackedWidget#stackedDialog QTextBrowser QScrollBar::add-page, +QStackedWidget#stackedDialog QTextBrowser QScrollBar::sub-page { + background: none; +} + +QWidget#inputOSK { + border-bottom: 3px solid rgba(255, 255, 255, .9); +} + +QWidget#inputOSK QLineEdit { + background: transparent; + border: none; + color: #ccc; +} + +QWidget#inputBoxOSK { + border: 2px solid rgba(255, 255, 255, .9); +} + +QWidget#inputBoxOSK QTextEdit { + background: transparent; + border: none; + color: #ccc; +} + +QWidget#richDialog QTextBrowser { + background: transparent; + border: none; + padding: 35px 65px; +} + + +QWidget#lineOSK QLabel#label_header { + color: #f0f0f0; +} + +QWidget#lineOSK QLabel#label_sub, +QWidget#lineOSK QLabel#label_characters, +QWidget#boxOSK QLabel#label_characters_box { + color: #ccc; +} + +QWidget#contentDialog QLabel#label_title, +QWidget#contentRichDialog QLabel#label_title_rich { + color: #888; +} + +QWidget#contentDialog QLabel#label_dialog { + padding: 20px 65px; +} + +QWidget#contentDialog QLabel#label_title, +QWidget#contentRichDialog QLabel#label_title_rich { + padding: 0px 65px; +} + +QDialog#OverlayDialog QPushButton { + color: rgba(49, 79, 239, 1); + background: transparent; + border: none; + padding: 0px; + min-width: 0px; +} + +QDialog#OverlayDialog QPushButton:focus, +QDialog#OverlayDialog QPushButton:hover { + color: rgba(49, 79, 239, 1); + background: rgba(255, 255, 255, 1); + border: 5px solid rgba(148, 250, 202, 1); + border-radius: 6px; + outline: none; +} + +QDialog#OverlayDialog QPushButton:pressed { + color: rgba(240, 240, 240, 1); + background: rgba(150, 150, 150, 1); + border: 5px solid rgba(148, 250, 202, 1); + border-radius: 6px; + outline: none; +} + +QDialog#QtSoftwareKeyboardDialog QPushButton { + background: rgba(232, 232, 232, 1); + border: 2px solid rgba(240, 240, 240, 1); +} + +QDialog#QtSoftwareKeyboardDialog QPushButton#button_shift, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_return, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_space, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_shift_shift, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_return_shift, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_space_shift { + background: rgba(218, 218, 218, 1); + border: 2px solid rgba(240, 240, 240, 1); +} + +QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace_shift, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace_num { + color: rgba(240, 240, 240, 1); + background: rgba(44, 44, 44, 1); + border: 2px solid rgba(240, 240, 240, 1); +} + +QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok_shift, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok_num { + color: rgba(240, 240, 240, 1); + background: rgba(49, 79, 239, 1); + border: 2px solid rgba(240, 240, 240, 1); +} + +QDialog#QtSoftwareKeyboardDialog QPushButton:focus, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok:focus, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_shift:focus, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_space:focus, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_return:focus, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace:focus, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok_shift:focus, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_shift_shift:focus, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_space_shift:focus, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_return_shift:focus, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace_shift:focus, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok_num:focus, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace_num:focus, + +QDialog#QtSoftwareKeyboardDialog QPushButton:hover, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok:hover, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_shift:hover, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_space:hover, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_return:hover, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace:hover, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok_shift:hover, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_shift_shift:hover, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_space_shift:hover, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_return_shift:hover, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace_shift:hover, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok_num:hover, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace_num:hover { + color: rgba(0, 0, 0, 1); + background: rgba(255, 255, 255, 1); + border: 5px solid rgba(148, 250, 202, 1); + border-radius: 6px; + outline: none; +} + +QDialog#QtSoftwareKeyboardDialog QPushButton:pressed, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok:pressed, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_shift:pressed, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_space:pressed, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_return:pressed, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace:pressed, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok_shift:pressed, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_shift_shift:pressed, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_space_shift:pressed, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_return_shift:pressed, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace_shift:pressed, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok_num:pressed, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace_num:pressed { + color: rgba(240, 240, 240, 1); + background: rgba(150, 150, 150, 1); + border: 5px solid rgba(148, 250, 202, 1); + border-radius: 6px; +} + +QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace_shift, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace_num { + background-position: right top; + background-repeat: no-repeat; + background-origin: content; + background-image: url(:/overlay/osk_button_B.png); + qproperty-icon: url(:/overlay/osk_button_backspace.png); + qproperty-iconSize: 36px; +} + +QDialog#QtSoftwareKeyboardDialog QPushButton#button_space, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_space_shift { + background-position: right top; + background-repeat: no-repeat; + background-origin: content; + background-image: url(:/overlay/osk_button_Y.png); +} + +QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok_shift, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok_num { + background-position: right top; + background-repeat: no-repeat; + background-origin: content; + background-image: url(:/overlay/osk_button_plus.png); +} + +QDialog#QtSoftwareKeyboardDialog QPushButton#button_shift { + background-position: left top; + background-repeat: no-repeat; + background-origin: content; + background-image: url(:/overlay/osk_button_shift_lock_off.png); + qproperty-icon: url(:/overlay/osk_button_shift.png); + qproperty-iconSize: 36px; +} + +QDialog#QtSoftwareKeyboardDialog QPushButton#button_shift_shift { + background-position: left top; + background-repeat: no-repeat; + background-origin: content; + background-image: url(:/overlay/osk_button_shift_lock_off.png); + qproperty-icon: url(:/overlay/osk_button_shift_on.png); + qproperty-iconSize: 36px; +} + +QDialog#QtSoftwareKeyboardDialog QPushButton#button_left_bracket, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_right_bracket, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_left_parenthesis, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_right_parenthesis { + padding-bottom: 7px; +} + +QDialog#QtSoftwareKeyboardDialog QWidget#titleOSK QLabel { + background: transparent; + color: #ccc; +} + +QDialog#QtSoftwareKeyboardDialog QWidget#button_L, +QDialog#QtSoftwareKeyboardDialog QWidget#button_L_shift, +QDialog#QtSoftwareKeyboardDialog QWidget#button_L_num { + image: url(:/overlay/button_L.png); +} + +QDialog#QtSoftwareKeyboardDialog QWidget#arrow_left, +QDialog#QtSoftwareKeyboardDialog QWidget#arrow_left_shift, +QDialog#QtSoftwareKeyboardDialog QWidget#arrow_left_num { + image: url(:/overlay/arrow_left.png); +} + +QDialog#QtSoftwareKeyboardDialog QWidget#button_R, +QDialog#QtSoftwareKeyboardDialog QWidget#button_R_shift, +QDialog#QtSoftwareKeyboardDialog QWidget#button_R_num { + image: url(:/overlay/button_R.png); +} + +QDialog#QtSoftwareKeyboardDialog QWidget#arrow_right, +QDialog#QtSoftwareKeyboardDialog QWidget#arrow_right_shift, +QDialog#QtSoftwareKeyboardDialog QWidget#arrow_right_num { + image: url(:/overlay/arrow_right.png); +} + +QDialog#QtSoftwareKeyboardDialog QWidget#button_press_stick, +QDialog#QtSoftwareKeyboardDialog QWidget#button_press_stick_shift { + image: url(:/overlay/button_press_stick.png); +} + +QDialog#QtSoftwareKeyboardDialog QWidget#button_X, +QDialog#QtSoftwareKeyboardDialog QWidget#button_X_shift, +QDialog#QtSoftwareKeyboardDialog QWidget#button_X_num { + image: url(:/overlay/button_X.png); +} + +QDialog#QtSoftwareKeyboardDialog QWidget#button_A, +QDialog#QtSoftwareKeyboardDialog QWidget#button_A_shift, +QDialog#QtSoftwareKeyboardDialog QWidget#button_A_num { + image: url(:/overlay/button_A.png); +} + +QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok:disabled, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_space:disabled, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_return:disabled, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace:disabled, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok_shift:disabled, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_space_shift:disabled, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_return_shift:disabled, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace_shift:disabled, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok_num:disabled, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace_num:disabled { + color: rgba(164, 164, 164, 1); + background-color: rgba(218, 218, 218, 1); +} + +QDialog#QtSoftwareKeyboardDialog QPushButton#button_at:disabled, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_slash:disabled, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_percent:disabled, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_1:disabled, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_2:disabled, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_3:disabled, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_4:disabled, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_5:disabled, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_6:disabled, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_7:disabled, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_8:disabled, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_9:disabled, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_0:disabled, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_return:disabled { + color: rgba(164, 164, 164, 1); +} + +QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok:disabled, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok_shift:disabled, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok_num:disabled { + background-image: url(:/overlay/osk_button_plus_disabled.png); +} + +QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace:disabled, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace_shift:disabled, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace_num:disabled { + background-image: url(:/overlay/osk_button_B_disabled.png); +} + +QDialog#QtSoftwareKeyboardDialog QPushButton#button_space:disabled, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_space_shift:disabled { + background-image: url(:/overlay/osk_button_Y_disabled.png); +} diff --git a/dist/qt_themes/qdarkstyle/style.qss b/dist/qt_themes/qdarkstyle/style.qss index 2a1e8ddebe..8ce6d75f75 100644 --- a/dist/qt_themes/qdarkstyle/style.qss +++ b/dist/qt_themes/qdarkstyle/style.qss @@ -1560,7 +1560,400 @@ QWidget#controllerPlayer8 { background: transparent; } -/* touchscreen mapping widget */ -TouchScreenPreview { - qproperty-dotHighlightColor: #3daee9; +QDialog#QtSoftwareKeyboardDialog, +QStackedWidget#topOSK { + background: rgba(41, 41, 41, .9); +} + + +QDialog#OverlayDialog, +QStackedWidget#stackedDialog { + background: rgba(41, 41, 41, .7); +} + +QWidget#boxOSK, +QWidget#lineOSK, +QWidget#richDialog, +QWidget#lineDialog { + background: transparent; +} + +QStackedWidget#bottomOSK, +QWidget#contentDialog, +QWidget#contentRichDialog { + background: rgba(71, 69, 71, 1); +} + +QWidget#contentDialog, +QWidget#contentRichDialog { + margin: 5px; + border-radius: 6px; +} + +QWidget#buttonsDialog, +QWidget#buttonsRichDialog { + margin: 5px; + border-top: 2px solid rgba(255, 255, 255, .9); +} + +QWidget#legendOSKnum { + border-top: 1px solid rgba(255, 255, 255, 1); +} + +QStackedWidget#stackedDialog QTextBrowser QWidget { + background: transparent; +} + +QStackedWidget#stackedDialog QTextBrowser QScrollBar { + background: #2a2929; +} + +QStackedWidget#stackedDialog QTextBrowser QScrollBar::sub-line, +QStackedWidget#stackedDialog QTextBrowser QScrollBar::add-line { + border-image: none; +} + +QWidget#inputOSK { + border-bottom: 3px solid rgba(255, 255, 255, .9); +} + +QWidget#inputOSK QLineEdit { + background: transparent; + border: none; + color: #ccc; + padding: 0px; +} + +QWidget#inputBoxOSK { + border: 2px solid rgba(255, 255, 255, .9); +} + +QWidget#inputBoxOSK QTextEdit { + background: transparent; + border: none; + color: #ccc; +} + +QWidget#richDialog QTextBrowser { + background: transparent; + border: none; + color: #fff; + padding: 35px 65px; +} + +QWidget#lineOSK QLabel#label_header { + color: #f0f0f0; +} + +QWidget#lineOSK QLabel#label_sub, +QWidget#lineOSK QLabel#label_characters, +QWidget#contentDialog QLabel#label_title, +QWidget#contentRichDialog QLabel#label_title_rich, +QWidget#boxOSK QLabel#label_characters_box { + color: #ccc; +} + +QWidget#buttonsDialog, +QWidget#buttonsRichDialog, +QWidget#mainOSK, +QWidget#headerOSK, +QWidget#normalOSK, +QWidget#shiftOSK, +QWidget#numOSK, +QWidget#subOSK, +QWidget#inputOSK, +QWidget#inputBoxOSK, +QWidget#charactersOSK, +QWidget#charactersBoxOSK, +QWidget#legendOSK, +QWidget#legendOSK QWidget, +QWidget#legendOSKshift, +QWidget#legendOSKshift QWidget, +QWidget#legendOSKnum, +QWidget#legendOSKnum QWidget { + background: transparent; +} + +QWidget#contentDialog QLabel, +QWidget#legendOSK QLabel, +QWidget#legendOSKshift QLabel, +QWidget#legendOSKnum QLabel { + color: rgba(255, 255, 255, 1); +} + +QWidget#contentDialog QLabel#label_dialog { + padding: 20px 65px; +} + +QWidget#contentDialog QLabel#label_title, +QWidget#contentRichDialog QLabel#label_title_rich { + padding: 0px 65px; +} + +QDialog#OverlayDialog QPushButton { + color: rgba(1, 253, 201, 1); + background: transparent; + border: none; + padding: 0px; + min-width: 0px; +} + +QDialog#OverlayDialog QPushButton:focus, +QDialog#OverlayDialog QPushButton:hover { + color: rgba(1, 253, 201, 1); + background: rgba(58, 61, 66, 1); + border: 5px solid rgba(56, 189, 225, 1); + border-radius: 6px; + outline: none; +} + +QDialog#OverlayDialog QPushButton:pressed { + color: rgba(240, 240, 240, 1); + background: rgba(150, 150, 150, 1); + border: 5px solid rgba(56, 189, 225, 1); + border-radius: 6px; + outline: none; +} + +QDialog#QtSoftwareKeyboardDialog QPushButton { + color: rgba(255, 255, 255, 1); + background: rgba(80, 79, 80, 1); + border: 2px solid rgba(71, 69, 71, 1); + padding: 0px; + min-width: 0px; +} + +QDialog#QtSoftwareKeyboardDialog QPushButton#button_shift, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_return, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_space, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_shift_shift, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_return_shift, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_space_shift { + background: rgba(95, 94, 95, 1); + border: 2px solid rgba(71, 69, 71, 1); +} + +QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace_shift, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace_num { + color: rgba(240, 240, 240, 1); + background: rgba(255, 255, 255, 1); + border: 2px solid rgba(71, 69, 71, 1); +} + +QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok_shift, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok_num { + color: rgba(0, 0, 0, 1); + background: rgba(1, 253, 201, 1); + border: 2px solid rgba(71, 69, 71, 1); +} + +QDialog#QtSoftwareKeyboardDialog QPushButton:focus, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok:focus, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_shift:focus, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_space:focus, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_return:focus, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace:focus, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok_shift:focus, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_shift_shift:focus, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_space_shift:focus, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_return_shift:focus, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace_shift:focus, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok_num:focus, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace_num:focus, + +QDialog#QtSoftwareKeyboardDialog QPushButton:hover, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok:hover, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_shift:hover, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_space:hover, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_return:hover, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace:hover, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok_shift:hover, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_shift_shift:hover, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_space_shift:hover, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_return_shift:hover, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace_shift:hover, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok_num:hover, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace_num:hover { + color: rgba(255, 255, 255, 1); + background: rgba(58, 61, 66, 1); + border: 5px solid rgba(56, 189, 225, 1); + border-radius: 6px; + outline: none; +} + +QDialog#QtSoftwareKeyboardDialog QPushButton:pressed, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok:pressed, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_shift:pressed, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_space:pressed, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_return:pressed, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace:pressed, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok_shift:pressed, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_shift_shift:pressed, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_space_shift:pressed, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_return_shift:pressed, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace_shift:pressed, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok_num:pressed, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace_num:pressed { + color: rgba(240, 240, 240, 1); + background: rgba(150, 150, 150, 1); + border: 5px solid rgba(56, 189, 225, 1); + border-radius: 6px; +} + +QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace_shift, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace_num { + background-position: right top; + background-repeat: no-repeat; + background-origin: content; + background-image: url(:/overlay/osk_button_B_dark.png); + qproperty-icon: url(:/overlay/osk_button_backspace_dark.png); + qproperty-iconSize: 36px; +} + +QDialog#QtSoftwareKeyboardDialog QPushButton#button_space, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_space_shift { + background-position: right top; + background-repeat: no-repeat; + background-origin: content; + background-image: url(:/overlay/osk_button_Y_dark.png); +} + +QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok_shift, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok_num { + color: rgba(44, 44, 44, 1); + background-position: right top; + background-repeat: no-repeat; + background-origin: content; + background-image: url(:/overlay/osk_button_plus_dark.png); +} + +QDialog#QtSoftwareKeyboardDialog QPushButton#button_shift { + background-position: left top; + background-repeat: no-repeat; + background-origin: content; + background-image: url(:/overlay/osk_button_shift_lock_off.png); + qproperty-icon: url(:/overlay/osk_button_shift_dark.png); + qproperty-iconSize: 36px; +} + +QDialog#QtSoftwareKeyboardDialog QPushButton#button_shift_shift { + background-position: left top; + background-repeat: no-repeat; + background-origin: content; + background-image: url(:/overlay/osk_button_shift_lock_off.png); + qproperty-icon: url(:/overlay/osk_button_shift_on_dark.png); + qproperty-iconSize: 36px; +} + +QDialog#QtSoftwareKeyboardDialog QPushButton#button_left_bracket, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_right_bracket, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_left_parenthesis, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_right_parenthesis { + padding-bottom: 7px; +} + +QDialog#QtSoftwareKeyboardDialog QWidget#titleOSK QLabel { + background: transparent; + color: #ccc; +} + +QDialog#QtSoftwareKeyboardDialog QWidget#button_L, +QDialog#QtSoftwareKeyboardDialog QWidget#button_L_shift, +QDialog#QtSoftwareKeyboardDialog QWidget#button_L_num { + image: url(:/overlay/button_L_dark.png); +} + +QDialog#QtSoftwareKeyboardDialog QWidget#arrow_left, +QDialog#QtSoftwareKeyboardDialog QWidget#arrow_left_shift, +QDialog#QtSoftwareKeyboardDialog QWidget#arrow_left_num { + image: url(:/overlay/arrow_left_dark.png); +} + +QDialog#QtSoftwareKeyboardDialog QWidget#button_R, +QDialog#QtSoftwareKeyboardDialog QWidget#button_R_shift, +QDialog#QtSoftwareKeyboardDialog QWidget#button_R_num { + image: url(:/overlay/button_R_dark.png); +} + +QDialog#QtSoftwareKeyboardDialog QWidget#arrow_right, +QDialog#QtSoftwareKeyboardDialog QWidget#arrow_right_shift, +QDialog#QtSoftwareKeyboardDialog QWidget#arrow_right_num { + image: url(:/overlay/arrow_right_dark.png); +} + +QDialog#QtSoftwareKeyboardDialog QWidget#button_press_stick, +QDialog#QtSoftwareKeyboardDialog QWidget#button_press_stick_shift { + image: url(:/overlay/button_press_stick_dark.png); +} + +QDialog#QtSoftwareKeyboardDialog QWidget#button_X, +QDialog#QtSoftwareKeyboardDialog QWidget#button_X_shift, +QDialog#QtSoftwareKeyboardDialog QWidget#button_X_num { + image: url(:/overlay/button_X_dark.png); +} + +QDialog#QtSoftwareKeyboardDialog QWidget#button_A, +QDialog#QtSoftwareKeyboardDialog QWidget#button_A_shift, +QDialog#QtSoftwareKeyboardDialog QWidget#button_A_num { + image: url(:/overlay/button_A_dark.png); +} + +QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok:disabled, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_space:disabled, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_return:disabled, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace:disabled, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok_shift:disabled, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_space_shift:disabled, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_return_shift:disabled, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace_shift:disabled, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok_num:disabled, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace_num:disabled { + color: rgba(144, 144, 144, 1); + background-color: rgba(95, 94, 95, 1); +} + +QDialog#QtSoftwareKeyboardDialog QPushButton#button_at:disabled, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_slash:disabled, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_percent:disabled, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_1:disabled, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_2:disabled, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_3:disabled, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_4:disabled, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_5:disabled, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_6:disabled, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_7:disabled, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_8:disabled, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_9:disabled, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_0:disabled, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_return:disabled { + color: rgba(144, 144, 144, 1); +} + +QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok:disabled, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok_shift:disabled, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok_num:disabled { + background-image: url(:/overlay/osk_button_plus_dark_disabled.png); +} + +QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace:disabled, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace_shift:disabled, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace_num:disabled { + background-image: url(:/overlay/osk_button_B_dark_disabled.png); +} + +QDialog#QtSoftwareKeyboardDialog QPushButton#button_space:disabled, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_space_shift:disabled { + background-image: url(:/overlay/osk_button_Y_dark_disabled.png); +} + +QDialog#QtSoftwareKeyboardDialog QFrame, +QDialog#QtSoftwareKeyboardDialog QFrame[frameShape="0"], +QDialog#OverlayDialog QFrame, +QDialog#OverlayDialog QFrame[frameShape="0"] { + border-radius: 0px; + border: none; } diff --git a/dist/qt_themes/qdarkstyle_midnight_blue/style.qss b/dist/qt_themes/qdarkstyle_midnight_blue/style.qss index a640374550..64e1ecbcc6 100644 --- a/dist/qt_themes/qdarkstyle_midnight_blue/style.qss +++ b/dist/qt_themes/qdarkstyle_midnight_blue/style.qss @@ -1,10 +1,10 @@ /* --------------------------------------------------------------------------- - Created by the qtsass compiler v0.1.1 + Created by the qtsass compiler v0.1.1 - The definitions are in the "qdarkstyle.qss._styles.scss" module + The definitions are in the "qdarkstyle.qss._styles.scss" module - WARNING! All changes made in this file will be lost! + WARNING! All changes made in this file will be lost! --------------------------------------------------------------------------- */ /* QDarkStyleSheet ----------------------------------------------------------- @@ -15,34 +15,34 @@ It is based on three selecting colors, three greyish (background) colors plus three whitish (foreground) colors. Each set of widgets of the same type have a header like this: - ------------------ - GroupName -------- - ------------------ + ------------------ + GroupName -------- + ------------------ And each widget is separated with a header like this: - QWidgetName ------ + QWidgetName ------ This makes more easy to find and change some css field. The basic configuration is described bellow. - BACKGROUND ----------- + BACKGROUND ----------- - Light (unpressed) - Normal (border, disabled, pressed, checked, toolbars, menus) - Dark (background) + Light (unpressed) + Normal (border, disabled, pressed, checked, toolbars, menus) + Dark (background) - FOREGROUND ----------- + FOREGROUND ----------- - Light (texts/labels) - Normal (not used yet) - Dark (disabled texts) + Light (texts/labels) + Normal (not used yet) + Dark (disabled texts) - SELECTION ------------ + SELECTION ------------ - Light (selection/hover/active) - Normal (selected) - Dark (selected disabled) + Light (selection/hover/active) + Normal (selected) + Dark (selected disabled) If a stranger configuration is required because of a bugfix or anything else, keep the comment on the line above so nobody changes it, including the @@ -2483,3 +2483,404 @@ QWidget#controllerPlayer7, QWidget#controllerPlayer8 { background: transparent; } + +QDialog#QtSoftwareKeyboardDialog, +QStackedWidget#topOSK { + background: rgba(15, 25, 34, .9); +} + +QDialog#OverlayDialog, +QStackedWidget#stackedDialog { + background: rgba(15, 25, 34, .7); +} + +QWidget#boxOSK, +QWidget#lineOSK, +QWidget#richDialog, +QWidget#lineDialog { + background: transparent; +} + +QStackedWidget#bottomOSK, +QWidget#contentDialog, +QWidget#contentRichDialog { + background: rgba(31, 41, 51, 1); +} + +QWidget#contentDialog, +QWidget#contentRichDialog { + margin: 5px; + border-radius: 6px; +} + +QWidget#buttonsDialog, +QWidget#buttonsRichDialog { + margin: 5px; + border-top: 2px solid rgba(255, 255, 255, .9); +} + +QWidget#legendOSKnum { + border-top: 1px solid rgba(255, 255, 255, 1); +} + +QStackedWidget#stackedDialog QTextBrowser QWidget { + background: transparent; +} + +QStackedWidget#stackedDialog QTextBrowser QScrollBar { + background: #19232d; + border: none; +} + +QStackedWidget#stackedDialog QTextBrowser QScrollBar::sub-line, +QStackedWidget#stackedDialog QTextBrowser QScrollBar::add-line { + border-image: none; +} + +QWidget#mainOSK QStackedWidget, +QDialog#OverlayDialog QStackedWidget { + border: none; + padding: 0px; +} + +QWidget#inputOSK { + border-bottom: 3px solid rgba(255, 255, 255, .9); +} + +QWidget#inputOSK QLineEdit { + background: transparent; + border: none; + color: #ccc; + padding: 0px; +} + +QWidget#inputBoxOSK { + border: 2px solid rgba(255, 255, 255, .9); +} + +QWidget#inputBoxOSK QTextEdit { + background: transparent; + border: none; + color: #ccc; +} + +QWidget#richDialog QTextBrowser { + background: transparent; + border: none; + color: #fff; + padding: 35px 65px; +} + +QWidget#lineOSK QLabel#label_header { + color: #f0f0f0; +} + +QWidget#lineOSK QLabel#label_sub, +QWidget#lineOSK QLabel#label_characters, +QWidget#contentDialog QLabel#label_title, +QWidget#contentRichDialog QLabel#label_title_rich, +QWidget#boxOSK QLabel#label_characters_box { + color: #ccc; +} + +QWidget#buttonsDialog, +QWidget#buttonsRichDialog, +QWidget#mainOSK, +QWidget#headerOSK, +QWidget#normalOSK, +QWidget#shiftOSK, +QWidget#numOSK, +QWidget#subOSK, +QWidget#inputOSK, +QWidget#inputBoxOSK, +QWidget#charactersOSK, +QWidget#charactersBoxOSK, +QWidget#legendOSK, +QWidget#legendOSK QWidget, +QWidget#legendOSKshift, +QWidget#legendOSKshift QWidget, +QWidget#legendOSKnum, +QWidget#legendOSKnum QWidget { + background: transparent; +} + +QWidget#contentDialog QLabel, +QWidget#legendOSK QLabel, +QWidget#legendOSKshift QLabel, +QWidget#legendOSKnum QLabel { + color: rgba(255, 255, 255, 1); +} + +QWidget#contentDialog QLabel#label_dialog { + padding: 20px 65px; +} + +QWidget#contentDialog QLabel#label_title, +QWidget#contentRichDialog QLabel#label_title_rich { + padding: 0px 65px; +} + +QDialog#OverlayDialog QPushButton { + color: rgba(1, 253, 201, 1); + background: transparent; + border: none; + padding: 0px; + min-width: 0px; +} + +QDialog#OverlayDialog QPushButton:focus, +QDialog#OverlayDialog QPushButton:hover { + color: rgba(1, 253, 201, 1); + background: rgba(18, 33, 46, 1); + border: 5px solid rgba(56, 189, 225, 1); + border-radius: 6px; + outline: none; +} + +QDialog#OverlayDialog QPushButton:pressed { + color: rgba(240, 240, 240, 1); + background: rgba(110, 122, 130, 1); + border: 5px solid rgba(56, 189, 225, 1); + border-radius: 6px; + outline: none; +} + +QDialog#QtSoftwareKeyboardDialog QLabel { + padding: 0px; +} + +QDialog#QtSoftwareKeyboardDialog QPushButton { + color: rgba(255, 255, 255, 1); + background: rgba(40, 51, 60, 1); + border: 2px solid rgba(31, 41, 51, 1); + border-radius: 0px; + padding: 0px; + min-width: 0px; +} + +QDialog#QtSoftwareKeyboardDialog QPushButton#button_shift, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_return, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_space, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_shift_shift, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_return_shift, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_space_shift { + background: rgba(55, 66, 75, 1); + border: 2px solid rgba(31, 41, 51, 1); +} + +QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace_shift, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace_num { + color: rgba(240, 240, 240, 1); + background: rgba(255, 255, 255, 1); + border: 2px solid rgba(31, 41, 51, 1); +} + +QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok_shift, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok_num { + color: rgba(0, 0, 0, 1); + background: rgba(1, 253, 201, 1); + border: 2px solid rgba(31, 41, 51, 1); +} + +QDialog#QtSoftwareKeyboardDialog QPushButton:focus, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok:focus, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_shift:focus, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_space:focus, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_return:focus, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace:focus, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok_shift:focus, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_shift_shift:focus, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_space_shift:focus, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_return_shift:focus, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace_shift:focus, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok_num:focus, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace_num:focus, + +QDialog#QtSoftwareKeyboardDialog QPushButton:hover, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok:hover, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_shift:hover, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_space:hover, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_return:hover, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace:hover, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok_shift:hover, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_shift_shift:hover, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_space_shift:hover, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_return_shift:hover, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace_shift:hover, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok_num:hover, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace_num:hover { + color: rgba(255, 255, 255, 1); + background: rgba(18, 33, 46, 1); + border: 5px solid rgba(56, 189, 225, 1); + border-radius: 6px; + outline: none; +} + +QDialog#QtSoftwareKeyboardDialog QPushButton:pressed, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok:pressed, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_shift:pressed, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_space:pressed, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_return:pressed, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace:pressed, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok_shift:pressed, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_shift_shift:pressed, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_space_shift:pressed, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_return_shift:pressed, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace_shift:pressed, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok_num:pressed, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace_num:pressed { + color: rgba(240, 240, 240, 1); + background: rgba(110, 122, 130, 1); + border: 5px solid rgba(56, 189, 225, 1); + border-radius: 6px; +} + +QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace_shift, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace_num { + background-position: right top; + background-repeat: no-repeat; + background-origin: content; + background-image: url(:/overlay/osk_button_B_dark.png); + qproperty-icon: url(:/overlay/osk_button_backspace_dark.png); + qproperty-iconSize: 36px; +} + +QDialog#QtSoftwareKeyboardDialog QPushButton#button_space, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_space_shift { + background-position: right top; + background-repeat: no-repeat; + background-origin: content; + background-image: url(:/overlay/osk_button_Y_dark.png); +} + +QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok_shift, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok_num { + color: rgba(44, 44, 44, 1); + background-position: right top; + background-repeat: no-repeat; + background-origin: content; + background-image: url(:/overlay/osk_button_plus_dark.png); +} + +QDialog#QtSoftwareKeyboardDialog QPushButton#button_shift { + background-position: left top; + background-repeat: no-repeat; + background-origin: content; + background-image: url(:/overlay/osk_button_shift_lock_off.png); + qproperty-icon: url(:/overlay/osk_button_shift_dark.png); + qproperty-iconSize: 36px; +} + +QDialog#QtSoftwareKeyboardDialog QPushButton#button_shift_shift { + background-position: left top; + background-repeat: no-repeat; + background-origin: content; + background-image: url(:/overlay/osk_button_shift_lock_off.png); + qproperty-icon: url(:/overlay/osk_button_shift_on_dark.png); + qproperty-iconSize: 36px; +} + +QDialog#QtSoftwareKeyboardDialog QPushButton#button_left_bracket, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_right_bracket, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_left_parenthesis, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_right_parenthesis { + padding-bottom: 7px; +} + +QDialog#QtSoftwareKeyboardDialog QWidget#titleOSK QLabel { + background: transparent; + color: #ccc; +} + +QDialog#QtSoftwareKeyboardDialog QWidget#button_L, +QDialog#QtSoftwareKeyboardDialog QWidget#button_L_shift, +QDialog#QtSoftwareKeyboardDialog QWidget#button_L_num { + image: url(:/overlay/button_L_dark.png); +} + +QDialog#QtSoftwareKeyboardDialog QWidget#arrow_left, +QDialog#QtSoftwareKeyboardDialog QWidget#arrow_left_shift, +QDialog#QtSoftwareKeyboardDialog QWidget#arrow_left_num { + image: url(:/overlay/arrow_left_dark.png); +} + +QDialog#QtSoftwareKeyboardDialog QWidget#button_R, +QDialog#QtSoftwareKeyboardDialog QWidget#button_R_shift, +QDialog#QtSoftwareKeyboardDialog QWidget#button_R_num { + image: url(:/overlay/button_R_dark.png); +} + +QDialog#QtSoftwareKeyboardDialog QWidget#arrow_right, +QDialog#QtSoftwareKeyboardDialog QWidget#arrow_right_shift, +QDialog#QtSoftwareKeyboardDialog QWidget#arrow_right_num { + image: url(:/overlay/arrow_right_dark.png); +} + +QDialog#QtSoftwareKeyboardDialog QWidget#button_press_stick, +QDialog#QtSoftwareKeyboardDialog QWidget#button_press_stick_shift { + image: url(:/overlay/button_press_stick_dark.png); +} + +QDialog#QtSoftwareKeyboardDialog QWidget#button_X, +QDialog#QtSoftwareKeyboardDialog QWidget#button_X_shift, +QDialog#QtSoftwareKeyboardDialog QWidget#button_X_num { + image: url(:/overlay/button_X_dark.png); +} + +QDialog#QtSoftwareKeyboardDialog QWidget#button_A, +QDialog#QtSoftwareKeyboardDialog QWidget#button_A_shift, +QDialog#QtSoftwareKeyboardDialog QWidget#button_A_num { + image: url(:/overlay/button_A_dark.png); +} + +QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok:disabled, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_space:disabled, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_return:disabled, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace:disabled, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok_shift:disabled, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_space_shift:disabled, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_return_shift:disabled, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace_shift:disabled, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok_num:disabled, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace_num:disabled { + color: rgba(144, 144, 144, 1); + background-color: rgba(55, 66, 75, 1); +} + +QDialog#QtSoftwareKeyboardDialog QPushButton#button_at:disabled, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_slash:disabled, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_percent:disabled, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_1:disabled, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_2:disabled, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_3:disabled, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_4:disabled, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_5:disabled, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_6:disabled, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_7:disabled, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_8:disabled, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_9:disabled, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_0:disabled, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_return:disabled { + color: rgba(144, 144, 144, 1); +} + +QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok:disabled, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok_shift:disabled, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok_num:disabled { + background-image: url(:/overlay/osk_button_plus_dark_disabled.png); +} + +QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace:disabled, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace_shift:disabled, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace_num:disabled { + background-image: url(:/overlay/osk_button_B_dark_disabled.png); +} + +QDialog#QtSoftwareKeyboardDialog QPushButton#button_space:disabled, +QDialog#QtSoftwareKeyboardDialog QPushButton#button_space_shift:disabled { + background-image: url(:/overlay/osk_button_Y_dark_disabled.png); +} From aa3adf6c3fc20171abcbd2678ed7ad6b3bd21a8e Mon Sep 17 00:00:00 2001 From: Morph <39850852+Morph1984@users.noreply.github.com> Date: Sat, 20 Mar 2021 07:55:59 -0400 Subject: [PATCH 10/14] input_interpreter: Fix button hold being interpreted incorrectly on init We reset all the button states to 0 except the first index (which has all the buttons as pressed) to prevent a button hold being interpreted as a button that was pressed once on the first poll. --- src/core/frontend/input_interpreter.cpp | 15 ++++++++++++++- src/core/frontend/input_interpreter.h | 3 +++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/core/frontend/input_interpreter.cpp b/src/core/frontend/input_interpreter.cpp index ec5fe660ed..9f6a90e8f1 100644 --- a/src/core/frontend/input_interpreter.cpp +++ b/src/core/frontend/input_interpreter.cpp @@ -12,7 +12,9 @@ InputInterpreter::InputInterpreter(Core::System& system) : npad{system.ServiceManager() .GetService("hid") ->GetAppletResource() - ->GetController(Service::HID::HidController::NPad)} {} + ->GetController(Service::HID::HidController::NPad)} { + ResetButtonStates(); +} InputInterpreter::~InputInterpreter() = default; @@ -25,6 +27,17 @@ void InputInterpreter::PollInput() { button_states[current_index] = button_state; } +void InputInterpreter::ResetButtonStates() { + previous_index = 0; + current_index = 0; + + button_states[0] = 0xFFFFFFFF; + + for (std::size_t i = 1; i < button_states.size(); ++i) { + button_states[i] = 0; + } +} + bool InputInterpreter::IsButtonPressed(HIDButton button) const { return (button_states[current_index] & (1U << static_cast(button))) != 0; } diff --git a/src/core/frontend/input_interpreter.h b/src/core/frontend/input_interpreter.h index 73fc47ffbf..9495e3daf0 100644 --- a/src/core/frontend/input_interpreter.h +++ b/src/core/frontend/input_interpreter.h @@ -66,6 +66,9 @@ public: /// Gets a button state from HID and inserts it into the array of button states. void PollInput(); + /// Resets all the button states to their defaults. + void ResetButtonStates(); + /** * Checks whether the button is pressed. * From 4a5f9f5a6d3bfd8796f52d6c778a8abb3922ff40 Mon Sep 17 00:00:00 2001 From: Morph <39850852+Morph1984@users.noreply.github.com> Date: Fri, 26 Mar 2021 05:26:16 -0400 Subject: [PATCH 11/14] main: Move meta type registration into its own function Moves the existing meta type registration into its own function and adds registration of common integral, floating point and string types. This function is also now called in the constructor of the GMainWindow instead of on starting a game. --- src/yuzu/main.cpp | 58 +++++++++++++++++++++++++++++++++++++++++------ src/yuzu/main.h | 16 +++++++++++-- 2 files changed, 65 insertions(+), 9 deletions(-) diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp index e9d6e74211..422b3cff66 100644 --- a/src/yuzu/main.cpp +++ b/src/yuzu/main.cpp @@ -227,6 +227,8 @@ GMainWindow::GMainWindow() SetDiscordEnabled(UISettings::values.enable_discord_presence); discord_rpc->Update(); + RegisterMetaTypes(); + InitializeWidgets(); InitializeDebugWidgets(); InitializeRecentFileMenuActions(); @@ -375,6 +377,55 @@ GMainWindow::~GMainWindow() { delete render_window; } +void GMainWindow::RegisterMetaTypes() { + // Register integral and floating point types + qRegisterMetaType("u8"); + qRegisterMetaType("u16"); + qRegisterMetaType("u32"); + qRegisterMetaType("u64"); + qRegisterMetaType("u128"); + qRegisterMetaType("s8"); + qRegisterMetaType("s16"); + qRegisterMetaType("s32"); + qRegisterMetaType("s64"); + qRegisterMetaType("f32"); + qRegisterMetaType("f64"); + + // Register string types + qRegisterMetaType("std::string"); + qRegisterMetaType("std::wstring"); + qRegisterMetaType("std::u8string"); + qRegisterMetaType("std::u16string"); + qRegisterMetaType("std::u32string"); + qRegisterMetaType("std::string_view"); + qRegisterMetaType("std::wstring_view"); + qRegisterMetaType("std::u8string_view"); + qRegisterMetaType("std::u16string_view"); + qRegisterMetaType("std::u32string_view"); + + // Register applet types + + // Controller Applet + qRegisterMetaType("Core::Frontend::ControllerParameters"); + + // Software Keyboard Applet + qRegisterMetaType( + "Core::Frontend::KeyboardInitializeParameters"); + qRegisterMetaType( + "Core::Frontend::InlineAppearParameters"); + qRegisterMetaType("Core::Frontend::InlineTextParameters"); + qRegisterMetaType("Service::AM::Applets::SwkbdResult"); + qRegisterMetaType( + "Service::AM::Applets::SwkbdTextCheckResult"); + qRegisterMetaType("Service::AM::Applets::SwkbdReplyType"); + + // Web Browser Applet + qRegisterMetaType("Service::AM::Applets::WebExitReason"); + + // Register loader types + qRegisterMetaType("Core::System::ResultStatus"); +} + void GMainWindow::ControllerSelectorReconfigureControllers( const Core::Frontend::ControllerParameters& parameters) { QtControllerSelectorDialog dialog(this, parameters, input_subsystem.get()); @@ -2166,13 +2217,6 @@ void GMainWindow::OnStartGame() { emu_thread->SetRunning(true); - qRegisterMetaType("Core::Frontend::ControllerParameters"); - qRegisterMetaType("Core::System::ResultStatus"); - qRegisterMetaType("std::string"); - qRegisterMetaType>("std::optional"); - qRegisterMetaType("std::string_view"); - qRegisterMetaType("Service::AM::Applets::WebExitReason"); - connect(emu_thread.get(), &EmuThread::ErrorThrown, this, &GMainWindow::OnCoreError); ui.action_Start->setEnabled(false); diff --git a/src/yuzu/main.h b/src/yuzu/main.h index 6429549ae7..d8849f85b0 100644 --- a/src/yuzu/main.h +++ b/src/yuzu/main.h @@ -37,9 +37,13 @@ enum class GameListRemoveTarget; enum class InstalledEntryType; class GameListPlaceholder; +class QtSoftwareKeyboardDialog; + namespace Core::Frontend { struct ControllerParameters; -struct SoftwareKeyboardParameters; +struct InlineAppearParameters; +struct InlineTextParameters; +struct KeyboardInitializeParameters; } // namespace Core::Frontend namespace DiscordRPC { @@ -57,8 +61,11 @@ class InputSubsystem; } namespace Service::AM::Applets { +enum class SwkbdResult : u32; +enum class SwkbdTextCheckResult : u32; +enum class SwkbdReplyType : u32; enum class WebExitReason : u32; -} +} // namespace Service::AM::Applets enum class EmulatedDirectoryTarget { NAND, @@ -143,6 +150,8 @@ public slots: void OnAppFocusStateChanged(Qt::ApplicationState state); private: + void RegisterMetaTypes(); + void InitializeWidgets(); void InitializeDebugWidgets(); void InitializeRecentFileMenuActions(); @@ -329,6 +338,9 @@ private: // Disables the web applet for the rest of the emulated session bool disable_web_applet{}; + // Applets + QtSoftwareKeyboardDialog* software_keyboard = nullptr; + protected: void dropEvent(QDropEvent* event) override; void dragEnterEvent(QDragEnterEvent* event) override; From 4143675b2df288534e6e1a3f06a87d88dbaba257 Mon Sep 17 00:00:00 2001 From: Morph <39850852+Morph1984@users.noreply.github.com> Date: Sat, 20 Mar 2021 07:57:31 -0400 Subject: [PATCH 12/14] overlay_dialog: Add an overlay text dialog that accepts controller input An OverlayDialog is an interactive dialog that accepts controller input (while a game is running) This dialog attempts to replicate the look and feel of the Nintendo Switch's overlay dialogs and provide some extra features such as embedding HTML/Rich Text content in a QTextBrowser. The OverlayDialog provides 2 modes: one to embed regular text into a QLabel and another to embed HTML/Rich Text content into a QTextBrowser. Co-authored-by: Its-Rei --- src/yuzu/CMakeLists.txt | 3 + src/yuzu/main.cpp | 6 +- src/yuzu/util/overlay_dialog.cpp | 249 +++++++++++++++++++ src/yuzu/util/overlay_dialog.h | 107 ++++++++ src/yuzu/util/overlay_dialog.ui | 404 +++++++++++++++++++++++++++++++ 5 files changed, 768 insertions(+), 1 deletion(-) create mode 100644 src/yuzu/util/overlay_dialog.cpp create mode 100644 src/yuzu/util/overlay_dialog.h create mode 100644 src/yuzu/util/overlay_dialog.ui diff --git a/src/yuzu/CMakeLists.txt b/src/yuzu/CMakeLists.txt index b025ced1c8..3e00ff39ff 100644 --- a/src/yuzu/CMakeLists.txt +++ b/src/yuzu/CMakeLists.txt @@ -143,6 +143,9 @@ add_executable(yuzu uisettings.h util/limitable_input_dialog.cpp util/limitable_input_dialog.h + util/overlay_dialog.cpp + util/overlay_dialog.h + util/overlay_dialog.ui util/sequence_dialog/sequence_dialog.cpp util/sequence_dialog/sequence_dialog.h util/url_request_interceptor.cpp diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp index 422b3cff66..3d4558739e 100644 --- a/src/yuzu/main.cpp +++ b/src/yuzu/main.cpp @@ -101,6 +101,7 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual #include "core/perf_stats.h" #include "core/telemetry_session.h" #include "input_common/main.h" +#include "util/overlay_dialog.h" #include "video_core/gpu.h" #include "video_core/shader_notify.h" #include "yuzu/about_dialog.h" @@ -2266,7 +2267,10 @@ void GMainWindow::OnExecuteProgram(std::size_t program_index) { } void GMainWindow::ErrorDisplayDisplayError(QString body) { - QMessageBox::critical(this, tr("Error Display"), body); + OverlayDialog dialog(render_window, Core::System::GetInstance(), body, QString{}, tr("OK"), + Qt::AlignLeft | Qt::AlignVCenter); + dialog.exec(); + emit ErrorDisplayFinished(); } diff --git a/src/yuzu/util/overlay_dialog.cpp b/src/yuzu/util/overlay_dialog.cpp new file mode 100644 index 0000000000..95b1485450 --- /dev/null +++ b/src/yuzu/util/overlay_dialog.cpp @@ -0,0 +1,249 @@ +// Copyright 2021 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include + +#include "core/core.h" +#include "core/frontend/input_interpreter.h" +#include "ui_overlay_dialog.h" +#include "yuzu/util/overlay_dialog.h" + +namespace { + +constexpr float BASE_TITLE_FONT_SIZE = 14.0f; +constexpr float BASE_FONT_SIZE = 18.0f; +constexpr float BASE_WIDTH = 1280.0f; +constexpr float BASE_HEIGHT = 720.0f; + +} // Anonymous namespace + +OverlayDialog::OverlayDialog(QWidget* parent, Core::System& system, const QString& title_text, + const QString& body_text, const QString& left_button_text, + const QString& right_button_text, Qt::Alignment alignment, + bool use_rich_text_) + : QDialog(parent), ui{std::make_unique()}, use_rich_text{use_rich_text_} { + ui->setupUi(this); + + setWindowFlags(Qt::Dialog | Qt::FramelessWindowHint | Qt::WindowTitleHint | + Qt::WindowSystemMenuHint | Qt::CustomizeWindowHint); + setWindowModality(Qt::WindowModal); + setAttribute(Qt::WA_TranslucentBackground); + + if (use_rich_text) { + InitializeRichTextDialog(title_text, body_text, left_button_text, right_button_text, + alignment); + } else { + InitializeRegularTextDialog(title_text, body_text, left_button_text, right_button_text, + alignment); + } + + MoveAndResizeWindow(); + + // TODO (Morph): Remove this when InputInterpreter no longer relies on the HID backend + if (system.IsPoweredOn()) { + input_interpreter = std::make_unique(system); + + StartInputThread(); + } +} + +OverlayDialog::~OverlayDialog() { + StopInputThread(); +} + +void OverlayDialog::InitializeRegularTextDialog(const QString& title_text, const QString& body_text, + const QString& left_button_text, + const QString& right_button_text, + Qt::Alignment alignment) { + ui->stackedDialog->setCurrentIndex(0); + + ui->label_title->setText(title_text); + ui->label_dialog->setText(body_text); + ui->button_cancel->setText(left_button_text); + ui->button_ok_label->setText(right_button_text); + + ui->label_dialog->setAlignment(alignment); + + if (title_text.isEmpty()) { + ui->label_title->hide(); + ui->verticalLayout_2->setStretch(0, 0); + ui->verticalLayout_2->setStretch(1, 219); + ui->verticalLayout_2->setStretch(2, 82); + } + + if (left_button_text.isEmpty()) { + ui->button_cancel->hide(); + ui->button_cancel->setEnabled(false); + } + + if (right_button_text.isEmpty()) { + ui->button_ok_label->hide(); + ui->button_ok_label->setEnabled(false); + } + + connect( + ui->button_cancel, &QPushButton::clicked, this, + [this](bool) { + StopInputThread(); + QDialog::reject(); + }, + Qt::QueuedConnection); + connect( + ui->button_ok_label, &QPushButton::clicked, this, + [this](bool) { + StopInputThread(); + QDialog::accept(); + }, + Qt::QueuedConnection); +} + +void OverlayDialog::InitializeRichTextDialog(const QString& title_text, const QString& body_text, + const QString& left_button_text, + const QString& right_button_text, + Qt::Alignment alignment) { + ui->stackedDialog->setCurrentIndex(1); + + ui->label_title_rich->setText(title_text); + ui->text_browser_dialog->setText(body_text); + ui->button_cancel_rich->setText(left_button_text); + ui->button_ok_rich->setText(right_button_text); + + // TODO (Morph/Rei): Replace this with something that works better + ui->text_browser_dialog->setAlignment(alignment); + + if (title_text.isEmpty()) { + ui->label_title_rich->hide(); + ui->verticalLayout_3->setStretch(0, 0); + ui->verticalLayout_3->setStretch(1, 438); + ui->verticalLayout_3->setStretch(2, 82); + } + + if (left_button_text.isEmpty()) { + ui->button_cancel_rich->hide(); + ui->button_cancel_rich->setEnabled(false); + } + + if (right_button_text.isEmpty()) { + ui->button_ok_rich->hide(); + ui->button_ok_rich->setEnabled(false); + } + + connect( + ui->button_cancel_rich, &QPushButton::clicked, this, + [this](bool) { + StopInputThread(); + QDialog::reject(); + }, + Qt::QueuedConnection); + connect( + ui->button_ok_rich, &QPushButton::clicked, this, + [this](bool) { + StopInputThread(); + QDialog::accept(); + }, + Qt::QueuedConnection); +} + +void OverlayDialog::MoveAndResizeWindow() { + const auto pos = parentWidget()->mapToGlobal(parentWidget()->rect().topLeft()); + const auto width = static_cast(parentWidget()->width()); + const auto height = static_cast(parentWidget()->height()); + + // High DPI + const float dpi_scale = qApp->screenAt(pos)->logicalDotsPerInch() / 96.0f; + + const auto title_text_font_size = BASE_TITLE_FONT_SIZE * (height / BASE_HEIGHT) / dpi_scale; + const auto body_text_font_size = + BASE_FONT_SIZE * (((width / BASE_WIDTH) + (height / BASE_HEIGHT)) / 2.0f) / dpi_scale; + const auto button_text_font_size = BASE_FONT_SIZE * (height / BASE_HEIGHT) / dpi_scale; + + QFont title_text_font(QStringLiteral("MS Shell Dlg 2"), title_text_font_size, QFont::Normal); + QFont body_text_font(QStringLiteral("MS Shell Dlg 2"), body_text_font_size, QFont::Normal); + QFont button_text_font(QStringLiteral("MS Shell Dlg 2"), button_text_font_size, QFont::Normal); + + if (use_rich_text) { + ui->label_title_rich->setFont(title_text_font); + ui->text_browser_dialog->setFont(body_text_font); + ui->button_cancel_rich->setFont(button_text_font); + ui->button_ok_rich->setFont(button_text_font); + } else { + ui->label_title->setFont(title_text_font); + ui->label_dialog->setFont(body_text_font); + ui->button_cancel->setFont(button_text_font); + ui->button_ok_label->setFont(button_text_font); + } + + QDialog::move(pos); + QDialog::resize(width, height); +} + +template +void OverlayDialog::HandleButtonPressedOnce() { + const auto f = [this](HIDButton button) { + if (input_interpreter->IsButtonPressedOnce(button)) { + TranslateButtonPress(button); + } + }; + + (f(T), ...); +} + +void OverlayDialog::TranslateButtonPress(HIDButton button) { + QPushButton* left_button = use_rich_text ? ui->button_cancel_rich : ui->button_cancel; + QPushButton* right_button = use_rich_text ? ui->button_ok_rich : ui->button_ok_label; + + // TODO (Morph): Handle QTextBrowser text scrolling + // TODO (Morph): focusPrevious/NextChild() doesn't work well with the rich text dialog, fix it + + switch (button) { + case HIDButton::A: + case HIDButton::B: + if (left_button->hasFocus()) { + left_button->click(); + } else if (right_button->hasFocus()) { + right_button->click(); + } + break; + case HIDButton::DLeft: + case HIDButton::LStickLeft: + focusPreviousChild(); + break; + case HIDButton::DRight: + case HIDButton::LStickRight: + focusNextChild(); + break; + default: + break; + } +} + +void OverlayDialog::StartInputThread() { + if (input_thread_running) { + return; + } + + input_thread_running = true; + + input_thread = std::thread(&OverlayDialog::InputThread, this); +} + +void OverlayDialog::StopInputThread() { + input_thread_running = false; + + if (input_thread.joinable()) { + input_thread.join(); + } +} + +void OverlayDialog::InputThread() { + while (input_thread_running) { + input_interpreter->PollInput(); + + HandleButtonPressedOnce(); + + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + } +} diff --git a/src/yuzu/util/overlay_dialog.h b/src/yuzu/util/overlay_dialog.h new file mode 100644 index 0000000000..e8c388bd01 --- /dev/null +++ b/src/yuzu/util/overlay_dialog.h @@ -0,0 +1,107 @@ +// Copyright 2021 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include +#include + +#include + +#include "common/common_types.h" + +enum class HIDButton : u8; + +class InputInterpreter; + +namespace Core { +class System; +} + +namespace Ui { +class OverlayDialog; +} + +/** + * An OverlayDialog is an interactive dialog that accepts controller input (while a game is running) + * This dialog attempts to replicate the look and feel of the Nintendo Switch's overlay dialogs and + * provide some extra features such as embedding HTML/Rich Text content in a QTextBrowser. + * The OverlayDialog provides 2 modes: one to embed regular text into a QLabel and another to embed + * HTML/Rich Text content into a QTextBrowser. + */ +class OverlayDialog final : public QDialog { + Q_OBJECT + +public: + explicit OverlayDialog(QWidget* parent, Core::System& system, const QString& title_text, + const QString& body_text, const QString& left_button_text, + const QString& right_button_text, + Qt::Alignment alignment = Qt::AlignCenter, bool use_rich_text_ = false); + ~OverlayDialog() override; + +private: + /** + * Initializes a text dialog with a QLabel storing text. + * Only use this for short text as the dialog buttons would be squashed with longer text. + * + * @param title_text Title text to be displayed + * @param body_text Main text to be displayed + * @param left_button_text Left button text. If empty, the button is hidden and disabled + * @param right_button_text Right button text. If empty, the button is hidden and disabled + * @param alignment Main text alignment + */ + void InitializeRegularTextDialog(const QString& title_text, const QString& body_text, + const QString& left_button_text, + const QString& right_button_text, Qt::Alignment alignment); + + /** + * Initializes a text dialog with a QTextBrowser storing text. + * This is ideal for longer text or rich text content. A scrollbar is shown for longer text. + * + * @param title_text Title text to be displayed + * @param body_text Main text to be displayed + * @param left_button_text Left button text. If empty, the button is hidden and disabled + * @param right_button_text Right button text. If empty, the button is hidden and disabled + * @param alignment Main text alignment + */ + void InitializeRichTextDialog(const QString& title_text, const QString& body_text, + const QString& left_button_text, const QString& right_button_text, + Qt::Alignment alignment); + + /// Moves and resizes the dialog to be fully overlayed on top of the parent window. + void MoveAndResizeWindow(); + + /** + * Handles button presses and converts them into keyboard input. + * + * @tparam HIDButton The list of buttons that can be converted into keyboard input. + */ + template + void HandleButtonPressedOnce(); + + /** + * Translates a button press to focus or click either the left or right buttons. + * + * @param button The button press to process. + */ + void TranslateButtonPress(HIDButton button); + + void StartInputThread(); + void StopInputThread(); + + /// The thread where input is being polled and processed. + void InputThread(); + + std::unique_ptr ui; + + bool use_rich_text; + + std::unique_ptr input_interpreter; + + std::thread input_thread; + + std::atomic input_thread_running{}; +}; diff --git a/src/yuzu/util/overlay_dialog.ui b/src/yuzu/util/overlay_dialog.ui new file mode 100644 index 0000000000..278e2f219e --- /dev/null +++ b/src/yuzu/util/overlay_dialog.ui @@ -0,0 +1,404 @@ + + + OverlayDialog + + + + 0 + 0 + 1280 + 720 + + + + Dialog + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 0 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 14 + + + + Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft + + + + + + + + 18 + + + + Qt::AlignCenter + + + true + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 18 + + + + Cancel + + + + + + + + 0 + 0 + + + + + 18 + + + + OK + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 14 + + + + + + + Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft + + + + + + + + 18 + + + + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> +<html><head><meta name="qrichtext" content="1" /><style type="text/css"> +p, li { white-space: pre-wrap; } +</style></head><body style=" font-family:'MS Shell Dlg 2'; font-size:18pt; font-weight:400; font-style:normal;"> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p></body></html> + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 18 + + + + Cancel + + + + + + + + 0 + 0 + + + + + 18 + + + + OK + + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + + From b45930a0edbf762310f85fafa3724dc4766c2197 Mon Sep 17 00:00:00 2001 From: Morph <39850852+Morph1984@users.noreply.github.com> Date: Fri, 26 Mar 2021 06:07:27 -0400 Subject: [PATCH 13/14] error: Make the error code as the title text of the OverlayDialog Co-authored-by: Its-Rei --- src/yuzu/applets/error.cpp | 22 ++++++++++++---------- src/yuzu/applets/error.h | 2 +- src/yuzu/main.cpp | 6 +++--- src/yuzu/main.h | 2 +- 4 files changed, 17 insertions(+), 15 deletions(-) diff --git a/src/yuzu/applets/error.cpp b/src/yuzu/applets/error.cpp index 8ee03ddb3d..085688cd47 100644 --- a/src/yuzu/applets/error.cpp +++ b/src/yuzu/applets/error.cpp @@ -19,11 +19,11 @@ QtErrorDisplay::~QtErrorDisplay() = default; void QtErrorDisplay::ShowError(ResultCode error, std::function finished) const { callback = std::move(finished); emit MainWindowDisplayError( - tr("An error has occurred.\nPlease try again or contact the developer of the " - "software.\n\nError Code: %1-%2 (0x%3)") + tr("Error Code: %1-%2 (0x%3)") .arg(static_cast(error.module.Value()) + 2000, 4, 10, QChar::fromLatin1('0')) .arg(error.description, 4, 10, QChar::fromLatin1('0')) - .arg(error.raw, 8, 16, QChar::fromLatin1('0'))); + .arg(error.raw, 8, 16, QChar::fromLatin1('0')), + tr("An error has occurred.\nPlease try again or contact the developer of the software.")); } void QtErrorDisplay::ShowErrorWithTimestamp(ResultCode error, std::chrono::seconds time, @@ -32,13 +32,14 @@ void QtErrorDisplay::ShowErrorWithTimestamp(ResultCode error, std::chrono::secon const QDateTime date_time = QDateTime::fromSecsSinceEpoch(time.count()); emit MainWindowDisplayError( - tr("An error occurred on %1 at %2.\nPlease try again or contact the " - "developer of the software.\n\nError Code: %3-%4 (0x%5)") - .arg(date_time.toString(QStringLiteral("dddd, MMMM d, yyyy"))) - .arg(date_time.toString(QStringLiteral("h:mm:ss A"))) + tr("Error Code: %1-%2 (0x%3)") .arg(static_cast(error.module.Value()) + 2000, 4, 10, QChar::fromLatin1('0')) .arg(error.description, 4, 10, QChar::fromLatin1('0')) - .arg(error.raw, 8, 16, QChar::fromLatin1('0'))); + .arg(error.raw, 8, 16, QChar::fromLatin1('0')), + tr("An error occurred on %1 at %2.\nPlease try again or contact the developer of the " + "software.") + .arg(date_time.toString(QStringLiteral("dddd, MMMM d, yyyy"))) + .arg(date_time.toString(QStringLiteral("h:mm:ss A")))); } void QtErrorDisplay::ShowCustomErrorText(ResultCode error, std::string dialog_text, @@ -46,10 +47,11 @@ void QtErrorDisplay::ShowCustomErrorText(ResultCode error, std::string dialog_te std::function finished) const { callback = std::move(finished); emit MainWindowDisplayError( - tr("An error has occurred.\nError Code: %1-%2 (0x%3)\n\n%4\n\n%5") + tr("Error Code: %1-%2 (0x%3)") .arg(static_cast(error.module.Value()) + 2000, 4, 10, QChar::fromLatin1('0')) .arg(error.description, 4, 10, QChar::fromLatin1('0')) - .arg(error.raw, 8, 16, QChar::fromLatin1('0')) + .arg(error.raw, 8, 16, QChar::fromLatin1('0')), + tr("An error has occurred.\n\n%1\n\n%2") .arg(QString::fromStdString(dialog_text)) .arg(QString::fromStdString(fullscreen_text))); } diff --git a/src/yuzu/applets/error.h b/src/yuzu/applets/error.h index b0932d8957..8bd895a32d 100644 --- a/src/yuzu/applets/error.h +++ b/src/yuzu/applets/error.h @@ -24,7 +24,7 @@ public: std::function finished) const override; signals: - void MainWindowDisplayError(QString error) const; + void MainWindowDisplayError(QString error_code, QString error_text) const; private: void MainWindowFinishedError(); diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp index 3d4558739e..ce83dee278 100644 --- a/src/yuzu/main.cpp +++ b/src/yuzu/main.cpp @@ -2266,9 +2266,9 @@ void GMainWindow::OnExecuteProgram(std::size_t program_index) { BootGame(last_filename_booted, program_index); } -void GMainWindow::ErrorDisplayDisplayError(QString body) { - OverlayDialog dialog(render_window, Core::System::GetInstance(), body, QString{}, tr("OK"), - Qt::AlignLeft | Qt::AlignVCenter); +void GMainWindow::ErrorDisplayDisplayError(QString error_code, QString error_text) { + OverlayDialog dialog(render_window, Core::System::GetInstance(), error_code, error_text, + QString{}, tr("OK"), Qt::AlignLeft | Qt::AlignVCenter); dialog.exec(); emit ErrorDisplayFinished(); diff --git a/src/yuzu/main.h b/src/yuzu/main.h index d8849f85b0..4c8a879d2c 100644 --- a/src/yuzu/main.h +++ b/src/yuzu/main.h @@ -143,7 +143,7 @@ public slots: void OnExecuteProgram(std::size_t program_index); void ControllerSelectorReconfigureControllers( const Core::Frontend::ControllerParameters& parameters); - void ErrorDisplayDisplayError(QString body); + void ErrorDisplayDisplayError(QString error_code, QString error_text); void ProfileSelectorSelectProfile(); void WebBrowserOpenWebPage(std::string_view main_url, std::string_view additional_args, bool is_local); From 7eff91ff20765ba4e7f94a92de6fc0ffa2fc4f2f Mon Sep 17 00:00:00 2001 From: Morph <39850852+Morph1984@users.noreply.github.com> Date: Sat, 27 Mar 2021 15:15:32 -0400 Subject: [PATCH 14/14] applets/swkbd: Implement the Qt Software Keyboard frontend The Qt Software Keyboard frontend attempts to mimic the software keyboard rendered by the Nintendo Switch. This frontend implements multiple keyboard types, such as the normal software keyboard, the numeric pad software keyboard and the inline software keyboard. Keyboard and controller input is also supported in this frontend. Keyboard input is handled as native keyboard input, and so the on-screen keyboard cannot be navigated with the keyboard arrow keys as the arrow keys are used to move the text cursor. Controller input is translated into mouse hover movements on the onscreen keyboard or their respective button actions (B for backspace, A for entering the selected button, L/R for moving the text cursor, etc). The text check dialogs can also be confirmed with controller input through the use of the OverlayDialog Massive thanks to Rei for creating all the UI for the various keyboards and OverlayDialog. This would not have been possible without his excellent work. Co-authored-by: Its-Rei --- src/yuzu/CMakeLists.txt | 1 + src/yuzu/applets/software_keyboard.cpp | 1635 ++++++++++- src/yuzu/applets/software_keyboard.h | 267 +- src/yuzu/applets/software_keyboard.ui | 3503 ++++++++++++++++++++++++ src/yuzu/main.cpp | 112 + src/yuzu/main.h | 14 + 6 files changed, 5518 insertions(+), 14 deletions(-) create mode 100644 src/yuzu/applets/software_keyboard.ui diff --git a/src/yuzu/CMakeLists.txt b/src/yuzu/CMakeLists.txt index 3e00ff39ff..cc0790e077 100644 --- a/src/yuzu/CMakeLists.txt +++ b/src/yuzu/CMakeLists.txt @@ -18,6 +18,7 @@ add_executable(yuzu applets/profile_select.h applets/software_keyboard.cpp applets/software_keyboard.h + applets/software_keyboard.ui applets/web_browser.cpp applets/web_browser.h bootmanager.cpp diff --git a/src/yuzu/applets/software_keyboard.cpp b/src/yuzu/applets/software_keyboard.cpp index da0fed774e..fd3368479d 100644 --- a/src/yuzu/applets/software_keyboard.cpp +++ b/src/yuzu/applets/software_keyboard.cpp @@ -1,18 +1,1641 @@ -// Copyright 2018 yuzu Emulator Project +// Copyright 2021 yuzu Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. +#include +#include +#include + +#include "common/logging/log.h" +#include "common/settings.h" +#include "common/string_util.h" +#include "core/core.h" +#include "core/frontend/input_interpreter.h" +#include "ui_software_keyboard.h" #include "yuzu/applets/software_keyboard.h" #include "yuzu/main.h" +#include "yuzu/util/overlay_dialog.h" -QtSoftwareKeyboardValidator::QtSoftwareKeyboardValidator() {} +namespace { -QValidator::State QtSoftwareKeyboardValidator::validate(QString& input, int& pos) const {} +using namespace Service::AM::Applets; -QtSoftwareKeyboardDialog::QtSoftwareKeyboardDialog(QWidget* parent) : QDialog(parent) {} +constexpr float BASE_HEADER_FONT_SIZE = 23.0f; +constexpr float BASE_SUB_FONT_SIZE = 17.0f; +constexpr float BASE_EDITOR_FONT_SIZE = 26.0f; +constexpr float BASE_CHAR_BUTTON_FONT_SIZE = 28.0f; +constexpr float BASE_LABEL_BUTTON_FONT_SIZE = 18.0f; +constexpr float BASE_ICON_BUTTON_SIZE = 36.0f; +[[maybe_unused]] constexpr float BASE_WIDTH = 1280.0f; +constexpr float BASE_HEIGHT = 720.0f; -QtSoftwareKeyboardDialog::~QtSoftwareKeyboardDialog() = default; +} // Anonymous namespace -QtSoftwareKeyboard::QtSoftwareKeyboard(GMainWindow& main_window) {} +QtSoftwareKeyboardDialog::QtSoftwareKeyboardDialog( + QWidget* parent, Core::System& system_, bool is_inline_, + Core::Frontend::KeyboardInitializeParameters initialize_parameters_) + : QDialog(parent), ui{std::make_unique()}, system{system_}, + is_inline{is_inline_}, initialize_parameters{std::move(initialize_parameters_)} { + ui->setupUi(this); + + setWindowFlags(Qt::Dialog | Qt::FramelessWindowHint | Qt::WindowTitleHint | + Qt::WindowSystemMenuHint | Qt::CustomizeWindowHint); + setWindowModality(Qt::WindowModal); + setAttribute(Qt::WA_DeleteOnClose); + setAttribute(Qt::WA_TranslucentBackground); + + keyboard_buttons = {{ + {{ + { + ui->button_1, + ui->button_2, + ui->button_3, + ui->button_4, + ui->button_5, + ui->button_6, + ui->button_7, + ui->button_8, + ui->button_9, + ui->button_0, + ui->button_minus, + ui->button_backspace, + }, + { + ui->button_q, + ui->button_w, + ui->button_e, + ui->button_r, + ui->button_t, + ui->button_y, + ui->button_u, + ui->button_i, + ui->button_o, + ui->button_p, + ui->button_slash, + ui->button_return, + }, + { + ui->button_a, + ui->button_s, + ui->button_d, + ui->button_f, + ui->button_g, + ui->button_h, + ui->button_j, + ui->button_k, + ui->button_l, + ui->button_colon, + ui->button_apostrophe, + ui->button_return, + }, + { + ui->button_z, + ui->button_x, + ui->button_c, + ui->button_v, + ui->button_b, + ui->button_n, + ui->button_m, + ui->button_comma, + ui->button_dot, + ui->button_question, + ui->button_exclamation, + ui->button_ok, + }, + { + ui->button_shift, + ui->button_shift, + ui->button_space, + ui->button_space, + ui->button_space, + ui->button_space, + ui->button_space, + ui->button_space, + ui->button_space, + ui->button_space, + ui->button_space, + ui->button_ok, + }, + }}, + {{ + { + ui->button_hash, + ui->button_left_bracket, + ui->button_right_bracket, + ui->button_dollar, + ui->button_percent, + ui->button_circumflex, + ui->button_ampersand, + ui->button_asterisk, + ui->button_left_parenthesis, + ui->button_right_parenthesis, + ui->button_underscore, + ui->button_backspace_shift, + }, + { + ui->button_q_shift, + ui->button_w_shift, + ui->button_e_shift, + ui->button_r_shift, + ui->button_t_shift, + ui->button_y_shift, + ui->button_u_shift, + ui->button_i_shift, + ui->button_o_shift, + ui->button_p_shift, + ui->button_at, + ui->button_return_shift, + }, + { + ui->button_a_shift, + ui->button_s_shift, + ui->button_d_shift, + ui->button_f_shift, + ui->button_g_shift, + ui->button_h_shift, + ui->button_j_shift, + ui->button_k_shift, + ui->button_l_shift, + ui->button_semicolon, + ui->button_quotation, + ui->button_return_shift, + }, + { + ui->button_z_shift, + ui->button_x_shift, + ui->button_c_shift, + ui->button_v_shift, + ui->button_b_shift, + ui->button_n_shift, + ui->button_m_shift, + ui->button_less_than, + ui->button_greater_than, + ui->button_plus, + ui->button_equal, + ui->button_ok_shift, + }, + { + ui->button_shift_shift, + ui->button_shift_shift, + ui->button_space_shift, + ui->button_space_shift, + ui->button_space_shift, + ui->button_space_shift, + ui->button_space_shift, + ui->button_space_shift, + ui->button_space_shift, + ui->button_space_shift, + ui->button_space_shift, + ui->button_ok_shift, + }, + }}, + }}; + + numberpad_buttons = {{ + { + ui->button_1_num, + ui->button_2_num, + ui->button_3_num, + ui->button_backspace_num, + }, + { + ui->button_4_num, + ui->button_5_num, + ui->button_6_num, + ui->button_ok_num, + }, + { + ui->button_7_num, + ui->button_8_num, + ui->button_9_num, + ui->button_ok_num, + }, + { + nullptr, + ui->button_0_num, + nullptr, + ui->button_ok_num, + }, + }}; + + all_buttons = { + ui->button_1, + ui->button_2, + ui->button_3, + ui->button_4, + ui->button_5, + ui->button_6, + ui->button_7, + ui->button_8, + ui->button_9, + ui->button_0, + ui->button_minus, + ui->button_backspace, + ui->button_q, + ui->button_w, + ui->button_e, + ui->button_r, + ui->button_t, + ui->button_y, + ui->button_u, + ui->button_i, + ui->button_o, + ui->button_p, + ui->button_slash, + ui->button_return, + ui->button_a, + ui->button_s, + ui->button_d, + ui->button_f, + ui->button_g, + ui->button_h, + ui->button_j, + ui->button_k, + ui->button_l, + ui->button_colon, + ui->button_apostrophe, + ui->button_z, + ui->button_x, + ui->button_c, + ui->button_v, + ui->button_b, + ui->button_n, + ui->button_m, + ui->button_comma, + ui->button_dot, + ui->button_question, + ui->button_exclamation, + ui->button_ok, + ui->button_shift, + ui->button_space, + ui->button_hash, + ui->button_left_bracket, + ui->button_right_bracket, + ui->button_dollar, + ui->button_percent, + ui->button_circumflex, + ui->button_ampersand, + ui->button_asterisk, + ui->button_left_parenthesis, + ui->button_right_parenthesis, + ui->button_underscore, + ui->button_backspace_shift, + ui->button_q_shift, + ui->button_w_shift, + ui->button_e_shift, + ui->button_r_shift, + ui->button_t_shift, + ui->button_y_shift, + ui->button_u_shift, + ui->button_i_shift, + ui->button_o_shift, + ui->button_p_shift, + ui->button_at, + ui->button_return_shift, + ui->button_a_shift, + ui->button_s_shift, + ui->button_d_shift, + ui->button_f_shift, + ui->button_g_shift, + ui->button_h_shift, + ui->button_j_shift, + ui->button_k_shift, + ui->button_l_shift, + ui->button_semicolon, + ui->button_quotation, + ui->button_z_shift, + ui->button_x_shift, + ui->button_c_shift, + ui->button_v_shift, + ui->button_b_shift, + ui->button_n_shift, + ui->button_m_shift, + ui->button_less_than, + ui->button_greater_than, + ui->button_plus, + ui->button_equal, + ui->button_ok_shift, + ui->button_shift_shift, + ui->button_space_shift, + ui->button_1_num, + ui->button_2_num, + ui->button_3_num, + ui->button_backspace_num, + ui->button_4_num, + ui->button_5_num, + ui->button_6_num, + ui->button_ok_num, + ui->button_7_num, + ui->button_8_num, + ui->button_9_num, + ui->button_0_num, + }; + + SetupMouseHover(); + + if (!initialize_parameters.ok_text.empty()) { + ui->button_ok->setText(QString::fromStdU16String(initialize_parameters.ok_text)); + } + + ui->label_header->setText(QString::fromStdU16String(initialize_parameters.header_text)); + ui->label_sub->setText(QString::fromStdU16String(initialize_parameters.sub_text)); + + current_text = initialize_parameters.initial_text; + cursor_position = initialize_parameters.initial_cursor_position; + + SetTextDrawType(); + + for (auto* button : all_buttons) { + connect(button, &QPushButton::clicked, this, [this, button](bool) { + if (is_inline) { + InlineKeyboardButtonClicked(button); + } else { + NormalKeyboardButtonClicked(button); + } + }); + } + + // TODO (Morph): Remove this when InputInterpreter no longer relies on the HID backend + if (system.IsPoweredOn()) { + input_interpreter = std::make_unique(system); + } +} + +QtSoftwareKeyboardDialog::~QtSoftwareKeyboardDialog() { + StopInputThread(); +} + +void QtSoftwareKeyboardDialog::ShowNormalKeyboard(QPoint pos, QSize size) { + if (isVisible()) { + return; + } + + MoveAndResizeWindow(pos, size); + + SetKeyboardType(); + SetPasswordMode(); + SetControllerImage(); + DisableKeyboardButtons(); + SetBackspaceOkEnabled(); + + open(); +} + +void QtSoftwareKeyboardDialog::ShowTextCheckDialog( + Service::AM::Applets::SwkbdTextCheckResult text_check_result, + std::u16string text_check_message) { + switch (text_check_result) { + case SwkbdTextCheckResult::Success: + case SwkbdTextCheckResult::Silent: + default: + break; + case SwkbdTextCheckResult::Failure: { + StopInputThread(); + + OverlayDialog dialog(this, system, QString{}, QString::fromStdU16String(text_check_message), + QString{}, tr("OK"), Qt::AlignCenter); + dialog.exec(); + + StartInputThread(); + break; + } + case SwkbdTextCheckResult::Confirm: { + StopInputThread(); + + OverlayDialog dialog(this, system, QString{}, QString::fromStdU16String(text_check_message), + tr("Cancel"), tr("OK"), Qt::AlignCenter); + if (dialog.exec() == QDialog::Accepted) { + emit SubmitNormalText(SwkbdResult::Ok, current_text); + break; + } + + StartInputThread(); + break; + } + } +} + +void QtSoftwareKeyboardDialog::ShowInlineKeyboard( + Core::Frontend::InlineAppearParameters appear_parameters, QPoint pos, QSize size) { + MoveAndResizeWindow(pos, size); + + ui->topOSK->setStyleSheet(QStringLiteral("background: rgba(0, 0, 0, 0);")); + + ui->headerOSK->hide(); + ui->subOSK->hide(); + ui->inputOSK->hide(); + ui->charactersOSK->hide(); + ui->inputBoxOSK->hide(); + ui->charactersBoxOSK->hide(); + + initialize_parameters.max_text_length = appear_parameters.max_text_length; + initialize_parameters.min_text_length = appear_parameters.min_text_length; + initialize_parameters.type = appear_parameters.type; + initialize_parameters.key_disable_flags = appear_parameters.key_disable_flags; + initialize_parameters.enable_backspace_button = appear_parameters.enable_backspace_button; + initialize_parameters.enable_return_button = appear_parameters.enable_return_button; + initialize_parameters.disable_cancel_button = initialize_parameters.disable_cancel_button; + + SetKeyboardType(); + SetControllerImage(); + DisableKeyboardButtons(); + SetBackspaceOkEnabled(); + + open(); +} + +void QtSoftwareKeyboardDialog::HideInlineKeyboard() { + StopInputThread(); + QDialog::hide(); +} + +void QtSoftwareKeyboardDialog::InlineTextChanged( + Core::Frontend::InlineTextParameters text_parameters) { + current_text = text_parameters.input_text; + cursor_position = text_parameters.cursor_position; + + SetBackspaceOkEnabled(); +} + +void QtSoftwareKeyboardDialog::ExitKeyboard() { + StopInputThread(); + QDialog::done(QDialog::Accepted); +} + +void QtSoftwareKeyboardDialog::open() { + QDialog::open(); + + row = 0; + column = 0; + + const auto* const curr_button = + keyboard_buttons[static_cast(bottom_osk_index)][row][column]; + + // This is a workaround for setFocus() randomly not showing focus in the UI + QCursor::setPos(curr_button->mapToGlobal(curr_button->rect().center())); + + StartInputThread(); +} + +void QtSoftwareKeyboardDialog::reject() { + // Pressing the ESC key in a dialog calls QDialog::reject(). + // We will override this behavior to the "Cancel" action on the software keyboard. + if (is_inline) { + emit SubmitInlineText(SwkbdReplyType::DecidedCancel, current_text, cursor_position); + } else { + emit SubmitNormalText(SwkbdResult::Cancel, current_text); + } +} + +void QtSoftwareKeyboardDialog::keyPressEvent(QKeyEvent* event) { + if (!is_inline) { + QDialog::keyPressEvent(event); + return; + } + + const auto entered_key = event->key(); + + switch (entered_key) { + case Qt::Key_Escape: + QDialog::keyPressEvent(event); + return; + case Qt::Key_Backspace: + switch (bottom_osk_index) { + case BottomOSKIndex::LowerCase: + ui->button_backspace->click(); + break; + case BottomOSKIndex::UpperCase: + ui->button_backspace_shift->click(); + break; + case BottomOSKIndex::NumberPad: + ui->button_backspace_num->click(); + break; + default: + break; + } + return; + case Qt::Key_Return: + switch (bottom_osk_index) { + case BottomOSKIndex::LowerCase: + ui->button_ok->click(); + break; + case BottomOSKIndex::UpperCase: + ui->button_ok_shift->click(); + break; + case BottomOSKIndex::NumberPad: + ui->button_ok_num->click(); + break; + default: + break; + } + return; + case Qt::Key_Left: + MoveTextCursorDirection(Direction::Left); + return; + case Qt::Key_Right: + MoveTextCursorDirection(Direction::Right); + return; + default: + break; + } + + const auto entered_text = event->text(); + + if (entered_text.isEmpty()) { + return; + } + + InlineTextInsertString(entered_text.toStdU16String()); +} + +void QtSoftwareKeyboardDialog::MoveAndResizeWindow(QPoint pos, QSize size) { + QDialog::move(pos); + QDialog::resize(size); + + // High DPI + const float dpi_scale = qApp->screenAt(pos)->logicalDotsPerInch() / 96.0f; + + RescaleKeyboardElements(size.width(), size.height(), dpi_scale); +} + +void QtSoftwareKeyboardDialog::RescaleKeyboardElements(float width, float height, float dpi_scale) { + const auto header_font_size = BASE_HEADER_FONT_SIZE * (height / BASE_HEIGHT) / dpi_scale; + const auto sub_font_size = BASE_SUB_FONT_SIZE * (height / BASE_HEIGHT) / dpi_scale; + const auto editor_font_size = BASE_EDITOR_FONT_SIZE * (height / BASE_HEIGHT) / dpi_scale; + const auto char_button_font_size = + BASE_CHAR_BUTTON_FONT_SIZE * (height / BASE_HEIGHT) / dpi_scale; + const auto label_button_font_size = + BASE_LABEL_BUTTON_FONT_SIZE * (height / BASE_HEIGHT) / dpi_scale; + + QFont header_font(QStringLiteral("MS Shell Dlg 2"), header_font_size, QFont::Normal); + QFont sub_font(QStringLiteral("MS Shell Dlg 2"), sub_font_size, QFont::Normal); + QFont editor_font(QStringLiteral("MS Shell Dlg 2"), editor_font_size, QFont::Normal); + QFont char_button_font(QStringLiteral("MS Shell Dlg 2"), char_button_font_size, QFont::Normal); + QFont label_button_font(QStringLiteral("MS Shell Dlg 2"), label_button_font_size, + QFont::Normal); + + ui->label_header->setFont(header_font); + ui->label_sub->setFont(sub_font); + ui->line_edit_osk->setFont(editor_font); + ui->text_edit_osk->setFont(editor_font); + ui->label_characters->setFont(sub_font); + ui->label_characters_box->setFont(sub_font); + + ui->label_shift->setFont(label_button_font); + ui->label_shift_shift->setFont(label_button_font); + ui->label_cancel->setFont(label_button_font); + ui->label_cancel_shift->setFont(label_button_font); + ui->label_cancel_num->setFont(label_button_font); + ui->label_enter->setFont(label_button_font); + ui->label_enter_shift->setFont(label_button_font); + ui->label_enter_num->setFont(label_button_font); + + for (auto* button : all_buttons) { + if (button == ui->button_return || button == ui->button_return_shift) { + button->setFont(label_button_font); + continue; + } + + if (button == ui->button_space || button == ui->button_space_shift) { + button->setFont(label_button_font); + continue; + } + + if (button == ui->button_shift || button == ui->button_shift_shift) { + button->setFont(label_button_font); + button->setIconSize(QSize(BASE_ICON_BUTTON_SIZE, BASE_ICON_BUTTON_SIZE) * + (height / BASE_HEIGHT)); + continue; + } + + if (button == ui->button_backspace || button == ui->button_backspace_shift || + button == ui->button_backspace_num) { + button->setFont(label_button_font); + button->setIconSize(QSize(BASE_ICON_BUTTON_SIZE, BASE_ICON_BUTTON_SIZE) * + (height / BASE_HEIGHT)); + continue; + } + + if (button == ui->button_ok || button == ui->button_ok_shift || + button == ui->button_ok_num) { + button->setFont(label_button_font); + continue; + } + + button->setFont(char_button_font); + } +} + +void QtSoftwareKeyboardDialog::SetKeyboardType() { + switch (initialize_parameters.type) { + case SwkbdType::Normal: + case SwkbdType::Qwerty: + case SwkbdType::Unknown3: + case SwkbdType::Latin: + case SwkbdType::SimplifiedChinese: + case SwkbdType::TraditionalChinese: + case SwkbdType::Korean: + default: { + bottom_osk_index = BottomOSKIndex::LowerCase; + ui->bottomOSK->setCurrentIndex(static_cast(bottom_osk_index)); + + ui->verticalLayout_2->setStretch(0, 320); + ui->verticalLayout_2->setStretch(1, 400); + + ui->gridLineOSK->setRowStretch(5, 94); + ui->gridBoxOSK->setRowStretch(2, 81); + break; + } + case SwkbdType::NumberPad: { + bottom_osk_index = BottomOSKIndex::NumberPad; + ui->bottomOSK->setCurrentIndex(static_cast(bottom_osk_index)); + + ui->verticalLayout_2->setStretch(0, 370); + ui->verticalLayout_2->setStretch(1, 350); + + ui->gridLineOSK->setRowStretch(5, 144); + ui->gridBoxOSK->setRowStretch(2, 131); + break; + } + } +} + +void QtSoftwareKeyboardDialog::SetPasswordMode() { + switch (initialize_parameters.password_mode) { + case SwkbdPasswordMode::Disabled: + default: + ui->line_edit_osk->setEchoMode(QLineEdit::Normal); + break; + case SwkbdPasswordMode::Enabled: + ui->line_edit_osk->setEchoMode(QLineEdit::Password); + break; + } +} + +void QtSoftwareKeyboardDialog::SetTextDrawType() { + switch (initialize_parameters.text_draw_type) { + case SwkbdTextDrawType::Line: + case SwkbdTextDrawType::DownloadCode: { + ui->topOSK->setCurrentIndex(0); + + if (initialize_parameters.max_text_length <= 10) { + ui->gridLineOSK->setColumnStretch(0, 390); + ui->gridLineOSK->setColumnStretch(1, 500); + ui->gridLineOSK->setColumnStretch(2, 390); + } else { + ui->gridLineOSK->setColumnStretch(0, 130); + ui->gridLineOSK->setColumnStretch(1, 1020); + ui->gridLineOSK->setColumnStretch(2, 130); + } + + if (is_inline) { + return; + } + + connect(ui->line_edit_osk, &QLineEdit::textChanged, [this](const QString& changed_string) { + const auto is_valid = ValidateInputText(changed_string); + + const auto text_length = static_cast(changed_string.length()); + + ui->label_characters->setText(QStringLiteral("%1/%2") + .arg(text_length) + .arg(initialize_parameters.max_text_length)); + + ui->button_ok->setEnabled(is_valid); + ui->button_ok_shift->setEnabled(is_valid); + ui->button_ok_num->setEnabled(is_valid); + + ui->line_edit_osk->setFocus(); + }); + + connect(ui->line_edit_osk, &QLineEdit::cursorPositionChanged, + [this](int old_cursor_position, int new_cursor_position) { + ui->button_backspace->setEnabled( + initialize_parameters.enable_backspace_button && new_cursor_position > 0); + ui->button_backspace_shift->setEnabled( + initialize_parameters.enable_backspace_button && new_cursor_position > 0); + ui->button_backspace_num->setEnabled( + initialize_parameters.enable_backspace_button && new_cursor_position > 0); + + ui->line_edit_osk->setFocus(); + }); + + connect(ui->line_edit_osk, &QLineEdit::returnPressed, [this] { + switch (bottom_osk_index) { + case BottomOSKIndex::LowerCase: + ui->button_ok->click(); + break; + case BottomOSKIndex::UpperCase: + ui->button_ok_shift->click(); + break; + case BottomOSKIndex::NumberPad: + ui->button_ok_num->click(); + break; + default: + break; + } + }); + + ui->line_edit_osk->setPlaceholderText( + QString::fromStdU16String(initialize_parameters.guide_text)); + ui->line_edit_osk->setText(QString::fromStdU16String(initialize_parameters.initial_text)); + ui->line_edit_osk->setMaxLength(initialize_parameters.max_text_length); + ui->line_edit_osk->setCursorPosition(initialize_parameters.initial_cursor_position); + + ui->label_characters->setText(QStringLiteral("%1/%2") + .arg(initialize_parameters.initial_text.size()) + .arg(initialize_parameters.max_text_length)); + break; + } + case SwkbdTextDrawType::Box: + default: { + ui->topOSK->setCurrentIndex(1); + + if (is_inline) { + return; + } + + connect(ui->text_edit_osk, &QTextEdit::textChanged, [this] { + if (static_cast(ui->text_edit_osk->toPlainText().length()) > + initialize_parameters.max_text_length) { + auto text_cursor = ui->text_edit_osk->textCursor(); + ui->text_edit_osk->setTextCursor(text_cursor); + text_cursor.deletePreviousChar(); + } + + const auto is_valid = ValidateInputText(ui->text_edit_osk->toPlainText()); + + const auto text_length = static_cast(ui->text_edit_osk->toPlainText().length()); + + ui->label_characters_box->setText(QStringLiteral("%1/%2") + .arg(text_length) + .arg(initialize_parameters.max_text_length)); + + ui->button_ok->setEnabled(is_valid); + ui->button_ok_shift->setEnabled(is_valid); + ui->button_ok_num->setEnabled(is_valid); + + ui->text_edit_osk->setFocus(); + }); + + connect(ui->text_edit_osk, &QTextEdit::cursorPositionChanged, [this] { + const auto new_cursor_position = ui->text_edit_osk->textCursor().position(); + + ui->button_backspace->setEnabled(initialize_parameters.enable_backspace_button && + new_cursor_position > 0); + ui->button_backspace_shift->setEnabled(initialize_parameters.enable_backspace_button && + new_cursor_position > 0); + ui->button_backspace_num->setEnabled(initialize_parameters.enable_backspace_button && + new_cursor_position > 0); + + ui->text_edit_osk->setFocus(); + }); + + ui->text_edit_osk->setPlaceholderText( + QString::fromStdU16String(initialize_parameters.guide_text)); + ui->text_edit_osk->setText(QString::fromStdU16String(initialize_parameters.initial_text)); + ui->text_edit_osk->moveCursor(initialize_parameters.initial_cursor_position == 0 + ? QTextCursor::Start + : QTextCursor::End); + + ui->label_characters_box->setText(QStringLiteral("%1/%2") + .arg(initialize_parameters.initial_text.size()) + .arg(initialize_parameters.max_text_length)); + break; + } + } +} + +void QtSoftwareKeyboardDialog::SetControllerImage() { + const auto controller_type = Settings::values.players.GetValue()[8].connected + ? Settings::values.players.GetValue()[8].controller_type + : Settings::values.players.GetValue()[0].controller_type; + + const QString theme = [] { + if (QIcon::themeName().contains(QStringLiteral("dark")) || + QIcon::themeName().contains(QStringLiteral("midnight"))) { + return QStringLiteral("_dark"); + } else { + return QString{}; + } + }(); + + switch (controller_type) { + case Settings::ControllerType::ProController: + case Settings::ControllerType::GameCube: + ui->icon_controller->setStyleSheet( + QStringLiteral("image: url(:/overlay/controller_pro%1.png);").arg(theme)); + ui->icon_controller_shift->setStyleSheet( + QStringLiteral("image: url(:/overlay/controller_pro%1.png);").arg(theme)); + ui->icon_controller_num->setStyleSheet( + QStringLiteral("image: url(:/overlay/controller_pro%1.png);").arg(theme)); + break; + case Settings::ControllerType::DualJoyconDetached: + ui->icon_controller->setStyleSheet( + QStringLiteral("image: url(:/overlay/controller_dual_joycon%1.png);").arg(theme)); + ui->icon_controller_shift->setStyleSheet( + QStringLiteral("image: url(:/overlay/controller_dual_joycon%1.png);").arg(theme)); + ui->icon_controller_num->setStyleSheet( + QStringLiteral("image: url(:/overlay/controller_dual_joycon%1.png);").arg(theme)); + break; + case Settings::ControllerType::LeftJoycon: + ui->icon_controller->setStyleSheet( + QStringLiteral("image: url(:/overlay/controller_single_joycon_left%1.png);") + .arg(theme)); + ui->icon_controller_shift->setStyleSheet( + QStringLiteral("image: url(:/overlay/controller_single_joycon_left%1.png);") + .arg(theme)); + ui->icon_controller_num->setStyleSheet( + QStringLiteral("image: url(:/overlay/controller_single_joycon_left%1.png);") + .arg(theme)); + break; + case Settings::ControllerType::RightJoycon: + ui->icon_controller->setStyleSheet( + QStringLiteral("image: url(:/overlay/controller_single_joycon_right%1.png);") + .arg(theme)); + ui->icon_controller_shift->setStyleSheet( + QStringLiteral("image: url(:/overlay/controller_single_joycon_right%1.png);") + .arg(theme)); + ui->icon_controller_num->setStyleSheet( + QStringLiteral("image: url(:/overlay/controller_single_joycon_right%1.png);") + .arg(theme)); + break; + case Settings::ControllerType::Handheld: + ui->icon_controller->setStyleSheet( + QStringLiteral("image: url(:/overlay/controller_handheld%1.png);").arg(theme)); + ui->icon_controller_shift->setStyleSheet( + QStringLiteral("image: url(:/overlay/controller_handheld%1.png);").arg(theme)); + ui->icon_controller_num->setStyleSheet( + QStringLiteral("image: url(:/overlay/controller_handheld%1.png);").arg(theme)); + break; + default: + break; + } +} + +void QtSoftwareKeyboardDialog::DisableKeyboardButtons() { + switch (bottom_osk_index) { + case BottomOSKIndex::LowerCase: + case BottomOSKIndex::UpperCase: + default: { + for (const auto& keys : keyboard_buttons) { + for (const auto& rows : keys) { + for (auto* button : rows) { + if (!button) { + continue; + } + + button->setEnabled(true); + } + } + } + + const auto& key_disable_flags = initialize_parameters.key_disable_flags; + + ui->button_space->setDisabled(key_disable_flags.space); + ui->button_space_shift->setDisabled(key_disable_flags.space); + + ui->button_at->setDisabled(key_disable_flags.at || key_disable_flags.username); + + ui->button_percent->setDisabled(key_disable_flags.percent || key_disable_flags.username); + + ui->button_slash->setDisabled(key_disable_flags.slash); + + ui->button_1->setDisabled(key_disable_flags.numbers); + ui->button_2->setDisabled(key_disable_flags.numbers); + ui->button_3->setDisabled(key_disable_flags.numbers); + ui->button_4->setDisabled(key_disable_flags.numbers); + ui->button_5->setDisabled(key_disable_flags.numbers); + ui->button_6->setDisabled(key_disable_flags.numbers); + ui->button_7->setDisabled(key_disable_flags.numbers); + ui->button_8->setDisabled(key_disable_flags.numbers); + ui->button_9->setDisabled(key_disable_flags.numbers); + ui->button_0->setDisabled(key_disable_flags.numbers); + + ui->button_return->setEnabled(initialize_parameters.enable_return_button); + ui->button_return_shift->setEnabled(initialize_parameters.enable_return_button); + break; + } + case BottomOSKIndex::NumberPad: { + for (const auto& rows : numberpad_buttons) { + for (auto* button : rows) { + if (!button) { + continue; + } + + button->setEnabled(true); + } + } + break; + } + } +} + +void QtSoftwareKeyboardDialog::SetBackspaceOkEnabled() { + if (is_inline) { + ui->button_ok->setEnabled(current_text.size() >= initialize_parameters.min_text_length); + ui->button_ok_shift->setEnabled(current_text.size() >= + initialize_parameters.min_text_length); + ui->button_ok_num->setEnabled(current_text.size() >= initialize_parameters.min_text_length); + + ui->button_backspace->setEnabled(initialize_parameters.enable_backspace_button && + cursor_position > 0); + ui->button_backspace_shift->setEnabled(initialize_parameters.enable_backspace_button && + cursor_position > 0); + ui->button_backspace_num->setEnabled(initialize_parameters.enable_backspace_button && + cursor_position > 0); + } else { + const auto text_length = [this] { + if (ui->topOSK->currentIndex() == 1) { + return static_cast(ui->text_edit_osk->toPlainText().length()); + } else { + return static_cast(ui->line_edit_osk->text().length()); + } + }(); + + const auto normal_cursor_position = [this] { + if (ui->topOSK->currentIndex() == 1) { + return ui->text_edit_osk->textCursor().position(); + } else { + return ui->line_edit_osk->cursorPosition(); + } + }(); + + ui->button_ok->setEnabled(text_length >= initialize_parameters.min_text_length); + ui->button_ok_shift->setEnabled(text_length >= initialize_parameters.min_text_length); + ui->button_ok_num->setEnabled(text_length >= initialize_parameters.min_text_length); + + ui->button_backspace->setEnabled(initialize_parameters.enable_backspace_button && + normal_cursor_position > 0); + ui->button_backspace_shift->setEnabled(initialize_parameters.enable_backspace_button && + normal_cursor_position > 0); + ui->button_backspace_num->setEnabled(initialize_parameters.enable_backspace_button && + normal_cursor_position > 0); + } +} + +bool QtSoftwareKeyboardDialog::ValidateInputText(const QString& input_text) { + const auto& key_disable_flags = initialize_parameters.key_disable_flags; + + const auto input_text_length = static_cast(input_text.length()); + + if (input_text_length < initialize_parameters.min_text_length || + input_text_length > initialize_parameters.max_text_length) { + return false; + } + + if (key_disable_flags.space && input_text.contains(QLatin1Char{' '})) { + return false; + } + + if ((key_disable_flags.at || key_disable_flags.username) && + input_text.contains(QLatin1Char{'@'})) { + return false; + } + + if ((key_disable_flags.percent || key_disable_flags.username) && + input_text.contains(QLatin1Char{'%'})) { + return false; + } + + if (key_disable_flags.slash && input_text.contains(QLatin1Char{'/'})) { + return false; + } + + if ((key_disable_flags.backslash || key_disable_flags.username) && + input_text.contains(QLatin1Char('\\'))) { + return false; + } + + if (key_disable_flags.numbers && + std::any_of(input_text.begin(), input_text.end(), [](QChar c) { return c.isDigit(); })) { + return false; + } + + if (bottom_osk_index == BottomOSKIndex::NumberPad && + std::any_of(input_text.begin(), input_text.end(), [](QChar c) { return !c.isDigit(); })) { + return false; + } + + return true; +} + +void QtSoftwareKeyboardDialog::ChangeBottomOSKIndex() { + switch (bottom_osk_index) { + case BottomOSKIndex::LowerCase: + bottom_osk_index = BottomOSKIndex::UpperCase; + ui->bottomOSK->setCurrentIndex(static_cast(bottom_osk_index)); + + ui->button_shift_shift->setStyleSheet( + QStringLiteral("background-image: url(:/overlay/osk_button_shift_lock_off.png);" + "\nbackground-position: left top;" + "\nbackground-repeat: no-repeat;" + "\nbackground-origin: content;")); + + ui->button_shift_shift->setIconSize(ui->button_shift->iconSize()); + ui->button_backspace_shift->setIconSize(ui->button_backspace->iconSize()); + break; + case BottomOSKIndex::UpperCase: + if (caps_lock_enabled) { + caps_lock_enabled = false; + + ui->button_shift_shift->setStyleSheet( + QStringLiteral("background-image: url(:/overlay/osk_button_shift_lock_off.png);" + "\nbackground-position: left top;" + "\nbackground-repeat: no-repeat;" + "\nbackground-origin: content;")); + + ui->button_shift_shift->setIconSize(ui->button_shift->iconSize()); + ui->button_backspace_shift->setIconSize(ui->button_backspace->iconSize()); + + ui->label_shift_shift->setText(QStringLiteral("Caps Lock")); + + bottom_osk_index = BottomOSKIndex::LowerCase; + ui->bottomOSK->setCurrentIndex(static_cast(bottom_osk_index)); + } else { + caps_lock_enabled = true; + + ui->button_shift_shift->setStyleSheet( + QStringLiteral("background-image: url(:/overlay/osk_button_shift_lock_on.png);" + "\nbackground-position: left top;" + "\nbackground-repeat: no-repeat;" + "\nbackground-origin: content;")); + + ui->button_shift_shift->setIconSize(ui->button_shift->iconSize()); + ui->button_backspace_shift->setIconSize(ui->button_backspace->iconSize()); + + ui->label_shift_shift->setText(QStringLiteral("Caps Lock Off")); + } + break; + case BottomOSKIndex::NumberPad: + default: + break; + } +} + +void QtSoftwareKeyboardDialog::NormalKeyboardButtonClicked(QPushButton* button) { + if (button == ui->button_ampersand) { + if (ui->topOSK->currentIndex() == 1) { + ui->text_edit_osk->insertPlainText(QStringLiteral("&")); + } else { + ui->line_edit_osk->insert(QStringLiteral("&")); + } + return; + } + + if (button == ui->button_return || button == ui->button_return_shift) { + if (ui->topOSK->currentIndex() == 1) { + ui->text_edit_osk->insertPlainText(QStringLiteral("\n")); + } else { + ui->line_edit_osk->insert(QStringLiteral("\n")); + } + return; + } + + if (button == ui->button_space || button == ui->button_space_shift) { + if (ui->topOSK->currentIndex() == 1) { + ui->text_edit_osk->insertPlainText(QStringLiteral(" ")); + } else { + ui->line_edit_osk->insert(QStringLiteral(" ")); + } + return; + } + + if (button == ui->button_shift || button == ui->button_shift_shift) { + ChangeBottomOSKIndex(); + return; + } + + if (button == ui->button_backspace || button == ui->button_backspace_shift || + button == ui->button_backspace_num) { + if (ui->topOSK->currentIndex() == 1) { + auto text_cursor = ui->text_edit_osk->textCursor(); + ui->text_edit_osk->setTextCursor(text_cursor); + text_cursor.deletePreviousChar(); + } else { + ui->line_edit_osk->backspace(); + } + return; + } + + if (button == ui->button_ok || button == ui->button_ok_shift || button == ui->button_ok_num) { + if (ui->topOSK->currentIndex() == 1) { + emit SubmitNormalText(SwkbdResult::Ok, + ui->text_edit_osk->toPlainText().toStdU16String()); + } else { + emit SubmitNormalText(SwkbdResult::Ok, ui->line_edit_osk->text().toStdU16String()); + } + return; + } + + if (ui->topOSK->currentIndex() == 1) { + ui->text_edit_osk->insertPlainText(button->text()); + } else { + ui->line_edit_osk->insert(button->text()); + } + + // Revert the keyboard to lowercase if the shift key is active. + if (bottom_osk_index == BottomOSKIndex::UpperCase && !caps_lock_enabled) { + // This is set to true since ChangeBottomOSKIndex will change bottom_osk_index to LowerCase + // if bottom_osk_index is UpperCase and caps_lock_enabled is true. + caps_lock_enabled = true; + ChangeBottomOSKIndex(); + } +} + +void QtSoftwareKeyboardDialog::InlineKeyboardButtonClicked(QPushButton* button) { + if (!button->isEnabled()) { + return; + } + + if (button == ui->button_ampersand) { + InlineTextInsertString(u"&"); + return; + } + + if (button == ui->button_return || button == ui->button_return_shift) { + InlineTextInsertString(u"\n"); + return; + } + + if (button == ui->button_space || button == ui->button_space_shift) { + InlineTextInsertString(u" "); + return; + } + + if (button == ui->button_shift || button == ui->button_shift_shift) { + ChangeBottomOSKIndex(); + return; + } + + if (button == ui->button_backspace || button == ui->button_backspace_shift || + button == ui->button_backspace_num) { + if (cursor_position <= 0 || current_text.empty()) { + cursor_position = 0; + return; + } + + --cursor_position; + + current_text.erase(cursor_position, 1); + + SetBackspaceOkEnabled(); + + emit SubmitInlineText(SwkbdReplyType::ChangedString, current_text, cursor_position); + return; + } + + if (button == ui->button_ok || button == ui->button_ok_shift || button == ui->button_ok_num) { + emit SubmitInlineText(SwkbdReplyType::DecidedEnter, current_text, cursor_position); + return; + } + + InlineTextInsertString(button->text().toStdU16String()); + + // Revert the keyboard to lowercase if the shift key is active. + if (bottom_osk_index == BottomOSKIndex::UpperCase && !caps_lock_enabled) { + // This is set to true since ChangeBottomOSKIndex will change bottom_osk_index to LowerCase + // if bottom_osk_index is UpperCase and caps_lock_enabled is true. + caps_lock_enabled = true; + ChangeBottomOSKIndex(); + } +} + +void QtSoftwareKeyboardDialog::InlineTextInsertString(std::u16string_view string) { + if ((current_text.size() + string.size()) > initialize_parameters.max_text_length) { + return; + } + + current_text.insert(cursor_position, string); + + cursor_position += static_cast(string.size()); + + SetBackspaceOkEnabled(); + + emit SubmitInlineText(SwkbdReplyType::ChangedString, current_text, cursor_position); +} + +void QtSoftwareKeyboardDialog::SetupMouseHover() { + // setFocus() has a bug where continuously changing focus will cause the focus UI to + // mysteriously disappear. A workaround we have found is using the mouse to hover over + // the buttons to act in place of the button focus. As a result, we will have to set + // a blank cursor when hovering over all the buttons and set a no focus policy so the + // buttons do not stay in focus in addition to the mouse hover. + for (auto* button : all_buttons) { + button->setCursor(QCursor(Qt::BlankCursor)); + button->setFocusPolicy(Qt::NoFocus); + } +} + +template +void QtSoftwareKeyboardDialog::HandleButtonPressedOnce() { + const auto f = [this](HIDButton button) { + if (input_interpreter->IsButtonPressedOnce(button)) { + TranslateButtonPress(button); + } + }; + + (f(T), ...); +} + +template +void QtSoftwareKeyboardDialog::HandleButtonHold() { + const auto f = [this](HIDButton button) { + if (input_interpreter->IsButtonHeld(button)) { + TranslateButtonPress(button); + } + }; + + (f(T), ...); +} + +void QtSoftwareKeyboardDialog::TranslateButtonPress(HIDButton button) { + switch (button) { + case HIDButton::A: + switch (bottom_osk_index) { + case BottomOSKIndex::LowerCase: + case BottomOSKIndex::UpperCase: + keyboard_buttons[static_cast(bottom_osk_index)][row][column]->click(); + break; + case BottomOSKIndex::NumberPad: + numberpad_buttons[row][column]->click(); + break; + default: + break; + } + break; + case HIDButton::B: + switch (bottom_osk_index) { + case BottomOSKIndex::LowerCase: + ui->button_backspace->click(); + break; + case BottomOSKIndex::UpperCase: + ui->button_backspace_shift->click(); + break; + case BottomOSKIndex::NumberPad: + ui->button_backspace_num->click(); + break; + default: + break; + } + break; + case HIDButton::X: + if (is_inline) { + emit SubmitInlineText(SwkbdReplyType::DecidedCancel, current_text, cursor_position); + } else { + if (ui->topOSK->currentIndex() == 1) { + emit SubmitNormalText(SwkbdResult::Cancel, + ui->text_edit_osk->toPlainText().toStdU16String()); + } else { + emit SubmitNormalText(SwkbdResult::Cancel, + ui->line_edit_osk->text().toStdU16String()); + } + } + break; + case HIDButton::Y: + switch (bottom_osk_index) { + case BottomOSKIndex::LowerCase: + ui->button_space->click(); + break; + case BottomOSKIndex::UpperCase: + ui->button_space_shift->click(); + break; + case BottomOSKIndex::NumberPad: + default: + break; + } + break; + case HIDButton::LStick: + case HIDButton::RStick: + switch (bottom_osk_index) { + case BottomOSKIndex::LowerCase: + ui->button_shift->click(); + break; + case BottomOSKIndex::UpperCase: + ui->button_shift_shift->click(); + break; + case BottomOSKIndex::NumberPad: + default: + break; + } + break; + case HIDButton::L: + MoveTextCursorDirection(Direction::Left); + break; + case HIDButton::R: + MoveTextCursorDirection(Direction::Right); + break; + case HIDButton::Plus: + switch (bottom_osk_index) { + case BottomOSKIndex::LowerCase: + ui->button_ok->click(); + break; + case BottomOSKIndex::UpperCase: + ui->button_ok_shift->click(); + break; + case BottomOSKIndex::NumberPad: + ui->button_ok_num->click(); + break; + default: + break; + } + break; + case HIDButton::DLeft: + case HIDButton::LStickLeft: + case HIDButton::RStickLeft: + MoveButtonDirection(Direction::Left); + break; + case HIDButton::DUp: + case HIDButton::LStickUp: + case HIDButton::RStickUp: + MoveButtonDirection(Direction::Up); + break; + case HIDButton::DRight: + case HIDButton::LStickRight: + case HIDButton::RStickRight: + MoveButtonDirection(Direction::Right); + break; + case HIDButton::DDown: + case HIDButton::LStickDown: + case HIDButton::RStickDown: + MoveButtonDirection(Direction::Down); + break; + default: + break; + } +} + +void QtSoftwareKeyboardDialog::MoveButtonDirection(Direction direction) { + // Changes the row or column index depending on the direction. + auto move_direction = [this, direction](std::size_t max_rows, std::size_t max_columns) { + switch (direction) { + case Direction::Left: + column = (column + max_columns - 1) % max_columns; + break; + case Direction::Up: + row = (row + max_rows - 1) % max_rows; + break; + case Direction::Right: + column = (column + 1) % max_columns; + break; + case Direction::Down: + row = (row + 1) % max_rows; + break; + default: + break; + } + }; + + switch (bottom_osk_index) { + case BottomOSKIndex::LowerCase: + case BottomOSKIndex::UpperCase: { + const auto index = static_cast(bottom_osk_index); + + const auto* const prev_button = keyboard_buttons[index][row][column]; + move_direction(NUM_ROWS_NORMAL, NUM_COLUMNS_NORMAL); + auto* curr_button = keyboard_buttons[index][row][column]; + + while (!curr_button || !curr_button->isEnabled() || curr_button == prev_button) { + move_direction(NUM_ROWS_NORMAL, NUM_COLUMNS_NORMAL); + curr_button = keyboard_buttons[index][row][column]; + } + + // This is a workaround for setFocus() randomly not showing focus in the UI + QCursor::setPos(curr_button->mapToGlobal(curr_button->rect().center())); + break; + } + case BottomOSKIndex::NumberPad: { + const auto* const prev_button = numberpad_buttons[row][column]; + move_direction(NUM_ROWS_NUMPAD, NUM_COLUMNS_NUMPAD); + auto* curr_button = numberpad_buttons[row][column]; + + while (!curr_button || !curr_button->isEnabled() || curr_button == prev_button) { + move_direction(NUM_ROWS_NUMPAD, NUM_COLUMNS_NUMPAD); + curr_button = numberpad_buttons[row][column]; + } + + // This is a workaround for setFocus() randomly not showing focus in the UI + QCursor::setPos(curr_button->mapToGlobal(curr_button->rect().center())); + break; + } + default: + break; + } +} + +void QtSoftwareKeyboardDialog::MoveTextCursorDirection(Direction direction) { + switch (direction) { + case Direction::Left: + if (is_inline) { + if (cursor_position <= 0) { + cursor_position = 0; + } else { + --cursor_position; + emit SubmitInlineText(SwkbdReplyType::MovedCursor, current_text, cursor_position); + } + } else { + if (ui->topOSK->currentIndex() == 1) { + ui->text_edit_osk->moveCursor(QTextCursor::Left); + } else { + ui->line_edit_osk->setCursorPosition(ui->line_edit_osk->cursorPosition() - 1); + } + } + break; + case Direction::Right: + if (is_inline) { + if (cursor_position >= static_cast(current_text.size())) { + cursor_position = static_cast(current_text.size()); + } else { + ++cursor_position; + emit SubmitInlineText(SwkbdReplyType::MovedCursor, current_text, cursor_position); + } + } else { + if (ui->topOSK->currentIndex() == 1) { + ui->text_edit_osk->moveCursor(QTextCursor::Right); + } else { + ui->line_edit_osk->setCursorPosition(ui->line_edit_osk->cursorPosition() + 1); + } + } + break; + default: + break; + } +} + +void QtSoftwareKeyboardDialog::StartInputThread() { + if (input_thread_running) { + return; + } + + input_thread_running = true; + + input_thread = std::thread(&QtSoftwareKeyboardDialog::InputThread, this); +} + +void QtSoftwareKeyboardDialog::StopInputThread() { + input_thread_running = false; + + if (input_thread.joinable()) { + input_thread.join(); + } + + if (input_interpreter) { + input_interpreter->ResetButtonStates(); + } +} + +void QtSoftwareKeyboardDialog::InputThread() { + while (input_thread_running) { + input_interpreter->PollInput(); + + HandleButtonPressedOnce(); + + HandleButtonHold(); + + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + } +} + +QtSoftwareKeyboard::QtSoftwareKeyboard(GMainWindow& main_window) { + connect(this, &QtSoftwareKeyboard::MainWindowInitializeKeyboard, &main_window, + &GMainWindow::SoftwareKeyboardInitialize, Qt::QueuedConnection); + connect(this, &QtSoftwareKeyboard::MainWindowShowNormalKeyboard, &main_window, + &GMainWindow::SoftwareKeyboardShowNormal, Qt::QueuedConnection); + connect(this, &QtSoftwareKeyboard::MainWindowShowTextCheckDialog, &main_window, + &GMainWindow::SoftwareKeyboardShowTextCheck, Qt::QueuedConnection); + connect(this, &QtSoftwareKeyboard::MainWindowShowInlineKeyboard, &main_window, + &GMainWindow::SoftwareKeyboardShowInline, Qt::QueuedConnection); + connect(this, &QtSoftwareKeyboard::MainWindowHideInlineKeyboard, &main_window, + &GMainWindow::SoftwareKeyboardHideInline, Qt::QueuedConnection); + connect(this, &QtSoftwareKeyboard::MainWindowInlineTextChanged, &main_window, + &GMainWindow::SoftwareKeyboardInlineTextChanged, Qt::QueuedConnection); + connect(this, &QtSoftwareKeyboard::MainWindowExitKeyboard, &main_window, + &GMainWindow::SoftwareKeyboardExit, Qt::QueuedConnection); + connect(&main_window, &GMainWindow::SoftwareKeyboardSubmitNormalText, this, + &QtSoftwareKeyboard::SubmitNormalText, Qt::QueuedConnection); + connect(&main_window, &GMainWindow::SoftwareKeyboardSubmitInlineText, this, + &QtSoftwareKeyboard::SubmitInlineText, Qt::QueuedConnection); +} QtSoftwareKeyboard::~QtSoftwareKeyboard() = default; + +void QtSoftwareKeyboard::InitializeKeyboard( + bool is_inline, Core::Frontend::KeyboardInitializeParameters initialize_parameters, + std::function submit_normal_callback_, + std::function + submit_inline_callback_) { + if (is_inline) { + submit_inline_callback = std::move(submit_inline_callback_); + } else { + submit_normal_callback = std::move(submit_normal_callback_); + } + + LOG_INFO(Service_AM, + "\nKeyboardInitializeParameters:" + "\nok_text={}" + "\nheader_text={}" + "\nsub_text={}" + "\nguide_text={}" + "\ninitial_text={}" + "\nmax_text_length={}" + "\nmin_text_length={}" + "\ninitial_cursor_position={}" + "\ntype={}" + "\npassword_mode={}" + "\ntext_draw_type={}" + "\nkey_disable_flags={}" + "\nuse_blur_background={}" + "\nenable_backspace_button={}" + "\nenable_return_button={}" + "\ndisable_cancel_button={}", + Common::UTF16ToUTF8(initialize_parameters.ok_text), + Common::UTF16ToUTF8(initialize_parameters.header_text), + Common::UTF16ToUTF8(initialize_parameters.sub_text), + Common::UTF16ToUTF8(initialize_parameters.guide_text), + Common::UTF16ToUTF8(initialize_parameters.initial_text), + initialize_parameters.max_text_length, initialize_parameters.min_text_length, + initialize_parameters.initial_cursor_position, initialize_parameters.type, + initialize_parameters.password_mode, initialize_parameters.text_draw_type, + initialize_parameters.key_disable_flags.raw, initialize_parameters.use_blur_background, + initialize_parameters.enable_backspace_button, + initialize_parameters.enable_return_button, + initialize_parameters.disable_cancel_button); + + emit MainWindowInitializeKeyboard(is_inline, std::move(initialize_parameters)); +} + +void QtSoftwareKeyboard::ShowNormalKeyboard() const { + emit MainWindowShowNormalKeyboard(); +} + +void QtSoftwareKeyboard::ShowTextCheckDialog( + Service::AM::Applets::SwkbdTextCheckResult text_check_result, + std::u16string text_check_message) const { + emit MainWindowShowTextCheckDialog(text_check_result, text_check_message); +} + +void QtSoftwareKeyboard::ShowInlineKeyboard( + Core::Frontend::InlineAppearParameters appear_parameters) const { + LOG_INFO(Service_AM, + "\nInlineAppearParameters:" + "\nmax_text_length={}" + "\nmin_text_length={}" + "\nkey_top_scale_x={}" + "\nkey_top_scale_y={}" + "\nkey_top_translate_x={}" + "\nkey_top_translate_y={}" + "\ntype={}" + "\nkey_disable_flags={}" + "\nkey_top_as_floating={}" + "\nenable_backspace_button={}" + "\nenable_return_button={}" + "\ndisable_cancel_button={}", + appear_parameters.max_text_length, appear_parameters.min_text_length, + appear_parameters.key_top_scale_x, appear_parameters.key_top_scale_y, + appear_parameters.key_top_translate_x, appear_parameters.key_top_translate_y, + appear_parameters.type, appear_parameters.key_disable_flags.raw, + appear_parameters.key_top_as_floating, appear_parameters.enable_backspace_button, + appear_parameters.enable_return_button, appear_parameters.disable_cancel_button); + + emit MainWindowShowInlineKeyboard(std::move(appear_parameters)); +} + +void QtSoftwareKeyboard::HideInlineKeyboard() const { + emit MainWindowHideInlineKeyboard(); +} + +void QtSoftwareKeyboard::InlineTextChanged( + Core::Frontend::InlineTextParameters text_parameters) const { + LOG_INFO(Service_AM, + "\nInlineTextParameters:" + "\ninput_text={}" + "\ncursor_position={}", + Common::UTF16ToUTF8(text_parameters.input_text), text_parameters.cursor_position); + + emit MainWindowInlineTextChanged(std::move(text_parameters)); +} + +void QtSoftwareKeyboard::ExitKeyboard() const { + emit MainWindowExitKeyboard(); +} + +void QtSoftwareKeyboard::SubmitNormalText(Service::AM::Applets::SwkbdResult result, + std::u16string submitted_text) const { + submit_normal_callback(result, submitted_text); +} + +void QtSoftwareKeyboard::SubmitInlineText(Service::AM::Applets::SwkbdReplyType reply_type, + std::u16string submitted_text, + s32 cursor_position) const { + submit_inline_callback(reply_type, submitted_text, cursor_position); +} diff --git a/src/yuzu/applets/software_keyboard.h b/src/yuzu/applets/software_keyboard.h index 8427c0a6ca..1a03c098ce 100644 --- a/src/yuzu/applets/software_keyboard.h +++ b/src/yuzu/applets/software_keyboard.h @@ -1,28 +1,228 @@ -// Copyright 2018 yuzu Emulator Project +// Copyright 2021 yuzu Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. #pragma once +#include +#include +#include +#include + #include #include #include "core/frontend/applets/software_keyboard.h" -class GMainWindow; +enum class HIDButton : u8; -class QtSoftwareKeyboardValidator final : public QValidator { -public: - explicit QtSoftwareKeyboardValidator(); - State validate(QString& input, int& pos) const override; -}; +class InputInterpreter; + +namespace Core { +class System; +} + +namespace Ui { +class QtSoftwareKeyboardDialog; +} + +class GMainWindow; class QtSoftwareKeyboardDialog final : public QDialog { Q_OBJECT public: - QtSoftwareKeyboardDialog(QWidget* parent); + QtSoftwareKeyboardDialog(QWidget* parent, Core::System& system_, bool is_inline_, + Core::Frontend::KeyboardInitializeParameters initialize_parameters_); ~QtSoftwareKeyboardDialog() override; + + void ShowNormalKeyboard(QPoint pos, QSize size); + + void ShowTextCheckDialog(Service::AM::Applets::SwkbdTextCheckResult text_check_result, + std::u16string text_check_message); + + void ShowInlineKeyboard(Core::Frontend::InlineAppearParameters appear_parameters, QPoint pos, + QSize size); + + void HideInlineKeyboard(); + + void InlineTextChanged(Core::Frontend::InlineTextParameters text_parameters); + + void ExitKeyboard(); + +signals: + void SubmitNormalText(Service::AM::Applets::SwkbdResult result, + std::u16string submitted_text) const; + + void SubmitInlineText(Service::AM::Applets::SwkbdReplyType reply_type, + std::u16string submitted_text, s32 cursor_position) const; + +public slots: + void open() override; + void reject() override; + +protected: + /// We override the keyPressEvent for inputting text into the inline software keyboard. + void keyPressEvent(QKeyEvent* event) override; + +private: + enum class Direction { + Left, + Up, + Right, + Down, + }; + + enum class BottomOSKIndex { + LowerCase, + UpperCase, + NumberPad, + }; + + /** + * Moves and resizes the window to a specified position and size. + * + * @param pos Top-left window position + * @param size Window size + */ + void MoveAndResizeWindow(QPoint pos, QSize size); + + /** + * Rescales all keyboard elements to account for High DPI displays. + * + * @param width Window width + * @param height Window height + * @param dpi_scale Display scaling factor + */ + void RescaleKeyboardElements(float width, float height, float dpi_scale); + + /// Sets the keyboard type based on initialize_parameters. + void SetKeyboardType(); + + /// Sets the password mode based on initialize_parameters. + void SetPasswordMode(); + + /// Sets the text draw type based on initialize_parameters. + void SetTextDrawType(); + + /// Sets the controller image at the bottom left of the software keyboard. + void SetControllerImage(); + + /// Disables buttons based on initialize_parameters. + void DisableKeyboardButtons(); + + /// Changes whether the backspace or/and ok buttons should be enabled or disabled. + void SetBackspaceOkEnabled(); + + /** + * Validates the input text sent in based on the parameters in initialize_parameters. + * + * @param input_text Input text + * + * @returns True if the input text is valid, false otherwise. + */ + bool ValidateInputText(const QString& input_text); + + /// Switches between LowerCase and UpperCase (Shift and Caps Lock) + void ChangeBottomOSKIndex(); + + /// Processes a keyboard button click from the UI as normal keyboard input. + void NormalKeyboardButtonClicked(QPushButton* button); + + /// Processes a keyboard button click from the UI as inline keyboard input. + void InlineKeyboardButtonClicked(QPushButton* button); + + /** + * Inserts a string of arbitrary length into the current_text at the current cursor position. + * This is only used for the inline software keyboard. + */ + void InlineTextInsertString(std::u16string_view string); + + /// Setup the mouse hover workaround for "focusing" buttons. This should only be called once. + void SetupMouseHover(); + + /** + * Handles button presses and converts them into keyboard input. + * + * @tparam HIDButton The list of buttons that can be converted into keyboard input. + */ + template + void HandleButtonPressedOnce(); + + /** + * Handles button holds and converts them into keyboard input. + * + * @tparam HIDButton The list of buttons that can be converted into keyboard input. + */ + template + void HandleButtonHold(); + + /** + * Translates a button press to focus or click a keyboard button. + * + * @param button The button press to process. + */ + void TranslateButtonPress(HIDButton button); + + /** + * Moves the focus of a button in a certain direction. + * + * @param direction The direction to move. + */ + void MoveButtonDirection(Direction direction); + + /** + * Moves the text cursor in a certain direction. + * + * @param direction The direction to move. + */ + void MoveTextCursorDirection(Direction direction); + + void StartInputThread(); + void StopInputThread(); + + /// The thread where input is being polled and processed. + void InputThread(); + + std::unique_ptr ui; + + Core::System& system; + + // True if it is the inline software keyboard. + bool is_inline; + + // Common software keyboard initialize parameters. + Core::Frontend::KeyboardInitializeParameters initialize_parameters; + + // Used only by the inline software keyboard since the QLineEdit or QTextEdit is hidden. + std::u16string current_text; + s32 cursor_position{0}; + + static constexpr std::size_t NUM_ROWS_NORMAL = 5; + static constexpr std::size_t NUM_COLUMNS_NORMAL = 12; + static constexpr std::size_t NUM_ROWS_NUMPAD = 4; + static constexpr std::size_t NUM_COLUMNS_NUMPAD = 4; + + // Stores the normal keyboard layout. + std::array, NUM_ROWS_NORMAL>, 2> + keyboard_buttons; + // Stores the numberpad keyboard layout. + std::array, NUM_ROWS_NUMPAD> numberpad_buttons; + + // Contains a set of all buttons used in keyboard_buttons and numberpad_buttons. + std::array all_buttons; + + std::size_t row{0}; + std::size_t column{0}; + + BottomOSKIndex bottom_osk_index{BottomOSKIndex::LowerCase}; + std::atomic caps_lock_enabled{false}; + + std::unique_ptr input_interpreter; + + std::thread input_thread; + + std::atomic input_thread_running{}; }; class QtSoftwareKeyboard final : public QObject, public Core::Frontend::SoftwareKeyboardApplet { @@ -31,4 +231,55 @@ class QtSoftwareKeyboard final : public QObject, public Core::Frontend::Software public: explicit QtSoftwareKeyboard(GMainWindow& parent); ~QtSoftwareKeyboard() override; + + void InitializeKeyboard( + bool is_inline, Core::Frontend::KeyboardInitializeParameters initialize_parameters, + std::function + submit_normal_callback_, + std::function + submit_inline_callback_) override; + + void ShowNormalKeyboard() const override; + + void ShowTextCheckDialog(Service::AM::Applets::SwkbdTextCheckResult text_check_result, + std::u16string text_check_message) const override; + + void ShowInlineKeyboard( + Core::Frontend::InlineAppearParameters appear_parameters) const override; + + void HideInlineKeyboard() const override; + + void InlineTextChanged(Core::Frontend::InlineTextParameters text_parameters) const override; + + void ExitKeyboard() const override; + +signals: + void MainWindowInitializeKeyboard( + bool is_inline, Core::Frontend::KeyboardInitializeParameters initialize_parameters) const; + + void MainWindowShowNormalKeyboard() const; + + void MainWindowShowTextCheckDialog(Service::AM::Applets::SwkbdTextCheckResult text_check_result, + std::u16string text_check_message) const; + + void MainWindowShowInlineKeyboard( + Core::Frontend::InlineAppearParameters appear_parameters) const; + + void MainWindowHideInlineKeyboard() const; + + void MainWindowInlineTextChanged(Core::Frontend::InlineTextParameters text_parameters) const; + + void MainWindowExitKeyboard() const; + +private: + void SubmitNormalText(Service::AM::Applets::SwkbdResult result, + std::u16string submitted_text) const; + + void SubmitInlineText(Service::AM::Applets::SwkbdReplyType reply_type, + std::u16string submitted_text, s32 cursor_position) const; + + mutable std::function + submit_normal_callback; + mutable std::function + submit_inline_callback; }; diff --git a/src/yuzu/applets/software_keyboard.ui b/src/yuzu/applets/software_keyboard.ui new file mode 100644 index 0000000000..b0a1fcde9d --- /dev/null +++ b/src/yuzu/applets/software_keyboard.ui @@ -0,0 +1,3503 @@ + + + QtSoftwareKeyboardDialog + + + + 0 + 0 + 1280 + 720 + + + + Software Keyboard + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 0 + + + + + 0 + 100 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 0 + + + 0 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 17 + + + + 0/32 + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + 26 + 50 + false + + + + Qt::StrongFocus + + + + + + 32 + + + Enter Text + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + + 127 + 20 + + + + + + + + + 23 + + + + + + + + + + + Qt::Horizontal + + + + 127 + 20 + + + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + + 127 + 20 + + + + + + + + + 17 + + + + + + + + + + + Qt::Horizontal + + + + 127 + 20 + + + + + + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + true + + + + 17 + + + + 0/500 + + + + + + + + + + + 0 + + + 14 + + + 9 + + + 14 + + + 9 + + + + + + 26 + + + + Qt::StrongFocus + + + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> +<html><head><meta name="qrichtext" content="1" /><style type="text/css"> +p, li { white-space: pre-wrap; } +</style></head><body style=" font-family:'MS Shell Dlg 2'; font-size:26pt; font-weight:400; font-style:normal;"> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p></body></html> + + + + + + + + + + + + + + + + 0 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 0 + + + + + + 0 + + + 2 + + + 0 + + + 0 + + + 0 + + + + + + + + + + + + Qt::Horizontal + + + + 0 + 20 + + + + + + + + + + + Qt::Horizontal + + + + 0 + 20 + + + + + + + + + + + Qt::Horizontal + + + + 0 + 20 + + + + + + + + + + + Qt::Horizontal + + + + 0 + 20 + + + + + + + + + + + Qt::Horizontal + + + + 0 + 20 + + + + + + + + + + + Qt::Horizontal + + + + 0 + 20 + + + + + + + + + 18 + + + + Shift + + + + + + + Qt::Horizontal + + + + 0 + 20 + + + + + + + + + + + Qt::Horizontal + + + + 0 + 20 + + + + + + + + + 18 + + + + Cancel + + + + + + + Qt::Horizontal + + + + 0 + 20 + + + + + + + + + + + Qt::Horizontal + + + + 0 + 20 + + + + + + + + + 18 + + + + Enter + + + + + + + Qt::Horizontal + + + + 0 + 20 + + + + + + + + + + + + 1 + 1 + + + + + 28 + + + + - + + + + + + + + 1 + 1 + + + + + 28 + + + + ' + + + + + + + + 1 + 1 + + + + + 28 + + + + / + + + + + + + + 1 + 1 + + + + + 28 + + + + ! + + + + + + + + 1 + 1 + + + + + 28 + + + + 7 + + + + + + + + 1 + 1 + + + + + 28 + + + + 8 + + + + + + + + 1 + 1 + + + + + 28 + + + + 0 + + + + + + + + 1 + 1 + + + + + 28 + + + + 9 + + + + + + + + 1 + 1 + + + + + 28 + + + + w + + + + + + + + 1 + 1 + + + + + 28 + + + + r + + + + + + + + 1 + 1 + + + + + 28 + + + + e + + + + + + + + 1 + 1 + + + + + 28 + + + + q + + + + + + + + 1 + 1 + + + + + 28 + + + + u + + + + + + + + 1 + 1 + + + + + 28 + + + + y + + + + + + + + 1 + 1 + + + + + 28 + + + + t + + + + + + + + 1 + 1 + + + + + 28 + + + + o + + + + + + + + 1 + 1 + + + + + 28 + + + + p + + + + + + + + 1 + 1 + + + + + 28 + + + + i + + + + + + + + 1 + 1 + + + + + 28 + + + + a + + + + + + + + 1 + 1 + + + + + 28 + + + + s + + + + + + + + 1 + 1 + + + + + 28 + + + + d + + + + + + + + 1 + 1 + + + + + 28 + + + + f + + + + + + + + 1 + 1 + + + + + 28 + + + + h + + + + + + + + 1 + 1 + + + + + 28 + + + + j + + + + + + + + 1 + 1 + + + + + 28 + + + + g + + + + + + + + 1 + 1 + + + + + 28 + + + + k + + + + + + + + 1 + 1 + + + + + 28 + + + + l + + + + + + + + 1 + 1 + + + + + 28 + + + + : + + + + + + + + 1 + 1 + + + + + 18 + + + + Return + + + + + + + + 1 + 1 + + + + + 18 + + + + OK + + + + + + + + 1 + 1 + + + + + 28 + + + + z + + + + + + + + 1 + 1 + + + + + 28 + + + + c + + + + + + + + 1 + 1 + + + + + 28 + + + + x + + + + + + + + 1 + 1 + + + + + 28 + + + + v + + + + + + + + 1 + 1 + + + + + 28 + + + + m + + + + + + + + 1 + 1 + + + + + 28 + + + + , + + + + + + + + 1 + 1 + + + + + 28 + + + + n + + + + + + + + 1 + 1 + + + + + 28 + + + + b + + + + + + + + 1 + 1 + + + + + 18 + + + + + + + true + + + false + + + + + + + + 1 + 1 + + + + + 28 + + + + ? + + + + + + + + 1 + 1 + + + + + 28 + + + + . + + + + + + + + 1 + 1 + + + + + 28 + + + + 1 + + + + + + + + 1 + 1 + + + + + 28 + + + + 3 + + + + + + + + 1 + 1 + + + + + 28 + + + + 4 + + + + + + + + 1 + 1 + + + + + 28 + + + + 2 + + + + + + + + 1 + 1 + + + + + 28 + + + + 6 + + + + + + + + 1 + 1 + + + + + 28 + + + + 5 + + + + + + + + 1 + 1 + + + + + 18 + + + + Space + + + + + + + + 1 + 1 + + + + + 18 + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Qt::Vertical + + + + 20 + 0 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 0 + + + + + + 0 + + + 2 + + + 0 + + + 0 + + + 0 + + + + + + + + + + + + Qt::Horizontal + + + + 0 + 20 + + + + + + + + + + + Qt::Horizontal + + + + 0 + 20 + + + + + + + + + + + Qt::Horizontal + + + + 0 + 20 + + + + + + + + + + + Qt::Horizontal + + + + 0 + 20 + + + + + + + + + + + Qt::Horizontal + + + + 0 + 20 + + + + + + + + + + + Qt::Horizontal + + + + 0 + 20 + + + + + + + + + 18 + + + + Caps Lock + + + + + + + Qt::Horizontal + + + + 0 + 20 + + + + + + + + + + + Qt::Horizontal + + + + 0 + 20 + + + + + + + + + 18 + + + + Cancel + + + + + + + Qt::Horizontal + + + + 0 + 20 + + + + + + + + + + + Qt::Horizontal + + + + 0 + 20 + + + + + + + + + 18 + + + + Enter + + + + + + + Qt::Horizontal + + + + 0 + 20 + + + + + + + + + + + + 1 + 1 + + + + + 28 + + + + _ + + + + + + + + 1 + 1 + + + + + 28 + + + + " + + + + + + + + 1 + 1 + + + + + 28 + + + + @ + + + + + + + + 1 + 1 + + + + + 28 + + + + = + + + + + + + + 1 + 1 + + + + + 28 + + + + && + + + + + + + + 1 + 1 + + + + + 28 + + + + * + + + + + + + + 1 + 1 + + + + + 28 + + + + ) + + + + + + + + 1 + 1 + + + + + 28 + + + + ( + + + + + + + + 1 + 1 + + + + + 28 + + + + W + + + + + + + + 1 + 1 + + + + + 28 + + + + R + + + + + + + + 1 + 1 + + + + + 28 + + + + E + + + + + + + + 1 + 1 + + + + + 28 + + + + Q + + + + + + + + 1 + 1 + + + + + 28 + + + + U + + + + + + + + 1 + 1 + + + + + 28 + + + + Y + + + + + + + + 1 + 1 + + + + + 28 + + + + T + + + + + + + + 1 + 1 + + + + + 28 + + + + O + + + + + + + + 1 + 1 + + + + + 28 + + + + P + + + + + + + + 1 + 1 + + + + + 28 + + + + I + + + + + + + + 1 + 1 + + + + + 28 + + + + A + + + + + + + + 1 + 1 + + + + + 28 + + + + S + + + + + + + + 1 + 1 + + + + + 28 + + + + D + + + + + + + + 1 + 1 + + + + + 28 + + + + F + + + + + + + + 1 + 1 + + + + + 28 + + + + H + + + + + + + + 1 + 1 + + + + + 28 + + + + J + + + + + + + + 1 + 1 + + + + + 28 + + + + G + + + + + + + + 1 + 1 + + + + + 28 + + + + K + + + + + + + + 1 + 1 + + + + + 28 + + + + L + + + + + + + + 1 + 1 + + + + + 28 + + + + ; + + + + + + + + 1 + 1 + + + + + 18 + + + + Return + + + + + + + + 1 + 1 + + + + + 18 + + + + OK + + + + + + + + 1 + 1 + + + + + 28 + + + + Z + + + + + + + + 1 + 1 + + + + + 28 + + + + C + + + + + + + + 1 + 1 + + + + + 28 + + + + X + + + + + + + + 1 + 1 + + + + + 28 + + + + V + + + + + + + + 1 + 1 + + + + + 28 + + + + M + + + + + + + + 1 + 1 + + + + + 28 + + + + < + + + + + + + + 1 + 1 + + + + + 28 + + + + N + + + + + + + + 1 + 1 + + + + + 28 + + + + B + + + + + + + + 1 + 1 + + + + + 18 + + + + + + + true + + + false + + + + + + + + 1 + 1 + + + + + 28 + + + + + + + + + + + + + 1 + 1 + + + + + 28 + + + + > + + + + + + + + 1 + 1 + + + + + 28 + + + + # + + + + + + + + 1 + 1 + + + + + 28 + + + + ] + + + + + + + + 1 + 1 + + + + + 28 + + + + $ + + + + + + + + 1 + 1 + + + + + 28 + + + + [ + + + + + + + + 1 + 1 + + + + + 28 + + + + ^ + + + + + + + + 1 + 1 + + + + + 28 + + + + % + + + + + + + + 1 + 1 + + + + + 18 + + + + Space + + + + + + + + 1 + 1 + + + + + 18 + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Qt::Vertical + + + + 20 + 0 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 0 + + + 0 + + + 0 + + + + + + 1 + 1 + + + + + 18 + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Qt::Vertical + + + + 20 + 0 + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + + Qt::Horizontal + + + + 0 + 20 + + + + + + + + + + + Qt::Horizontal + + + + 0 + 20 + + + + + + + + + + + Qt::Horizontal + + + + 0 + 20 + + + + + + + + + + + Qt::Horizontal + + + + 0 + 20 + + + + + + + + + + + Qt::Horizontal + + + + 0 + 20 + + + + + + + + + + + Qt::Horizontal + + + + 0 + 20 + + + + + + + + + 18 + + + + Cancel + + + + + + + Qt::Horizontal + + + + 0 + 20 + + + + + + + + + + + Qt::Horizontal + + + + 0 + 20 + + + + + + + + + 18 + + + + Enter + + + + + + + Qt::Horizontal + + + + 0 + 20 + + + + + + + + + + + + 1 + 1 + + + + + 28 + + + + 6 + + + + + + + + 1 + 1 + + + + + 28 + + + + 4 + + + + + + + + 1 + 1 + + + + + 28 + + + + 9 + + + + + + + + 1 + 1 + + + + + 28 + + + + 5 + + + + + + + + 1 + 1 + + + + + 18 + + + + OK + + + + + + + + 1 + 1 + + + + + 28 + + + + 7 + + + + + + + + 1 + 1 + + + + + 28 + + + + 8 + + + + + + + + 1 + 1 + + + + + 28 + + + + 2 + + + + + + + + 1 + 1 + + + + + 28 + + + + 1 + + + + + + + + 1 + 1 + + + + + 28 + + + + 0 + + + + + + + + 1 + 1 + + + + + 28 + + + + 3 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Qt::Vertical + + + + 20 + 0 + + + + + + + + + + + + + + + + + button_1 + button_2 + button_3 + button_4 + button_5 + button_6 + button_7 + button_8 + button_9 + button_0 + button_minus + button_backspace + button_q + button_w + button_e + button_r + button_t + button_y + button_u + button_i + button_o + button_p + button_slash + button_return + button_a + button_s + button_d + button_f + button_g + button_h + button_j + button_k + button_l + button_colon + button_apostrophe + button_z + button_x + button_c + button_v + button_b + button_n + button_m + button_comma + button_dot + button_question + button_exclamation + button_ok + button_shift + button_space + button_hash + button_left_bracket + button_right_bracket + button_dollar + button_percent + button_circumflex + button_ampersand + button_asterisk + button_left_parenthesis + button_right_parenthesis + button_underscore + button_backspace_shift + button_q_shift + button_w_shift + button_e_shift + button_r_shift + button_t_shift + button_y_shift + button_u_shift + button_i_shift + button_o_shift + button_p_shift + button_at + button_return_shift + button_a_shift + button_s_shift + button_d_shift + button_f_shift + button_g_shift + button_h_shift + button_j_shift + button_k_shift + button_l_shift + button_semicolon + button_quotation + button_z_shift + button_x_shift + button_c_shift + button_v_shift + button_b_shift + button_n_shift + button_m_shift + button_less_than + button_greater_than + button_plus + button_equal + button_ok_shift + button_shift_shift + button_space_shift + button_1_num + button_2_num + button_3_num + button_backspace_num + button_4_num + button_5_num + button_6_num + button_ok_num + button_7_num + button_8_num + button_9_num + button_0_num + + + + + + diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp index ce83dee278..5f6cdc0c6f 100644 --- a/src/yuzu/main.cpp +++ b/src/yuzu/main.cpp @@ -466,6 +466,114 @@ void GMainWindow::ProfileSelectorSelectProfile() { emit ProfileSelectorFinishedSelection(uuid); } +void GMainWindow::SoftwareKeyboardInitialize( + bool is_inline, Core::Frontend::KeyboardInitializeParameters initialize_parameters) { + if (software_keyboard) { + LOG_ERROR(Frontend, "The software keyboard is already initialized!"); + return; + } + + software_keyboard = new QtSoftwareKeyboardDialog(render_window, Core::System::GetInstance(), + is_inline, std::move(initialize_parameters)); + + if (is_inline) { + connect( + software_keyboard, &QtSoftwareKeyboardDialog::SubmitInlineText, this, + [this](Service::AM::Applets::SwkbdReplyType reply_type, std::u16string submitted_text, + s32 cursor_position) { + emit SoftwareKeyboardSubmitInlineText(reply_type, submitted_text, cursor_position); + }, + Qt::QueuedConnection); + } else { + connect( + software_keyboard, &QtSoftwareKeyboardDialog::SubmitNormalText, this, + [this](Service::AM::Applets::SwkbdResult result, std::u16string submitted_text) { + emit SoftwareKeyboardSubmitNormalText(result, submitted_text); + }, + Qt::QueuedConnection); + } +} + +void GMainWindow::SoftwareKeyboardShowNormal() { + if (!software_keyboard) { + LOG_ERROR(Frontend, "The software keyboard is not initialized!"); + return; + } + + const auto& layout = render_window->GetFramebufferLayout(); + + const auto x = layout.screen.left; + const auto y = layout.screen.top; + const auto w = layout.screen.GetWidth(); + const auto h = layout.screen.GetHeight(); + + software_keyboard->ShowNormalKeyboard(render_window->mapToGlobal(QPoint(x, y)), QSize(w, h)); +} + +void GMainWindow::SoftwareKeyboardShowTextCheck( + Service::AM::Applets::SwkbdTextCheckResult text_check_result, + std::u16string text_check_message) { + if (!software_keyboard) { + LOG_ERROR(Frontend, "The software keyboard is not initialized!"); + return; + } + + software_keyboard->ShowTextCheckDialog(text_check_result, text_check_message); +} + +void GMainWindow::SoftwareKeyboardShowInline( + Core::Frontend::InlineAppearParameters appear_parameters) { + if (!software_keyboard) { + LOG_ERROR(Frontend, "The software keyboard is not initialized!"); + return; + } + + const auto& layout = render_window->GetFramebufferLayout(); + + const auto x = + static_cast(layout.screen.left + (0.5f * layout.screen.GetWidth() * + ((2.0f * appear_parameters.key_top_translate_x) + + (1.0f - appear_parameters.key_top_scale_x)))); + const auto y = + static_cast(layout.screen.top + (layout.screen.GetHeight() * + ((2.0f * appear_parameters.key_top_translate_y) + + (1.0f - appear_parameters.key_top_scale_y)))); + const auto w = static_cast(layout.screen.GetWidth() * appear_parameters.key_top_scale_x); + const auto h = static_cast(layout.screen.GetHeight() * appear_parameters.key_top_scale_y); + + software_keyboard->ShowInlineKeyboard(std::move(appear_parameters), + render_window->mapToGlobal(QPoint(x, y)), QSize(w, h)); +} + +void GMainWindow::SoftwareKeyboardHideInline() { + if (!software_keyboard) { + LOG_ERROR(Frontend, "The software keyboard is not initialized!"); + return; + } + + software_keyboard->HideInlineKeyboard(); +} + +void GMainWindow::SoftwareKeyboardInlineTextChanged( + Core::Frontend::InlineTextParameters text_parameters) { + if (!software_keyboard) { + LOG_ERROR(Frontend, "The software keyboard is not initialized!"); + return; + } + + software_keyboard->InlineTextChanged(std::move(text_parameters)); +} + +void GMainWindow::SoftwareKeyboardExit() { + if (!software_keyboard) { + return; + } + + software_keyboard->ExitKeyboard(); + + software_keyboard = nullptr; +} + void GMainWindow::WebBrowserOpenWebPage(std::string_view main_url, std::string_view additional_args, bool is_local) { #ifdef YUZU_USE_QT_WEB_ENGINE @@ -1009,6 +1117,10 @@ void GMainWindow::ConnectWidgetEvents() { connect(this, &GMainWindow::EmulationStopping, render_window, &GRenderWindow::OnEmulationStopping); + // Software Keyboard Applet + connect(this, &GMainWindow::EmulationStarting, this, &GMainWindow::SoftwareKeyboardExit); + connect(this, &GMainWindow::EmulationStopping, this, &GMainWindow::SoftwareKeyboardExit); + connect(&status_bar_update_timer, &QTimer::timeout, this, &GMainWindow::UpdateStatusBar); } diff --git a/src/yuzu/main.h b/src/yuzu/main.h index 4c8a879d2c..7f1e50a5b0 100644 --- a/src/yuzu/main.h +++ b/src/yuzu/main.h @@ -135,6 +135,11 @@ signals: void ProfileSelectorFinishedSelection(std::optional uuid); + void SoftwareKeyboardSubmitNormalText(Service::AM::Applets::SwkbdResult result, + std::u16string submitted_text); + void SoftwareKeyboardSubmitInlineText(Service::AM::Applets::SwkbdReplyType reply_type, + std::u16string submitted_text, s32 cursor_position); + void WebBrowserExtractOfflineRomFS(); void WebBrowserClosed(Service::AM::Applets::WebExitReason exit_reason, std::string last_url); @@ -143,6 +148,15 @@ public slots: void OnExecuteProgram(std::size_t program_index); void ControllerSelectorReconfigureControllers( const Core::Frontend::ControllerParameters& parameters); + void SoftwareKeyboardInitialize( + bool is_inline, Core::Frontend::KeyboardInitializeParameters initialize_parameters); + void SoftwareKeyboardShowNormal(); + void SoftwareKeyboardShowTextCheck(Service::AM::Applets::SwkbdTextCheckResult text_check_result, + std::u16string text_check_message); + void SoftwareKeyboardShowInline(Core::Frontend::InlineAppearParameters appear_parameters); + void SoftwareKeyboardHideInline(); + void SoftwareKeyboardInlineTextChanged(Core::Frontend::InlineTextParameters text_parameters); + void SoftwareKeyboardExit(); void ErrorDisplayDisplayError(QString error_code, QString error_text); void ProfileSelectorSelectProfile(); void WebBrowserOpenWebPage(std::string_view main_url, std::string_view additional_args,