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] 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 b025ced1c..3e00ff39f 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 422b3cff6..3d4558739 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 000000000..95b148545 --- /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 000000000..e8c388bd0 --- /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 000000000..278e2f219 --- /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 + + + + + + + + + + + + + + +