diff options
author | Zephyron <zephyron@citron-emu.org> | 2024-12-31 16:19:25 +1000 |
---|---|---|
committer | Zephyron <zephyron@citron-emu.org> | 2024-12-31 16:19:25 +1000 |
commit | 9427e27e24a7135880ee2881c3c44988e174b41a (patch) | |
tree | 83f0062a35be144f6b162eaa823c5b3c7620146e /src/citron/util | |
parent | b35ae725d20960411e8588b11c12a2d55f86c9d0 (diff) |
chore: update project branding to citron
Diffstat (limited to 'src/citron/util')
-rw-r--r-- | src/citron/util/clickable_label.cpp | 11 | ||||
-rw-r--r-- | src/citron/util/clickable_label.h | 21 | ||||
-rw-r--r-- | src/citron/util/controller_navigation.cpp | 179 | ||||
-rw-r--r-- | src/citron/util/controller_navigation.h | 50 | ||||
-rw-r--r-- | src/citron/util/limitable_input_dialog.cpp | 88 | ||||
-rw-r--r-- | src/citron/util/limitable_input_dialog.h | 40 | ||||
-rw-r--r-- | src/citron/util/overlay_dialog.cpp | 268 | ||||
-rw-r--r-- | src/citron/util/overlay_dialog.h | 108 | ||||
-rw-r--r-- | src/citron/util/overlay_dialog.ui | 404 | ||||
-rw-r--r-- | src/citron/util/sequence_dialog/sequence_dialog.cpp | 38 | ||||
-rw-r--r-- | src/citron/util/sequence_dialog/sequence_dialog.h | 23 | ||||
-rw-r--r-- | src/citron/util/url_request_interceptor.cpp | 33 | ||||
-rw-r--r-- | src/citron/util/url_request_interceptor.h | 29 | ||||
-rw-r--r-- | src/citron/util/util.cpp | 152 | ||||
-rw-r--r-- | src/citron/util/util.h | 29 |
15 files changed, 1473 insertions, 0 deletions
diff --git a/src/citron/util/clickable_label.cpp b/src/citron/util/clickable_label.cpp new file mode 100644 index 000000000..89d14190a --- /dev/null +++ b/src/citron/util/clickable_label.cpp @@ -0,0 +1,11 @@ +// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "yuzu/util/clickable_label.h" + +ClickableLabel::ClickableLabel(QWidget* parent, [[maybe_unused]] Qt::WindowFlags f) + : QLabel(parent) {} + +void ClickableLabel::mouseReleaseEvent([[maybe_unused]] QMouseEvent* event) { + emit clicked(); +} diff --git a/src/citron/util/clickable_label.h b/src/citron/util/clickable_label.h new file mode 100644 index 000000000..4fe744150 --- /dev/null +++ b/src/citron/util/clickable_label.h @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include <QLabel> +#include <QWidget> + +class ClickableLabel : public QLabel { + Q_OBJECT + +public: + explicit ClickableLabel(QWidget* parent = nullptr, Qt::WindowFlags f = Qt::WindowFlags()); + ~ClickableLabel() = default; + +signals: + void clicked(); + +protected: + void mouseReleaseEvent(QMouseEvent* event); +}; diff --git a/src/citron/util/controller_navigation.cpp b/src/citron/util/controller_navigation.cpp new file mode 100644 index 000000000..0dbfca243 --- /dev/null +++ b/src/citron/util/controller_navigation.cpp @@ -0,0 +1,179 @@ +// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "common/settings_input.h" +#include "hid_core/frontend/emulated_controller.h" +#include "hid_core/hid_core.h" +#include "yuzu/util/controller_navigation.h" + +ControllerNavigation::ControllerNavigation(Core::HID::HIDCore& hid_core, QWidget* parent) { + player1_controller = hid_core.GetEmulatedController(Core::HID::NpadIdType::Player1); + handheld_controller = hid_core.GetEmulatedController(Core::HID::NpadIdType::Handheld); + Core::HID::ControllerUpdateCallback engine_callback{ + .on_change = [this](Core::HID::ControllerTriggerType type) { ControllerUpdateEvent(type); }, + .is_npad_service = false, + }; + player1_callback_key = player1_controller->SetCallback(engine_callback); + handheld_callback_key = handheld_controller->SetCallback(engine_callback); + is_controller_set = true; +} + +ControllerNavigation::~ControllerNavigation() { + UnloadController(); +} + +void ControllerNavigation::UnloadController() { + if (is_controller_set) { + player1_controller->DeleteCallback(player1_callback_key); + handheld_controller->DeleteCallback(handheld_callback_key); + is_controller_set = false; + } +} + +void ControllerNavigation::TriggerButton(Settings::NativeButton::Values native_button, + Qt::Key key) { + if (button_values[native_button].value && !button_values[native_button].locked) { + emit TriggerKeyboardEvent(key); + } +} + +void ControllerNavigation::ControllerUpdateEvent(Core::HID::ControllerTriggerType type) { + std::scoped_lock lock{mutex}; + if (!Settings::values.controller_navigation) { + return; + } + if (type == Core::HID::ControllerTriggerType::Button) { + ControllerUpdateButton(); + return; + } + + if (type == Core::HID::ControllerTriggerType::Stick) { + ControllerUpdateStick(); + return; + } +} + +void ControllerNavigation::ControllerUpdateButton() { + const auto controller_type = player1_controller->GetNpadStyleIndex(); + const auto& player1_buttons = player1_controller->GetButtonsValues(); + const auto& handheld_buttons = handheld_controller->GetButtonsValues(); + + for (std::size_t i = 0; i < player1_buttons.size(); ++i) { + const bool button = player1_buttons[i].value || handheld_buttons[i].value; + // Trigger only once + button_values[i].locked = button == button_values[i].value; + button_values[i].value = button; + } + + switch (controller_type) { + case Core::HID::NpadStyleIndex::Fullkey: + case Core::HID::NpadStyleIndex::JoyconDual: + case Core::HID::NpadStyleIndex::Handheld: + case Core::HID::NpadStyleIndex::GameCube: + TriggerButton(Settings::NativeButton::A, Qt::Key_Enter); + TriggerButton(Settings::NativeButton::B, Qt::Key_Escape); + TriggerButton(Settings::NativeButton::DDown, Qt::Key_Down); + TriggerButton(Settings::NativeButton::DLeft, Qt::Key_Left); + TriggerButton(Settings::NativeButton::DRight, Qt::Key_Right); + TriggerButton(Settings::NativeButton::DUp, Qt::Key_Up); + break; + case Core::HID::NpadStyleIndex::JoyconLeft: + TriggerButton(Settings::NativeButton::DDown, Qt::Key_Enter); + TriggerButton(Settings::NativeButton::DLeft, Qt::Key_Escape); + break; + case Core::HID::NpadStyleIndex::JoyconRight: + TriggerButton(Settings::NativeButton::X, Qt::Key_Enter); + TriggerButton(Settings::NativeButton::A, Qt::Key_Escape); + break; + default: + break; + } +} + +void ControllerNavigation::ControllerUpdateStick() { + const auto controller_type = player1_controller->GetNpadStyleIndex(); + const auto& player1_sticks = player1_controller->GetSticksValues(); + const auto& handheld_sticks = player1_controller->GetSticksValues(); + bool update = false; + + for (std::size_t i = 0; i < player1_sticks.size(); ++i) { + const Common::Input::StickStatus stick{ + .left = player1_sticks[i].left || handheld_sticks[i].left, + .right = player1_sticks[i].right || handheld_sticks[i].right, + .up = player1_sticks[i].up || handheld_sticks[i].up, + .down = player1_sticks[i].down || handheld_sticks[i].down, + }; + // Trigger only once + if (stick.down != stick_values[i].down || stick.left != stick_values[i].left || + stick.right != stick_values[i].right || stick.up != stick_values[i].up) { + update = true; + } + stick_values[i] = stick; + } + + if (!update) { + return; + } + + switch (controller_type) { + case Core::HID::NpadStyleIndex::Fullkey: + case Core::HID::NpadStyleIndex::JoyconDual: + case Core::HID::NpadStyleIndex::Handheld: + case Core::HID::NpadStyleIndex::GameCube: + if (stick_values[Settings::NativeAnalog::LStick].down) { + emit TriggerKeyboardEvent(Qt::Key_Down); + return; + } + if (stick_values[Settings::NativeAnalog::LStick].left) { + emit TriggerKeyboardEvent(Qt::Key_Left); + return; + } + if (stick_values[Settings::NativeAnalog::LStick].right) { + emit TriggerKeyboardEvent(Qt::Key_Right); + return; + } + if (stick_values[Settings::NativeAnalog::LStick].up) { + emit TriggerKeyboardEvent(Qt::Key_Up); + return; + } + break; + case Core::HID::NpadStyleIndex::JoyconLeft: + if (stick_values[Settings::NativeAnalog::LStick].left) { + emit TriggerKeyboardEvent(Qt::Key_Down); + return; + } + if (stick_values[Settings::NativeAnalog::LStick].up) { + emit TriggerKeyboardEvent(Qt::Key_Left); + return; + } + if (stick_values[Settings::NativeAnalog::LStick].down) { + emit TriggerKeyboardEvent(Qt::Key_Right); + return; + } + if (stick_values[Settings::NativeAnalog::LStick].right) { + emit TriggerKeyboardEvent(Qt::Key_Up); + return; + } + break; + case Core::HID::NpadStyleIndex::JoyconRight: + if (stick_values[Settings::NativeAnalog::RStick].right) { + emit TriggerKeyboardEvent(Qt::Key_Down); + return; + } + if (stick_values[Settings::NativeAnalog::RStick].down) { + emit TriggerKeyboardEvent(Qt::Key_Left); + return; + } + if (stick_values[Settings::NativeAnalog::RStick].up) { + emit TriggerKeyboardEvent(Qt::Key_Right); + return; + } + if (stick_values[Settings::NativeAnalog::RStick].left) { + emit TriggerKeyboardEvent(Qt::Key_Up); + return; + } + break; + default: + break; + } +} diff --git a/src/citron/util/controller_navigation.h b/src/citron/util/controller_navigation.h new file mode 100644 index 000000000..86e210368 --- /dev/null +++ b/src/citron/util/controller_navigation.h @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include <QKeyEvent> +#include <QObject> + +#include "common/input.h" +#include "common/settings_input.h" + +namespace Core::HID { +using ButtonValues = std::array<Common::Input::ButtonStatus, Settings::NativeButton::NumButtons>; +using SticksValues = std::array<Common::Input::StickStatus, Settings::NativeAnalog::NumAnalogs>; +enum class ControllerTriggerType; +class EmulatedController; +class HIDCore; +} // namespace Core::HID + +class ControllerNavigation : public QObject { + Q_OBJECT + +public: + explicit ControllerNavigation(Core::HID::HIDCore& hid_core, QWidget* parent = nullptr); + ~ControllerNavigation(); + + /// Disables events from the emulated controller + void UnloadController(); + +signals: + void TriggerKeyboardEvent(Qt::Key key); + +private: + void TriggerButton(Settings::NativeButton::Values native_button, Qt::Key key); + void ControllerUpdateEvent(Core::HID::ControllerTriggerType type); + + void ControllerUpdateButton(); + + void ControllerUpdateStick(); + + Core::HID::ButtonValues button_values{}; + Core::HID::SticksValues stick_values{}; + + int player1_callback_key{}; + int handheld_callback_key{}; + bool is_controller_set{}; + mutable std::mutex mutex; + Core::HID::EmulatedController* player1_controller; + Core::HID::EmulatedController* handheld_controller; +}; diff --git a/src/citron/util/limitable_input_dialog.cpp b/src/citron/util/limitable_input_dialog.cpp new file mode 100644 index 000000000..5f6a9c193 --- /dev/null +++ b/src/citron/util/limitable_input_dialog.cpp @@ -0,0 +1,88 @@ +// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include <QDialogButtonBox> +#include <QLabel> +#include <QLineEdit> +#include <QPushButton> +#include <QVBoxLayout> +#include "yuzu/util/limitable_input_dialog.h" + +LimitableInputDialog::LimitableInputDialog(QWidget* parent) : QDialog{parent} { + CreateUI(); + ConnectEvents(); +} + +LimitableInputDialog::~LimitableInputDialog() = default; + +void LimitableInputDialog::CreateUI() { + text_label = new QLabel(this); + text_entry = new QLineEdit(this); + text_label_invalid = new QLabel(this); + buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); + + auto* const layout = new QVBoxLayout; + layout->addWidget(text_label); + layout->addWidget(text_entry); + layout->addWidget(text_label_invalid); + layout->addWidget(buttons); + + setLayout(layout); +} + +void LimitableInputDialog::ConnectEvents() { + connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); +} + +QString LimitableInputDialog::GetText(QWidget* parent, const QString& title, const QString& text, + int min_character_limit, int max_character_limit, + InputLimiter limit_type) { + Q_ASSERT(min_character_limit <= max_character_limit); + + LimitableInputDialog dialog{parent}; + dialog.setWindowTitle(title); + dialog.text_label->setText(text); + dialog.text_entry->setMaxLength(max_character_limit); + dialog.text_label_invalid->show(); + + switch (limit_type) { + case InputLimiter::Filesystem: + dialog.invalid_characters = QStringLiteral("<>:;\"/\\|,.!?*"); + break; + default: + dialog.invalid_characters.clear(); + dialog.text_label_invalid->hide(); + break; + } + dialog.text_label_invalid->setText( + tr("The text can't contain any of the following characters:\n%1") + .arg(dialog.invalid_characters)); + + auto* const ok_button = dialog.buttons->button(QDialogButtonBox::Ok); + ok_button->setEnabled(false); + connect(dialog.text_entry, &QLineEdit::textEdited, [&] { + if (!dialog.invalid_characters.isEmpty()) { + dialog.RemoveInvalidCharacters(); + } + ok_button->setEnabled(dialog.text_entry->text().length() >= min_character_limit); + }); + + if (dialog.exec() != QDialog::Accepted) { + return {}; + } + + return dialog.text_entry->text(); +} + +void LimitableInputDialog::RemoveInvalidCharacters() { + auto cpos = text_entry->cursorPosition(); + for (int i = 0; i < text_entry->text().length(); i++) { + if (invalid_characters.contains(text_entry->text().at(i))) { + text_entry->setText(text_entry->text().remove(i, 1)); + i--; + cpos--; + } + } + text_entry->setCursorPosition(cpos); +} diff --git a/src/citron/util/limitable_input_dialog.h b/src/citron/util/limitable_input_dialog.h new file mode 100644 index 000000000..f261f1a0f --- /dev/null +++ b/src/citron/util/limitable_input_dialog.h @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include <QDialog> + +class QDialogButtonBox; +class QLabel; +class QLineEdit; + +/// A QDialog that functions similarly to QInputDialog, however, it allows +/// restricting the minimum and total number of characters that can be entered. +class LimitableInputDialog final : public QDialog { + Q_OBJECT +public: + explicit LimitableInputDialog(QWidget* parent = nullptr); + ~LimitableInputDialog() override; + + enum class InputLimiter { + None, + Filesystem, + }; + + static QString GetText(QWidget* parent, const QString& title, const QString& text, + int min_character_limit, int max_character_limit, + InputLimiter limit_type = InputLimiter::None); + +private: + void CreateUI(); + void ConnectEvents(); + + void RemoveInvalidCharacters(); + QString invalid_characters; + + QLabel* text_label; + QLineEdit* text_entry; + QLabel* text_label_invalid; + QDialogButtonBox* buttons; +}; diff --git a/src/citron/util/overlay_dialog.cpp b/src/citron/util/overlay_dialog.cpp new file mode 100644 index 000000000..466bbe7b2 --- /dev/null +++ b/src/citron/util/overlay_dialog.cpp @@ -0,0 +1,268 @@ +// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include <QKeyEvent> +#include <QScreen> +#include <QWindow> + +#include "core/core.h" +#include "hid_core/frontend/input_interpreter.h" +#include "hid_core/hid_types.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<Ui::OverlayDialog>()}, 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() && !ui->buttonsDialog->isHidden()) { + input_interpreter = std::make_unique<InputInterpreter>(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); + } + + if (ui->button_cancel->isHidden() && ui->button_ok_label->isHidden()) { + ui->buttonsDialog->hide(); + return; + } + + 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); + } + + if (ui->button_cancel_rich->isHidden() && ui->button_ok_rich->isHidden()) { + ui->buttonsRichDialog->hide(); + return; + } + + 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<float>(parentWidget()->width()); + const auto height = static_cast<float>(parentWidget()->height()); + + // High DPI + const float dpi_scale = screen()->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 <Core::HID::NpadButton... T> +void OverlayDialog::HandleButtonPressedOnce() { + const auto f = [this](Core::HID::NpadButton button) { + if (input_interpreter->IsButtonPressedOnce(button)) { + TranslateButtonPress(button); + } + }; + + (f(T), ...); +} + +void OverlayDialog::TranslateButtonPress(Core::HID::NpadButton 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 Core::HID::NpadButton::A: + case Core::HID::NpadButton::B: + if (left_button->hasFocus()) { + left_button->click(); + } else if (right_button->hasFocus()) { + right_button->click(); + } + break; + case Core::HID::NpadButton::Left: + case Core::HID::NpadButton::StickLLeft: + focusPreviousChild(); + break; + case Core::HID::NpadButton::Right: + case Core::HID::NpadButton::StickLRight: + 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<Core::HID::NpadButton::A, Core::HID::NpadButton::B, + Core::HID::NpadButton::Left, Core::HID::NpadButton::Right, + Core::HID::NpadButton::StickLLeft, + Core::HID::NpadButton::StickLRight>(); + + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + } +} + +void OverlayDialog::keyPressEvent(QKeyEvent* e) { + if (!ui->buttonsDialog->isHidden() || e->key() != Qt::Key_Escape) { + QDialog::keyPressEvent(e); + } +} diff --git a/src/citron/util/overlay_dialog.h b/src/citron/util/overlay_dialog.h new file mode 100644 index 000000000..62f9da311 --- /dev/null +++ b/src/citron/util/overlay_dialog.h @@ -0,0 +1,108 @@ +// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include <atomic> +#include <memory> +#include <thread> + +#include <QDialog> + +#include "common/common_types.h" + +class InputInterpreter; + +namespace Core { +class System; +} + +namespace Core::HID { +enum class NpadButton : u64; +} + +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 overlaid 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 <Core::HID::NpadButton... T> + 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(Core::HID::NpadButton button); + + void StartInputThread(); + void StopInputThread(); + + /// The thread where input is being polled and processed. + void InputThread(); + void keyPressEvent(QKeyEvent* e) override; + + std::unique_ptr<Ui::OverlayDialog> ui; + + bool use_rich_text; + + std::unique_ptr<InputInterpreter> input_interpreter; + + std::thread input_thread; + + std::atomic<bool> input_thread_running{}; +}; diff --git a/src/citron/util/overlay_dialog.ui b/src/citron/util/overlay_dialog.ui new file mode 100644 index 000000000..278e2f219 --- /dev/null +++ b/src/citron/util/overlay_dialog.ui @@ -0,0 +1,404 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>OverlayDialog</class> + <widget class="QDialog" name="OverlayDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>1280</width> + <height>720</height> + </rect> + </property> + <property name="windowTitle"> + <string>Dialog</string> + </property> + <property name="styleSheet"> + <string notr="true"/> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <property name="spacing"> + <number>0</number> + </property> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QStackedWidget" name="stackedDialog"> + <property name="currentIndex"> + <number>0</number> + </property> + <widget class="QWidget" name="lineDialog"> + <layout class="QGridLayout" name="lineDialogGridLayout" rowstretch="210,300,210" columnstretch="250,780,250"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <property name="spacing"> + <number>0</number> + </property> + <item row="1" column="1"> + <widget class="QWidget" name="contentDialog" native="true"> + <layout class="QVBoxLayout" name="verticalLayout_2" stretch="70,149,82"> + <property name="spacing"> + <number>0</number> + </property> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QLabel" name="label_title"> + <property name="font"> + <font> + <pointsize>14</pointsize> + </font> + </property> + <property name="alignment"> + <set>Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft</set> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="label_dialog"> + <property name="font"> + <font> + <pointsize>18</pointsize> + </font> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QWidget" name="buttonsDialog" native="true"> + <layout class="QHBoxLayout" name="horizontalLayout"> + <property name="spacing"> + <number>0</number> + </property> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QPushButton" name="button_cancel"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Expanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="font"> + <font> + <pointsize>18</pointsize> + </font> + </property> + <property name="text"> + <string>Cancel</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="button_ok_label"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Expanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="font"> + <font> + <pointsize>18</pointsize> + </font> + </property> + <property name="text"> + <string>OK</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + </layout> + </widget> + </item> + <item row="0" column="1"> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + <item row="1" column="0"> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item row="2" column="1"> + <spacer name="verticalSpacer_2"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + <item row="1" column="2"> + <spacer name="horizontalSpacer_2"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + <widget class="QWidget" name="richDialog"> + <layout class="QGridLayout" name="richDialogGridLayout" rowstretch="100,520,100" columnstretch="165,950,165"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <property name="spacing"> + <number>0</number> + </property> + <item row="1" column="0"> + <spacer name="horizontalSpacer_3"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item row="2" column="1"> + <spacer name="verticalSpacer_4"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + <item row="0" column="1"> + <spacer name="verticalSpacer_3"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + <item row="1" column="1"> + <widget class="QWidget" name="contentRichDialog" native="true"> + <layout class="QVBoxLayout" name="verticalLayout_3" stretch="70,368,82"> + <property name="spacing"> + <number>0</number> + </property> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QLabel" name="label_title_rich"> + <property name="font"> + <font> + <pointsize>14</pointsize> + </font> + </property> + <property name="text"> + <string/> + </property> + <property name="alignment"> + <set>Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft</set> + </property> + </widget> + </item> + <item> + <widget class="QTextBrowser" name="text_browser_dialog"> + <property name="font"> + <font> + <pointsize>18</pointsize> + </font> + </property> + <property name="html"> + <string><!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></string> + </property> + </widget> + </item> + <item> + <widget class="QWidget" name="buttonsRichDialog" native="true"> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <property name="spacing"> + <number>0</number> + </property> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QPushButton" name="button_cancel_rich"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Expanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="font"> + <font> + <pointsize>18</pointsize> + </font> + </property> + <property name="text"> + <string>Cancel</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="button_ok_rich"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Expanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="font"> + <font> + <pointsize>18</pointsize> + </font> + </property> + <property name="text"> + <string>OK</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + </layout> + </widget> + </item> + <item row="1" column="2"> + <spacer name="horizontalSpacer_4"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + </widget> + </item> + </layout> + </widget> + <resources> + <include location="../../../dist/icons/overlay/overlay.qrc"/> + </resources> + <connections/> +</ui> diff --git a/src/citron/util/sequence_dialog/sequence_dialog.cpp b/src/citron/util/sequence_dialog/sequence_dialog.cpp new file mode 100644 index 000000000..1670aa596 --- /dev/null +++ b/src/citron/util/sequence_dialog/sequence_dialog.cpp @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: 2018 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include <QDialogButtonBox> +#include <QKeySequenceEdit> +#include <QVBoxLayout> +#include "yuzu/util/sequence_dialog/sequence_dialog.h" + +SequenceDialog::SequenceDialog(QWidget* parent) : QDialog(parent) { + setWindowTitle(tr("Enter a hotkey")); + + key_sequence = new QKeySequenceEdit; + + auto* const buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + buttons->setCenterButtons(true); + + auto* const layout = new QVBoxLayout(this); + layout->addWidget(key_sequence); + layout->addWidget(buttons); + + connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); +} + +SequenceDialog::~SequenceDialog() = default; + +QKeySequence SequenceDialog::GetSequence() const { + // Only the first key is returned. The other 3, if present, are ignored. + return QKeySequence(key_sequence->keySequence()[0]); +} + +bool SequenceDialog::focusNextPrevChild(bool next) { + return false; +} + +void SequenceDialog::closeEvent(QCloseEvent*) { + reject(); +} diff --git a/src/citron/util/sequence_dialog/sequence_dialog.h b/src/citron/util/sequence_dialog/sequence_dialog.h new file mode 100644 index 000000000..85e146d40 --- /dev/null +++ b/src/citron/util/sequence_dialog/sequence_dialog.h @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: 2018 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include <QDialog> + +class QKeySequenceEdit; + +class SequenceDialog : public QDialog { + Q_OBJECT + +public: + explicit SequenceDialog(QWidget* parent = nullptr); + ~SequenceDialog() override; + + QKeySequence GetSequence() const; + void closeEvent(QCloseEvent*) override; + +private: + QKeySequenceEdit* key_sequence; + bool focusNextPrevChild(bool next) override; +}; diff --git a/src/citron/util/url_request_interceptor.cpp b/src/citron/util/url_request_interceptor.cpp new file mode 100644 index 000000000..996097e35 --- /dev/null +++ b/src/citron/util/url_request_interceptor.cpp @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#ifdef YUZU_USE_QT_WEB_ENGINE + +#include "yuzu/util/url_request_interceptor.h" + +UrlRequestInterceptor::UrlRequestInterceptor(QObject* p) : QWebEngineUrlRequestInterceptor(p) {} + +UrlRequestInterceptor::~UrlRequestInterceptor() = default; + +void UrlRequestInterceptor::interceptRequest(QWebEngineUrlRequestInfo& info) { + const auto resource_type = info.resourceType(); + + switch (resource_type) { + case QWebEngineUrlRequestInfo::ResourceTypeMainFrame: + requested_url = info.requestUrl(); + emit FrameChanged(); + break; + case QWebEngineUrlRequestInfo::ResourceTypeSubFrame: + case QWebEngineUrlRequestInfo::ResourceTypeXhr: + emit FrameChanged(); + break; + default: + break; + } +} + +QUrl UrlRequestInterceptor::GetRequestedURL() const { + return requested_url; +} + +#endif diff --git a/src/citron/util/url_request_interceptor.h b/src/citron/util/url_request_interceptor.h new file mode 100644 index 000000000..9831e1523 --- /dev/null +++ b/src/citron/util/url_request_interceptor.h @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#ifdef YUZU_USE_QT_WEB_ENGINE + +#include <QObject> +#include <QWebEngineUrlRequestInterceptor> + +class UrlRequestInterceptor : public QWebEngineUrlRequestInterceptor { + Q_OBJECT + +public: + explicit UrlRequestInterceptor(QObject* p = nullptr); + ~UrlRequestInterceptor() override; + + void interceptRequest(QWebEngineUrlRequestInfo& info) override; + + QUrl GetRequestedURL() const; + +signals: + void FrameChanged(); + +private: + QUrl requested_url; +}; + +#endif diff --git a/src/citron/util/util.cpp b/src/citron/util/util.cpp new file mode 100644 index 000000000..e22cf84bf --- /dev/null +++ b/src/citron/util/util.cpp @@ -0,0 +1,152 @@ +// SPDX-FileCopyrightText: 2015 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include <array> +#include <cmath> +#include <QPainter> + +#include "common/logging/log.h" +#include "yuzu/util/util.h" + +#ifdef _WIN32 +#include <windows.h> +#include "common/fs/file.h" +#endif + +QFont GetMonospaceFont() { + QFont font(QStringLiteral("monospace")); + // Automatic fallback to a monospace font on on platforms without a font called "monospace" + font.setStyleHint(QFont::Monospace); + font.setFixedPitch(true); + return font; +} + +QString ReadableByteSize(qulonglong size) { + static constexpr std::array units{"B", "KiB", "MiB", "GiB", "TiB", "PiB"}; + if (size == 0) { + return QStringLiteral("0"); + } + + const int digit_groups = std::min(static_cast<int>(std::log10(size) / std::log10(1024)), + static_cast<int>(units.size())); + return QStringLiteral("%L1 %2") + .arg(size / std::pow(1024, digit_groups), 0, 'f', 1) + .arg(QString::fromUtf8(units[digit_groups])); +} + +QPixmap CreateCirclePixmapFromColor(const QColor& color) { + QPixmap circle_pixmap(16, 16); + circle_pixmap.fill(Qt::transparent); + QPainter painter(&circle_pixmap); + painter.setRenderHint(QPainter::Antialiasing); + painter.setPen(color); + painter.setBrush(color); + painter.drawEllipse({circle_pixmap.width() / 2.0, circle_pixmap.height() / 2.0}, 7.0, 7.0); + return circle_pixmap; +} + +bool SaveIconToFile(const std::filesystem::path& icon_path, const QImage& image) { +#if defined(WIN32) +#pragma pack(push, 2) + struct IconDir { + WORD id_reserved; + WORD id_type; + WORD id_count; + }; + + struct IconDirEntry { + BYTE width; + BYTE height; + BYTE color_count; + BYTE reserved; + WORD planes; + WORD bit_count; + DWORD bytes_in_res; + DWORD image_offset; + }; +#pragma pack(pop) + + const QImage source_image = image.convertToFormat(QImage::Format_RGB32); + constexpr std::array<int, 7> scale_sizes{256, 128, 64, 48, 32, 24, 16}; + constexpr int bytes_per_pixel = 4; + + const IconDir icon_dir{ + .id_reserved = 0, + .id_type = 1, + .id_count = static_cast<WORD>(scale_sizes.size()), + }; + + Common::FS::IOFile icon_file(icon_path.string(), Common::FS::FileAccessMode::Write, + Common::FS::FileType::BinaryFile); + if (!icon_file.IsOpen()) { + return false; + } + + if (!icon_file.Write(icon_dir)) { + return false; + } + + std::size_t image_offset = sizeof(IconDir) + (sizeof(IconDirEntry) * scale_sizes.size()); + for (std::size_t i = 0; i < scale_sizes.size(); i++) { + const int image_size = scale_sizes[i] * scale_sizes[i] * bytes_per_pixel; + const IconDirEntry icon_entry{ + .width = static_cast<BYTE>(scale_sizes[i]), + .height = static_cast<BYTE>(scale_sizes[i]), + .color_count = 0, + .reserved = 0, + .planes = 1, + .bit_count = bytes_per_pixel * 8, + .bytes_in_res = static_cast<DWORD>(sizeof(BITMAPINFOHEADER) + image_size), + .image_offset = static_cast<DWORD>(image_offset), + }; + image_offset += icon_entry.bytes_in_res; + if (!icon_file.Write(icon_entry)) { + return false; + } + } + + for (std::size_t i = 0; i < scale_sizes.size(); i++) { + const QImage scaled_image = source_image.scaled( + scale_sizes[i], scale_sizes[i], Qt::IgnoreAspectRatio, Qt::SmoothTransformation); + const BITMAPINFOHEADER info_header{ + .biSize = sizeof(BITMAPINFOHEADER), + .biWidth = scaled_image.width(), + .biHeight = scaled_image.height() * 2, + .biPlanes = 1, + .biBitCount = bytes_per_pixel * 8, + .biCompression = BI_RGB, + .biSizeImage{}, + .biXPelsPerMeter{}, + .biYPelsPerMeter{}, + .biClrUsed{}, + .biClrImportant{}, + }; + + if (!icon_file.Write(info_header)) { + return false; + } + + for (int y = 0; y < scaled_image.height(); y++) { + const auto* line = scaled_image.scanLine(scaled_image.height() - 1 - y); + std::vector<u8> line_data(scaled_image.width() * bytes_per_pixel); + std::memcpy(line_data.data(), line, line_data.size()); + if (!icon_file.Write(line_data)) { + return false; + } + } + } + icon_file.Close(); + + return true; +#elif defined(__linux__) || defined(__FreeBSD__) + // Convert and write the icon as a PNG + if (!image.save(QString::fromStdString(icon_path.string()))) { + LOG_ERROR(Frontend, "Could not write icon as PNG to file"); + } else { + LOG_INFO(Frontend, "Wrote an icon to {}", icon_path.string()); + } + return true; +#else + return false; +#endif +} diff --git a/src/citron/util/util.h b/src/citron/util/util.h new file mode 100644 index 000000000..4094cf6c2 --- /dev/null +++ b/src/citron/util/util.h @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: 2015 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include <filesystem> +#include <QFont> +#include <QString> + +/// Returns a QFont object appropriate to use as a monospace font for debugging widgets, etc. +[[nodiscard]] QFont GetMonospaceFont(); + +/// Convert a size in bytes into a readable format (KiB, MiB, etc.) +[[nodiscard]] QString ReadableByteSize(qulonglong size); + +/** + * Creates a circle pixmap from a specified color + * @param color The color the pixmap shall have + * @return QPixmap circle pixmap + */ +[[nodiscard]] QPixmap CreateCirclePixmapFromColor(const QColor& color); + +/** + * Saves a windows icon to a file + * @param path The icons path + * @param image The image to save + * @return bool If the operation succeeded + */ +[[nodiscard]] bool SaveIconToFile(const std::filesystem::path& icon_path, const QImage& image); |