diff --git a/src/citron/CMakeLists.txt b/src/citron/CMakeLists.txt
index cb028c618..da419989d 100644
--- a/src/citron/CMakeLists.txt
+++ b/src/citron/CMakeLists.txt
@@ -178,6 +178,8 @@ add_executable(citron
game_list_worker.h
hotkeys.cpp
hotkeys.h
+ hotkey_profile_manager.cpp
+ hotkey_profile_manager.h
install_dialog.cpp
install_dialog.h
loading_screen.cpp
diff --git a/src/citron/citron.qrc b/src/citron/citron.qrc
index a85fb4575..4f5ed3919 100644
--- a/src/citron/citron.qrc
+++ b/src/citron/citron.qrc
@@ -10,5 +10,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
../../dist/dice.svg
+ ../../dist/citron.svg
diff --git a/src/citron/configuration/configure_dialog.cpp b/src/citron/configuration/configure_dialog.cpp
index fe8a679b8..e1f425d6b 100644
--- a/src/citron/configuration/configure_dialog.cpp
+++ b/src/citron/configuration/configure_dialog.cpp
@@ -2,7 +2,6 @@
// SPDX-FileCopyrightText: 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
-#include "citron/configuration/configure_dialog.h"
#include
#include
#include
@@ -20,17 +19,12 @@
#include
#include
#include
-#include "common/logging/log.h"
-#include "common/settings.h"
-#include "common/settings_enums.h"
-#include "core/core.h"
-#include "ui_configure.h"
-#include "vk_device_info.h"
#include "citron/configuration/configuration_shared.h"
#include "citron/configuration/configure_applets.h"
#include "citron/configuration/configure_audio.h"
#include "citron/configuration/configure_cpu.h"
#include "citron/configuration/configure_debug_tab.h"
+#include "citron/configuration/configure_dialog.h"
#include "citron/configuration/configure_filesystem.h"
#include "citron/configuration/configure_general.h"
#include "citron/configuration/configure_graphics.h"
@@ -44,12 +38,18 @@
#include "citron/configuration/configure_ui.h"
#include "citron/configuration/configure_web.h"
#include "citron/configuration/style_animation_event_filter.h"
-#include "citron/util/rainbow_style.h"
#include "citron/game_list.h"
#include "citron/hotkeys.h"
#include "citron/main.h"
#include "citron/theme.h"
#include "citron/uisettings.h"
+#include "citron/util/rainbow_style.h"
+#include "common/logging/log.h"
+#include "common/settings.h"
+#include "common/settings_enums.h"
+#include "core/core.h"
+#include "ui_configure.h"
+#include "vk_device_info.h"
static QScrollArea* CreateScrollArea(QWidget* widget) {
auto* scroll_area = new QScrollArea();
@@ -95,7 +95,7 @@ ConfigureDialog::ConfigureDialog(QWidget* parent, HotkeyRegistry& registry_,
ui_tab->UpdateScreenshotInfo(ratio, setup);
},
nullptr, *builder, this)},
- hotkeys_tab{std::make_unique(system_.HIDCore(), this)},
+ hotkeys_tab{std::make_unique(registry, system_.HIDCore(), this)},
input_tab{std::make_unique(system_, this)},
network_tab{std::make_unique(system_, this)},
profile_tab{std::make_unique(system_, this)},
@@ -103,8 +103,8 @@ ConfigureDialog::ConfigureDialog(QWidget* parent, HotkeyRegistry& registry_,
web_tab{std::make_unique(this)} {
if (auto* main_window = qobject_cast(parent)) {
- connect(filesystem_tab.get(), &ConfigureFilesystem::RequestGameListRefresh,
- main_window, &GMainWindow::RefreshGameList);
+ connect(filesystem_tab.get(), &ConfigureFilesystem::RequestGameListRefresh, main_window,
+ &GMainWindow::RefreshGameList);
}
Settings::SetConfiguringGlobal(true);
@@ -115,7 +115,8 @@ ConfigureDialog::ConfigureDialog(QWidget* parent, HotkeyRegistry& registry_,
setWindowFlags(Qt::Window | Qt::CustomizeWindowHint | Qt::WindowTitleHint);
setWindowModality(Qt::NonModal);
} else {
- setWindowFlags(Qt::Dialog | Qt::WindowTitleHint | Qt::WindowSystemMenuHint | Qt::WindowCloseButtonHint);
+ setWindowFlags(Qt::Dialog | Qt::WindowTitleHint | Qt::WindowSystemMenuHint |
+ Qt::WindowCloseButtonHint);
setWindowModality(Qt::WindowModal);
}
@@ -180,18 +181,21 @@ ConfigureDialog::ConfigureDialog(QWidget* parent, HotkeyRegistry& registry_,
ui->stackedWidget->addWidget(CreateScrollArea(applets_tab.get()));
ui->stackedWidget->addWidget(CreateScrollArea(debug_tab_tab.get()));
- connect(tab_button_group.get(), qOverload(&QButtonGroup::idClicked), this, &ConfigureDialog::AnimateTabSwitch);
+ connect(tab_button_group.get(), qOverload(&QButtonGroup::idClicked), this,
+ &ConfigureDialog::AnimateTabSwitch);
connect(ui_tab.get(), &ConfigureUi::themeChanged, this, &ConfigureDialog::UpdateTheme);
- connect(ui_tab.get(), &ConfigureUi::UIPositioningChanged, this, &ConfigureDialog::SetUIPositioning);
+ connect(ui_tab.get(), &ConfigureUi::UIPositioningChanged, this,
+ &ConfigureDialog::SetUIPositioning);
web_tab->SetWebServiceConfigEnabled(enable_web_config);
- hotkeys_tab->Populate(registry);
+ hotkeys_tab->Populate();
input_tab->Initialize(input_subsystem);
general_tab->SetResetCallback([&] { this->close(); });
SetConfiguration();
connect(ui_tab.get(), &ConfigureUi::LanguageChanged, this, &ConfigureDialog::OnLanguageChanged);
if (system.IsPoweredOn()) {
if (auto* apply_button = ui->buttonBox->button(QDialogButtonBox::Apply)) {
- connect(apply_button, &QAbstractButton::clicked, this, &ConfigureDialog::HandleApplyButtonClicked);
+ connect(apply_button, &QAbstractButton::clicked, this,
+ &ConfigureDialog::HandleApplyButtonClicked);
}
}
ui->stackedWidget->setCurrentIndex(0);
@@ -219,10 +223,12 @@ void ConfigureDialog::UpdateTheme() {
const QString d_txt = is_dark ? QStringLiteral("#8d8d8d") : QStringLiteral("#a0a0a0");
// Use dark shadow on light backgrounds, light shadow on dark backgrounds
- const QString shadow_color = is_dark ? QStringLiteral("rgba(0, 0, 0, 0.5)") : QStringLiteral("rgba(255, 255, 255, 0.8)");
+ const QString shadow_color =
+ is_dark ? QStringLiteral("rgba(0, 0, 0, 0.5)") : QStringLiteral("rgba(255, 255, 255, 0.8)");
static QString cached_template;
- if (cached_template.isEmpty()) cached_template = property("templateStyleSheet").toString();
+ if (cached_template.isEmpty())
+ cached_template = property("templateStyleSheet").toString();
QString style_sheet = cached_template;
style_sheet.replace(QStringLiteral("%%ACCENT_COLOR%%"), accent);
@@ -237,10 +243,10 @@ void ConfigureDialog::UpdateTheme() {
style_sheet.replace(QStringLiteral("%%FOCUS_BG_COLOR%%"), f_bg);
style_sheet.replace(QStringLiteral("%%DISABLED_TEXT_COLOR%%"), d_txt);
- style_sheet += QStringLiteral(
- "QSlider::handle:horizontal { background-color: %1; }"
- "QCheckBox::indicator:checked { background-color: %1; border-color: %1; }"
- ).arg(accent);
+ style_sheet +=
+ QStringLiteral("QSlider::handle:horizontal { background-color: %1; }"
+ "QCheckBox::indicator:checked { background-color: %1; border-color: %1; }")
+ .arg(accent);
setStyleSheet(style_sheet);
@@ -250,33 +256,37 @@ void ConfigureDialog::UpdateTheme() {
cpu_tab->SetTemplateStyleSheet(style_sheet);
graphics_advanced_tab->SetTemplateStyleSheet(style_sheet);
- QString sidebar_css = QStringLiteral(
- "QPushButton.tabButton { "
+ QString sidebar_css =
+ QStringLiteral(
+ "QPushButton.tabButton { "
"background-color: %1; "
"color: %2; "
"border: 2px solid transparent; "
- "}"
- "QPushButton.tabButton:checked { "
- "color: %4; " // Use main text color instead of dimmed color for checked state
+ "}"
+ "QPushButton.tabButton:checked { "
+ "color: %4; " // Use main text color instead of dimmed color for checked state
"border: 2px solid %3; "
- "}"
- "QPushButton.tabButton:hover { "
+ "}"
+ "QPushButton.tabButton:hover { "
"border: 2px solid %3; "
- "}"
- "QPushButton.tabButton:pressed { "
+ "}"
+ "QPushButton.tabButton:pressed { "
"background-color: %3; "
"color: #ffffff; "
- "}"
- ).arg(b_bg, d_txt, accent, txt);
+ "}")
+ .arg(b_bg, d_txt, accent, txt);
- if (ui->topButtonWidget) ui->topButtonWidget->setStyleSheet(sidebar_css);
- if (ui->horizontalNavWidget) ui->horizontalNavWidget->setStyleSheet(sidebar_css);
+ if (ui->topButtonWidget)
+ ui->topButtonWidget->setStyleSheet(sidebar_css);
+ if (ui->horizontalNavWidget)
+ ui->horizontalNavWidget->setStyleSheet(sidebar_css);
if (is_rainbow) {
if (!rainbow_timer) {
rainbow_timer = new QTimer(this);
connect(rainbow_timer, &QTimer::timeout, this, [this, b_bg, d_txt, txt, shadow_color] {
- if (ui->buttonBox->underMouse() || m_is_tab_animating || !this->isVisible() || !this->isActiveWindow()) {
+ if (ui->buttonBox->underMouse() || m_is_tab_animating || !this->isVisible() ||
+ !this->isActiveWindow()) {
return;
}
@@ -288,43 +298,52 @@ void ConfigureDialog::UpdateTheme() {
const QString hue_light = current_color.lighter(125).name();
const QString hue_dark = current_color.darker(150).name();
- QString rainbow_sidebar_css = QStringLiteral(
- "QPushButton.tabButton { "
- "background-color: %1; "
- "color: %2; "
- "border: 2px solid transparent; "
- "}"
- "QPushButton.tabButton:checked { "
- "color: %4; " // Use main text color for visibility
- "border: 2px solid %3; "
- "}"
- "QPushButton.tabButton:hover { "
- "border: 2px solid %3; "
- "}"
- "QPushButton.tabButton:pressed { "
- "background-color: %3; "
- "color: #ffffff; "
- "}"
- ).arg(b_bg, d_txt, hue_hex, txt);
+ QString rainbow_sidebar_css =
+ QStringLiteral("QPushButton.tabButton { "
+ "background-color: %1; "
+ "color: %2; "
+ "border: 2px solid transparent; "
+ "}"
+ "QPushButton.tabButton:checked { "
+ "color: %4; " // Use main text color for visibility
+ "border: 2px solid %3; "
+ "}"
+ "QPushButton.tabButton:hover { "
+ "border: 2px solid %3; "
+ "}"
+ "QPushButton.tabButton:pressed { "
+ "background-color: %3; "
+ "color: #ffffff; "
+ "}")
+ .arg(b_bg, d_txt, hue_hex, txt);
- if (ui->topButtonWidget) ui->topButtonWidget->setStyleSheet(rainbow_sidebar_css);
- if (ui->horizontalNavWidget) ui->horizontalNavWidget->setStyleSheet(rainbow_sidebar_css);
+ if (ui->topButtonWidget)
+ ui->topButtonWidget->setStyleSheet(rainbow_sidebar_css);
+ if (ui->horizontalNavWidget)
+ ui->horizontalNavWidget->setStyleSheet(rainbow_sidebar_css);
// Tab Content Area
- if (current_index == input_tab_index) return;
+ if (current_index == input_tab_index)
+ return;
QWidget* currentContainer = ui->stackedWidget->currentWidget();
if (currentContainer) {
- QString tab_css = QStringLiteral(
- "QCheckBox::indicator:checked, QRadioButton::indicator:checked { background-color: %1; border: 1px solid %1; }"
- "QSlider::sub-page:horizontal { background: %1; border-radius: 4px; }"
- "QSlider::handle:horizontal { background-color: %1; border: 1px solid %1; width: 18px; height: 18px; margin: -5px 0; border-radius: 9px; }"
- "QPushButton, QToolButton { background-color: transparent; color: %4; border: 2px solid %1; border-radius: 4px; padding: 5px; }"
- "QPushButton:hover, QToolButton:hover { border-color: %2; color: %2; }"
- "QPushButton:pressed, QToolButton:pressed { background-color: %3; color: #ffffff; border-color: %3; }"
- ).arg(hue_hex, hue_light, hue_dark, txt);
+ QString tab_css =
+ QStringLiteral(
+ "QCheckBox::indicator:checked, QRadioButton::indicator:checked { "
+ "background-color: %1; border: 1px solid %1; }"
+ "QSlider::sub-page:horizontal { background: %1; border-radius: 4px; }"
+ "QSlider::handle:horizontal { background-color: %1; border: 1px solid "
+ "%1; width: 18px; height: 18px; margin: -5px 0; border-radius: 9px; }"
+ "QPushButton, QToolButton { background-color: transparent; color: %4; "
+ "border: 2px solid %1; border-radius: 4px; padding: 5px; }"
+ "QPushButton:hover, QToolButton:hover { border-color: %2; color: %2; }"
+ "QPushButton:pressed, QToolButton:pressed { background-color: %3; "
+ "color: #ffffff; border-color: %3; }")
+ .arg(hue_hex, hue_light, hue_dark, txt);
currentContainer->setStyleSheet(tab_css);
- if (ui->buttonBox) ui->buttonBox->setStyleSheet(tab_css);
+ if (ui->buttonBox)
+ ui->buttonBox->setStyleSheet(tab_css);
}
});
}
@@ -334,7 +353,8 @@ void ConfigureDialog::UpdateTheme() {
if (UISettings::values.enable_rainbow_mode.GetValue() == false && rainbow_timer) {
rainbow_timer->stop();
- if (ui->buttonBox) ui->buttonBox->setStyleSheet({});
+ if (ui->buttonBox)
+ ui->buttonBox->setStyleSheet({});
for (int i = 0; i < ui->stackedWidget->count(); ++i) {
if (auto* w = ui->stackedWidget->widget(i)) {
w->setStyleSheet({});
@@ -370,7 +390,8 @@ void ConfigureDialog::SetUIPositioning(const QString& positioning) {
if (!tab_buttons.empty()) {
const int button_height = tab_buttons[0]->sizeHint().height();
- const int margins = h_layout->contentsMargins().top() + h_layout->contentsMargins().bottom();
+ const int margins =
+ h_layout->contentsMargins().top() + h_layout->contentsMargins().bottom();
// The scroll area frame adds a few pixels, this accounts for it.
const int fixed_height = button_height + margins + 4;
ui->horizontalNavScrollArea->setMaximumHeight(fixed_height);
@@ -417,7 +438,7 @@ void ConfigureDialog::ApplyConfiguration() {
profile_tab->ApplyConfiguration();
filesystem_tab->ApplyConfiguration();
input_tab->ApplyConfiguration();
- hotkeys_tab->ApplyConfiguration(registry);
+ hotkeys_tab->ApplyConfiguration();
cpu_tab->ApplyConfiguration();
graphics_tab->ApplyConfiguration();
graphics_advanced_tab->ApplyConfiguration();
@@ -501,7 +522,8 @@ void ConfigureDialog::AnimateTabSwitch(int id) {
anim_new_opacity->setDuration(duration);
anim_new_opacity->setEasingCurve(QEasingCurve::InQuad);
- auto* button_opacity_effect = qobject_cast(ui->buttonBox->graphicsEffect());
+ auto* button_opacity_effect =
+ qobject_cast(ui->buttonBox->graphicsEffect());
if (!button_opacity_effect) {
button_opacity_effect = new QGraphicsOpacityEffect(ui->buttonBox);
ui->buttonBox->setGraphicsEffect(button_opacity_effect);
@@ -529,18 +551,19 @@ void ConfigureDialog::AnimateTabSwitch(int id) {
animation_group->addAnimation(anim_new_opacity);
animation_group->addAnimation(button_anim_sequence);
- connect(animation_group, &QAbstractAnimation::finished, this, [this, current_widget, next_widget, id]() {
- ui->stackedWidget->setCurrentIndex(id);
+ connect(animation_group, &QAbstractAnimation::finished, this,
+ [this, current_widget, next_widget, id]() {
+ ui->stackedWidget->setCurrentIndex(id);
- next_widget->setGraphicsEffect(nullptr);
- current_widget->hide();
- current_widget->move(0, 0);
+ next_widget->setGraphicsEffect(nullptr);
+ current_widget->hide();
+ current_widget->move(0, 0);
- m_is_tab_animating = false; // Reset the flag
- for (auto button : tab_button_group->buttons()) {
- button->setEnabled(true);
- }
- });
+ m_is_tab_animating = false; // Reset the flag
+ for (auto button : tab_button_group->buttons()) {
+ button->setEnabled(true);
+ }
+ });
m_is_tab_animating = true; // Set the flag
for (auto button : tab_button_group->buttons()) {
diff --git a/src/citron/configuration/configure_hotkeys.cpp b/src/citron/configuration/configure_hotkeys.cpp
index ac08b7527..ac108df55 100644
--- a/src/citron/configuration/configure_hotkeys.cpp
+++ b/src/citron/configuration/configure_hotkeys.cpp
@@ -1,27 +1,37 @@
// SPDX-FileCopyrightText: 2017 Citra Emulator Project
+// SPDX-FileCopyrightText: 2026 Citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
+#include
+#include
+#include
+#include
+#include
#include
#include
+#include
+#include
#include
#include
#include "hid_core/frontend/emulated_controller.h"
#include "hid_core/hid_core.h"
-#include "frontend_common/config.h"
-#include "ui_configure_hotkeys.h"
#include "citron/configuration/configure_hotkeys.h"
#include "citron/hotkeys.h"
#include "citron/uisettings.h"
#include "citron/util/sequence_dialog/sequence_dialog.h"
+#include "frontend_common/config.h"
+#include "ui_configure_hotkeys.h"
constexpr int name_column = 0;
constexpr int hotkey_column = 1;
constexpr int controller_column = 2;
-ConfigureHotkeys::ConfigureHotkeys(Core::HID::HIDCore& hid_core, QWidget* parent)
- : QWidget(parent), ui(std::make_unique()),
+ConfigureHotkeys::ConfigureHotkeys(HotkeyRegistry& registry_, Core::HID::HIDCore& hid_core,
+ QWidget* parent)
+ : QWidget(parent), ui(std::make_unique()), registry(registry_),
+ controller(new Core::HID::EmulatedController(Core::HID::NpadIdType::Player1)),
timeout_timer(std::make_unique()), poll_timer(std::make_unique()) {
ui->setupUi(this);
setFocusPolicy(Qt::ClickFocus);
@@ -36,14 +46,28 @@ ConfigureHotkeys::ConfigureHotkeys(Core::HID::HIDCore& hid_core, QWidget* parent
ui->hotkey_list->setModel(model);
ui->hotkey_list->header()->setStretchLastSection(false);
- ui->hotkey_list->header()->setSectionResizeMode(name_column, QHeaderView::ResizeMode::Stretch);
- ui->hotkey_list->header()->setMinimumSectionSize(150);
+ ui->hotkey_list->header()->setSectionResizeMode(name_column, QHeaderView::Interactive);
+ ui->hotkey_list->header()->setSectionResizeMode(hotkey_column, QHeaderView::Interactive);
+ ui->hotkey_list->header()->setSectionResizeMode(controller_column, QHeaderView::Stretch);
+ ui->hotkey_list->header()->setMinimumSectionSize(70);
connect(ui->button_restore_defaults, &QPushButton::clicked, this,
&ConfigureHotkeys::RestoreDefaults);
connect(ui->button_clear_all, &QPushButton::clicked, this, &ConfigureHotkeys::ClearAll);
- controller = hid_core.GetEmulatedController(Core::HID::NpadIdType::Player1);
+ // Profile Management Connections
+ connect(ui->button_new_profile, &QPushButton::clicked, this,
+ &ConfigureHotkeys::OnCreateProfile);
+ connect(ui->button_delete_profile, &QPushButton::clicked, this,
+ &ConfigureHotkeys::OnDeleteProfile);
+ connect(ui->button_rename_profile, &QPushButton::clicked, this,
+ &ConfigureHotkeys::OnRenameProfile);
+ connect(ui->button_import_profile, &QPushButton::clicked, this,
+ &ConfigureHotkeys::OnImportProfile);
+ connect(ui->button_export_profile, &QPushButton::clicked, this,
+ &ConfigureHotkeys::OnExportProfile);
+ connect(ui->combo_box_profile, qOverload(&QComboBox::currentIndexChanged), this,
+ &ConfigureHotkeys::OnProfileChanged);
connect(timeout_timer.get(), &QTimer::timeout, [this] {
const bool is_button_pressed = pressed_buttons != Core::HID::NpadButton::None ||
@@ -53,8 +77,8 @@ ConfigureHotkeys::ConfigureHotkeys(Core::HID::HIDCore& hid_core, QWidget* parent
connect(poll_timer.get(), &QTimer::timeout, [this] {
pressed_buttons |= controller->GetNpadButtons().raw;
- pressed_home_button |= this->controller->GetHomeButtons().home != 0;
- pressed_capture_button |= this->controller->GetCaptureButtons().capture != 0;
+ pressed_home_button |= controller->GetHomeButtons().home != 0;
+ pressed_capture_button |= controller->GetCaptureButtons().capture != 0;
if (pressed_buttons != Core::HID::NpadButton::None || pressed_home_button ||
pressed_capture_button) {
const QString button_name =
@@ -64,38 +88,230 @@ ConfigureHotkeys::ConfigureHotkeys(Core::HID::HIDCore& hid_core, QWidget* parent
model->setData(button_model_index, button_name);
}
});
- RetranslateUI();
+
+ ui->hotkey_list->setContextMenuPolicy(Qt::CustomContextMenu);
+
+ // Populate profile list first
+ UpdateProfileList();
}
ConfigureHotkeys::~ConfigureHotkeys() = default;
-void ConfigureHotkeys::Populate(const HotkeyRegistry& registry) {
- for (const auto& group : registry.hotkey_groups) {
- QString parent_item_data = QString::fromStdString(group.first);
- auto* parent_item =
- new QStandardItem(QCoreApplication::translate("Hotkeys", qPrintable(parent_item_data)));
- parent_item->setEditable(false);
- parent_item->setData(parent_item_data);
- for (const auto& hotkey : group.second) {
- QString hotkey_action_data = QString::fromStdString(hotkey.first);
- auto* action = new QStandardItem(
- QCoreApplication::translate("Hotkeys", qPrintable(hotkey_action_data)));
- auto* keyseq =
- new QStandardItem(hotkey.second.keyseq.toString(QKeySequence::NativeText));
- auto* controller_keyseq =
- new QStandardItem(QString::fromStdString(hotkey.second.controller_keyseq));
- action->setEditable(false);
- action->setData(hotkey_action_data);
- keyseq->setEditable(false);
- controller_keyseq->setEditable(false);
- parent_item->appendRow({action, keyseq, controller_keyseq});
- }
- model->appendRow(parent_item);
+void ConfigureHotkeys::Populate(const std::string& profile_name) {
+ const auto& profiles = profile_manager.GetProfiles();
+ std::string target_profile = profile_name;
+ if (target_profile.empty()) {
+ target_profile = ui->combo_box_profile->currentText().toStdString();
+ }
+ if (target_profile.empty()) {
+ target_profile = profiles.current_profile;
}
+ // Use default if current profile missing (safety)
+ std::vector current_shortcuts;
+ if (profiles.profiles.count(target_profile)) {
+ current_shortcuts = profiles.profiles.at(target_profile);
+ } else if (profiles.profiles.count("Default")) {
+ current_shortcuts = profiles.profiles.at("Default");
+ }
+
+ // Map overrides for easy lookup: Key = Group + Name
+ std::map, Hotkey::BackendShortcut> overrides;
+ for (const auto& s : current_shortcuts) {
+ overrides[{s.group, s.name}] = s;
+ }
+
+ model->clear();
+ model->setColumnCount(3);
+ model->setHorizontalHeaderLabels({tr("Action"), tr("Hotkey"), tr("Controller Hotkey")});
+
+ for (const auto& [group_name, group_map] : registry.hotkey_groups) {
+ auto* parent_item = new QStandardItem(
+ QCoreApplication::translate("Hotkeys", qPrintable(QString::fromStdString(group_name))));
+ parent_item->setEditable(false);
+ parent_item->setData(QString::fromStdString(group_name), Qt::UserRole);
+ model->appendRow(parent_item);
+
+ for (const auto& [action_name, hotkey] : group_map) {
+ // Determine values (Registry Default vs Profile Override)
+ QString keyseq_str = hotkey.keyseq.toString(QKeySequence::NativeText);
+ QString portable_keyseq = hotkey.keyseq.toString(QKeySequence::PortableText);
+ QString controller_keyseq_str = QString::fromStdString(hotkey.controller_keyseq);
+
+ if (overrides.count({group_name, action_name})) {
+ const auto& overridden = overrides.at({group_name, action_name});
+ portable_keyseq = QString::fromStdString(overridden.shortcut.keyseq);
+ keyseq_str = QKeySequence(portable_keyseq).toString(QKeySequence::NativeText);
+ controller_keyseq_str =
+ QString::fromStdString(overridden.shortcut.controller_keyseq);
+ }
+
+ auto* action_item = new QStandardItem(QCoreApplication::translate(
+ "Hotkeys", qPrintable(QString::fromStdString(action_name))));
+ action_item->setEditable(false);
+ action_item->setData(QString::fromStdString(action_name), Qt::UserRole);
+
+ auto* keyseq_item = new QStandardItem(keyseq_str);
+ keyseq_item->setData(portable_keyseq, Qt::UserRole);
+ keyseq_item->setEditable(false);
+
+ auto* controller_item = new QStandardItem(controller_keyseq_str);
+ controller_item->setEditable(false);
+
+ // Store metadata (context and repeat) for saving later
+ int context = hotkey.context;
+ bool repeat = hotkey.repeat;
+ if (overrides.count({group_name, action_name})) {
+ const auto& overridden = overrides.at({group_name, action_name});
+ context = overridden.shortcut.context;
+ repeat = overridden.shortcut.repeat;
+ }
+ action_item->setData(context, Qt::UserRole + 1);
+ action_item->setData(repeat, Qt::UserRole + 2);
+
+ parent_item->appendRow({action_item, keyseq_item, controller_item});
+ }
+
+ if (group_name == "General" || group_name == "Main Window") {
+ ui->hotkey_list->expand(parent_item->index());
+ }
+ }
ui->hotkey_list->expandAll();
- ui->hotkey_list->resizeColumnToContents(hotkey_column);
- ui->hotkey_list->resizeColumnToContents(controller_column);
+
+ // Re-apply column sizing after model reset
+ ui->hotkey_list->header()->setStretchLastSection(false);
+ ui->hotkey_list->header()->setSectionResizeMode(name_column, QHeaderView::Interactive);
+ ui->hotkey_list->header()->setSectionResizeMode(hotkey_column, QHeaderView::Interactive);
+ ui->hotkey_list->header()->setSectionResizeMode(controller_column, QHeaderView::Stretch);
+ ui->hotkey_list->header()->setMinimumSectionSize(70);
+
+ ui->hotkey_list->setColumnWidth(name_column, 432);
+ ui->hotkey_list->setColumnWidth(hotkey_column, 240);
+
+ // Enforce fixed width for Restore Defaults button to prevent smudging
+ ui->button_restore_defaults->setFixedWidth(143);
+}
+
+void ConfigureHotkeys::UpdateProfileList() {
+ const QSignalBlocker blocker(ui->combo_box_profile);
+ ui->combo_box_profile->clear();
+
+ const auto& profiles = profile_manager.GetProfiles();
+ for (const auto& [name, val] : profiles.profiles) {
+ ui->combo_box_profile->addItem(QString::fromStdString(name));
+ }
+
+ ui->combo_box_profile->setCurrentText(QString::fromStdString(profiles.current_profile));
+ Populate();
+}
+
+void ConfigureHotkeys::OnCreateProfile() {
+ bool ok;
+ QString text = QInputDialog::getText(this, tr("Create Profile"), tr("Profile Name:"),
+ QLineEdit::Normal, QString(), &ok);
+ if (ok && !text.isEmpty()) {
+ if (profile_manager.CreateProfile(text.toStdString())) {
+ // New profile is empty. Fill with current defaults or copy current?
+ // "Defaults" logic usually implies defaults.
+ UpdateProfileList();
+ } else {
+ QMessageBox::warning(this, tr("Error"), tr("Failed to create profile."));
+ }
+ }
+}
+
+void ConfigureHotkeys::OnDeleteProfile() {
+ if (QMessageBox::question(this, tr("Delete Profile"),
+ tr("Are you sure you want to delete this profile?"),
+ QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
+ if (profile_manager.DeleteProfile(ui->combo_box_profile->currentText().toStdString())) {
+ UpdateProfileList();
+ } else {
+ QMessageBox::warning(this, tr("Error"), tr("Failed to delete profile."));
+ }
+ }
+}
+
+void ConfigureHotkeys::OnRenameProfile() {
+ bool ok;
+ QString current_name = ui->combo_box_profile->currentText();
+ QString text = QInputDialog::getText(this, tr("Rename Profile"), tr("New Name:"),
+ QLineEdit::Normal, current_name, &ok);
+ if (ok && !text.isEmpty()) {
+ if (profile_manager.RenameProfile(current_name.toStdString(), text.toStdString())) {
+ UpdateProfileList();
+ } else {
+ QMessageBox::warning(this, tr("Error"), tr("Failed to rename profile."));
+ }
+ }
+}
+
+void ConfigureHotkeys::OnImportProfile() {
+ QString fileName = QFileDialog::getOpenFileName(this, tr("Import Profile"), QString(),
+ tr("JSON Files (*.json)"));
+ if (fileName.isEmpty())
+ return;
+
+ QFile file(fileName);
+ if (!file.open(QIODevice::ReadOnly)) {
+ QMessageBox::warning(this, tr("Error"), tr("Failed to open file for reading."));
+ return;
+ }
+
+ const QByteArray jsonData = file.readAll();
+ const QJsonDocument doc = QJsonDocument::fromJson(jsonData);
+ if (!doc.isObject()) {
+ QMessageBox::warning(this, tr("Error"), tr("Invalid profile format."));
+ return;
+ }
+
+ const QJsonObject root = doc.object();
+ if (!root.contains(QStringLiteral("shortcuts"))) {
+ QMessageBox::warning(this, tr("Error"), tr("Invalid profile file (missing shortcuts)."));
+ return;
+ }
+
+ std::vector shortcuts;
+ const QJsonArray arr = root[QStringLiteral("shortcuts")].toArray();
+ for (const auto& val : arr) {
+ shortcuts.push_back(Hotkey::ProfileManager::DeserializeShortcut(val.toObject()));
+ }
+
+ ApplyShortcutsToModel(shortcuts);
+}
+
+void ConfigureHotkeys::OnExportProfile() {
+ QString current = ui->combo_box_profile->currentText();
+ QString fileName = QFileDialog::getSaveFileName(
+ this, tr("Export Profile"), current + QStringLiteral(".json"), tr("JSON Files (*.json)"));
+ if (fileName.isEmpty())
+ return;
+
+ const std::vector shortcuts = GatherShortcutsFromUI();
+
+ QJsonObject root_obj;
+ root_obj[QStringLiteral("name")] = current;
+ QJsonArray shortcuts_arr;
+ for (const auto& s : shortcuts) {
+ shortcuts_arr.append(Hotkey::ProfileManager::SerializeShortcut(s));
+ }
+ root_obj[QStringLiteral("shortcuts")] = shortcuts_arr;
+
+ QFile file(fileName);
+ if (!file.open(QIODevice::WriteOnly)) {
+ QMessageBox::warning(this, tr("Error"), tr("Failed to open file for writing."));
+ return;
+ }
+
+ file.write(QJsonDocument(root_obj).toJson());
+}
+
+void ConfigureHotkeys::OnProfileChanged(int index) {
+ if (index == -1)
+ return;
+ const std::string name = ui->combo_box_profile->currentText().toStdString();
+ // Decoupled from permanent SetCurrentProfile to ensure "stagnant" behavior.
+ Populate(name);
}
void ConfigureHotkeys::changeEvent(QEvent* event) {
@@ -108,6 +324,7 @@ void ConfigureHotkeys::changeEvent(QEvent* event) {
void ConfigureHotkeys::RetranslateUI() {
ui->retranslateUi(this);
+ ui->label_profile->setText(tr("Hotkey Profile:"));
model->setHorizontalHeaderLabels({tr("Action"), tr("Hotkey"), tr("Controller Hotkey")});
for (int key_id = 0; key_id < model->rowCount(); key_id++) {
@@ -153,6 +370,7 @@ void ConfigureHotkeys::Configure(QModelIndex index) {
tr("The entered key sequence is already assigned to: %1").arg(used_action));
} else {
model->setData(index, key_sequence.toString(QKeySequence::NativeText));
+ model->setData(index, key_sequence.toString(QKeySequence::PortableText), Qt::UserRole);
}
}
void ConfigureHotkeys::ConfigureController(QModelIndex index) {
@@ -307,54 +525,61 @@ std::pair ConfigureHotkeys::IsUsedControllerKey(const QString& ke
return std::make_pair(false, QString());
}
-void ConfigureHotkeys::ApplyConfiguration(HotkeyRegistry& registry) {
- for (int key_id = 0; key_id < model->rowCount(); key_id++) {
- const QStandardItem* parent = model->item(key_id, 0);
- for (int key_column_id = 0; key_column_id < parent->rowCount(); key_column_id++) {
- const QStandardItem* action = parent->child(key_column_id, name_column);
- const QStandardItem* keyseq = parent->child(key_column_id, hotkey_column);
- const QStandardItem* controller_keyseq =
- parent->child(key_column_id, controller_column);
- for (auto& [group, sub_actions] : registry.hotkey_groups) {
- if (group != parent->data().toString().toStdString())
- continue;
- for (auto& [action_name, hotkey] : sub_actions) {
- if (action_name != action->data().toString().toStdString())
- continue;
- hotkey.keyseq = QKeySequence(keyseq->text());
- hotkey.controller_keyseq = controller_keyseq->text().toStdString();
- }
- }
+void ConfigureHotkeys::ApplyConfiguration() {
+ // 1. Sync the current profile selection permanently
+ const std::string current_profile_name = ui->combo_box_profile->currentText().toStdString();
+ profile_manager.SetCurrentProfile(current_profile_name);
+
+ // 2. Update the runtime HotkeyRegistry and UISettings
+ const auto shortcuts = GatherShortcutsFromUI();
+
+ for (const auto& s : shortcuts) {
+ // Update Registry
+ auto& hk = registry.hotkey_groups[s.group][s.name];
+ hk.keyseq = QKeySequence::fromString(QString::fromStdString(s.shortcut.keyseq));
+ hk.controller_keyseq = s.shortcut.controller_keyseq;
+ hk.context = static_cast(s.shortcut.context);
+ hk.repeat = s.shortcut.repeat;
+
+ if (hk.shortcut) {
+ hk.shortcut->setKey(hk.keyseq);
+ }
+ if (hk.controller_shortcut) {
+ hk.controller_shortcut->SetKey(hk.controller_keyseq);
}
}
+ // This will correctly populate UISettings::values.shortcuts based on current registry state
registry.SaveHotkeys();
+
+ // 3. Update the ProfileManager (Storage)
+ profile_manager.SetProfileShortcuts(current_profile_name, shortcuts);
+ profile_manager.Save();
}
void ConfigureHotkeys::RestoreDefaults() {
- size_t hotkey_index = 0;
- const size_t total_default_hotkeys = UISettings::default_hotkeys.size();
-
for (int group_row = 0; group_row < model->rowCount(); ++group_row) {
QStandardItem* parent = model->item(group_row, 0);
+ const std::string group_name = parent->data(Qt::UserRole).toString().toStdString();
for (int child_row = 0; child_row < parent->rowCount(); ++child_row) {
- // This bounds check prevents a crash, and this was originally a safety check w/ showed if it failed,
- // however with further testing w/ restoring default functionality, it would work yet still display, so was changed to a regular Success!.
- if (hotkey_index >= total_default_hotkeys) {
- QMessageBox::information(this, tr("Success!"),
- tr("Citron's Default hotkey entries have been restored!"));
- return;
+ QStandardItem* action_item = parent->child(child_row, name_column);
+ const std::string action_name =
+ action_item->data(Qt::UserRole).toString().toStdString();
+
+ // Find default
+ for (const auto& def : UISettings::default_hotkeys) {
+ if (def.group == group_name && def.name == action_name) {
+ QStandardItem* hotkey_item = parent->child(child_row, hotkey_column);
+ hotkey_item->setText(
+ QKeySequence::fromString(QString::fromStdString(def.shortcut.keyseq))
+ .toString(QKeySequence::NativeText));
+ hotkey_item->setData(QString::fromStdString(def.shortcut.keyseq), Qt::UserRole);
+ parent->child(child_row, controller_column)
+ ->setText(QString::fromStdString(def.shortcut.controller_keyseq));
+ break;
+ }
}
-
- const auto& default_shortcut = UISettings::default_hotkeys[hotkey_index].shortcut;
-
- parent->child(child_row, hotkey_column)
- ->setText(QString::fromStdString(default_shortcut.keyseq));
- parent->child(child_row, controller_column)
- ->setText(QString::fromStdString(default_shortcut.controller_keyseq));
-
- hotkey_index++;
}
}
@@ -366,7 +591,9 @@ void ConfigureHotkeys::ClearAll() {
const QStandardItem* parent = model->item(r, 0);
for (int r2 = 0; r2 < parent->rowCount(); ++r2) {
- model->item(r, 0)->child(r2, hotkey_column)->setText(QString{});
+ QStandardItem* hotkey_item = model->item(r, 0)->child(r2, hotkey_column);
+ hotkey_item->setText(QString{});
+ hotkey_item->setData(QString{}, Qt::UserRole);
model->item(r, 0)->child(r2, controller_column)->setText(QString{});
}
}
@@ -401,8 +628,19 @@ void ConfigureHotkeys::PopupContextMenu(const QPoint& menu_location) {
}
void ConfigureHotkeys::RestoreControllerHotkey(QModelIndex index) {
- const QString& default_key_sequence =
- QString::fromStdString(UISettings::default_hotkeys[index.row()].shortcut.controller_keyseq);
+ const auto* group_item = model->itemFromIndex(index.parent());
+ const auto* action_item = group_item->child(index.row(), name_column);
+ const std::string group_name = group_item->data(Qt::UserRole).toString().toStdString();
+ const std::string action_name = action_item->data(Qt::UserRole).toString().toStdString();
+
+ QString default_key_sequence;
+ for (const auto& def : UISettings::default_hotkeys) {
+ if (def.group == group_name && def.name == action_name) {
+ default_key_sequence = QString::fromStdString(def.shortcut.controller_keyseq);
+ break;
+ }
+ }
+
const auto [key_sequence_used, used_action] = IsUsedControllerKey(default_key_sequence);
if (key_sequence_used && default_key_sequence != model->data(index).toString()) {
@@ -415,16 +653,85 @@ void ConfigureHotkeys::RestoreControllerHotkey(QModelIndex index) {
}
void ConfigureHotkeys::RestoreHotkey(QModelIndex index) {
- const QKeySequence& default_key_sequence = QKeySequence::fromString(
- QString::fromStdString(UISettings::default_hotkeys[index.row()].shortcut.keyseq),
- QKeySequence::NativeText);
+ const auto* group_item = model->itemFromIndex(index.parent());
+ const auto* action_item = group_item->child(index.row(), name_column);
+ const std::string group_name = group_item->data(Qt::UserRole).toString().toStdString();
+ const std::string action_name = action_item->data(Qt::UserRole).toString().toStdString();
+
+ QString default_key_str;
+ for (const auto& def : UISettings::default_hotkeys) {
+ if (def.group == group_name && def.name == action_name) {
+ default_key_str = QString::fromStdString(def.shortcut.keyseq);
+ break;
+ }
+ }
+
+ const QKeySequence& default_key_sequence =
+ QKeySequence::fromString(default_key_str, QKeySequence::NativeText);
const auto [key_sequence_used, used_action] = IsUsedKey(default_key_sequence);
if (key_sequence_used && default_key_sequence != QKeySequence(model->data(index).toString())) {
QMessageBox::warning(
this, tr("Conflicting Key Sequence"),
tr("The default key sequence is already assigned to: %1").arg(used_action));
- } else {
- model->setData(index, default_key_sequence.toString(QKeySequence::NativeText));
+ }
+}
+
+std::vector ConfigureHotkeys::GatherShortcutsFromUI() const {
+ std::vector shortcuts;
+ const auto& root = model->invisibleRootItem();
+ for (int group_row = 0; group_row < root->rowCount(); group_row++) {
+ const auto* group_item = root->child(group_row);
+ const std::string group_name = group_item->data(Qt::UserRole).toString().toStdString();
+ for (int row = 0; row < group_item->rowCount(); row++) {
+ const auto* action_item = group_item->child(row, name_column);
+ const auto* keyseq_item = group_item->child(row, hotkey_column);
+ const auto* controller_item = group_item->child(row, controller_column);
+
+ Hotkey::BackendShortcut s;
+ s.group = group_name;
+ s.name = action_item->data(Qt::UserRole).toString().toStdString();
+ s.shortcut.keyseq = keyseq_item->data(Qt::UserRole).toString().toStdString();
+ s.shortcut.controller_keyseq = controller_item->text().toStdString();
+ s.shortcut.context = action_item->data(Qt::UserRole + 1).toInt();
+ s.shortcut.repeat = action_item->data(Qt::UserRole + 2).toBool();
+ shortcuts.push_back(s);
+ }
+ }
+ return shortcuts;
+}
+
+void ConfigureHotkeys::ApplyShortcutsToModel(
+ const std::vector& shortcuts) {
+ // Map for faster lookup
+ std::map, Hotkey::BackendShortcut> shortcut_map;
+ for (const auto& s : shortcuts) {
+ shortcut_map[{s.group, s.name}] = s;
+ }
+
+ const auto& root = model->invisibleRootItem();
+ for (int group_row = 0; group_row < root->rowCount(); group_row++) {
+ auto* group_item = root->child(group_row);
+ const std::string group_name = group_item->data(Qt::UserRole).toString().toStdString();
+ for (int row = 0; row < group_item->rowCount(); row++) {
+ auto* action_item = group_item->child(row, name_column);
+ const std::string action_name =
+ action_item->data(Qt::UserRole).toString().toStdString();
+
+ if (shortcut_map.count({group_name, action_name})) {
+ const auto& s = shortcut_map.at({group_name, action_name});
+ QStandardItem* hotkey_item = group_item->child(row, hotkey_column);
+ hotkey_item->setText(QKeySequence(QString::fromStdString(s.shortcut.keyseq))
+ .toString(QKeySequence::NativeText));
+ hotkey_item->setData(QString::fromStdString(s.shortcut.keyseq), Qt::UserRole);
+
+ model->setData(model->index(row, controller_column, group_item->index()),
+ QString::fromStdString(s.shortcut.controller_keyseq));
+ model->setData(model->index(row, name_column, group_item->index()),
+ s.shortcut.context, Qt::UserRole + 1);
+ model->setData(model->index(row, name_column, group_item->index()),
+ s.shortcut.repeat, Qt::UserRole + 2);
+ }
+ }
}
}
diff --git a/src/citron/configuration/configure_hotkeys.h b/src/citron/configuration/configure_hotkeys.h
index 20ea3b515..44393c42d 100644
--- a/src/citron/configuration/configure_hotkeys.h
+++ b/src/citron/configuration/configure_hotkeys.h
@@ -1,11 +1,7 @@
-// SPDX-FileCopyrightText: 2017 Citra Emulator Project
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-#pragma once
-
#include
#include
#include
+#include "citron/hotkey_profile_manager.h"
namespace Common {
class ParamPackage;
@@ -28,21 +24,31 @@ class ConfigureHotkeys : public QWidget {
Q_OBJECT
public:
- explicit ConfigureHotkeys(Core::HID::HIDCore& hid_core_, QWidget* parent = nullptr);
+ explicit ConfigureHotkeys(HotkeyRegistry& registry, Core::HID::HIDCore& hid_core_,
+ QWidget* parent = nullptr);
~ConfigureHotkeys() override;
- void ApplyConfiguration(HotkeyRegistry& registry);
+ void ApplyConfiguration();
/**
- * Populates the hotkey list widget using data from the provided registry.
+ * Populates the hotkey list widget using data from the provided profiles.
* Called every time the Configure dialog is opened.
- * @param registry The HotkeyRegistry whose data is used to populate the list.
+ * @param profiles The UserHotkeyProfiles used to populate the list.
*/
- void Populate(const HotkeyRegistry& registry);
+ void Populate(const std::string& profile_name = "");
+
+private slots:
+ void OnCreateProfile();
+ void OnDeleteProfile();
+ void OnRenameProfile();
+ void OnImportProfile();
+ void OnExportProfile();
+ void OnProfileChanged(int index);
private:
void changeEvent(QEvent* event) override;
void RetranslateUI();
+ void UpdateProfileList();
void Configure(QModelIndex index);
void ConfigureController(QModelIndex index);
@@ -55,10 +61,15 @@ private:
void RestoreControllerHotkey(QModelIndex index);
void RestoreHotkey(QModelIndex index);
+ std::vector GatherShortcutsFromUI() const;
+ void ApplyShortcutsToModel(const std::vector& shortcuts);
+
void SetPollingResult(bool cancel);
QString GetButtonCombinationName(Core::HID::NpadButton button, bool home, bool capture) const;
std::unique_ptr ui;
+ Hotkey::ProfileManager profile_manager;
+ HotkeyRegistry& registry;
QStandardItemModel* model;
diff --git a/src/citron/configuration/configure_hotkeys.ui b/src/citron/configuration/configure_hotkeys.ui
index a6902a5d8..c8945769f 100644
--- a/src/citron/configuration/configure_hotkeys.ui
+++ b/src/citron/configuration/configure_hotkeys.ui
@@ -18,16 +18,65 @@
-
-
+
-
-
+
- Double-click on a binding to change it.
+ Hotkey Profile:
-
-
+
+
+
+ 0
+ 0
+
+
+
+
+
+
+ -
+
+
-
+
+
+ New
+
+
+
+ -
+
+
+ Delete
+
+
+
+ -
+
+
+ Rename
+
+
+
+ -
+
+
+ Import
+
+
+
+ -
+
+
+ Export
+
+
+
+ -
+
Qt::Horizontal
@@ -48,6 +97,24 @@
-
+
+
+ 0
+ 0
+
+
+
+
+ 139
+ 0
+
+
+
+
+ 139
+ 16777215
+
+
Restore Defaults
@@ -55,6 +122,13 @@
+ -
+
+
+ Double-click on a binding to change it.
+
+
+
-
-
diff --git a/src/citron/configuration/configure_per_game_cheats.cpp b/src/citron/configuration/configure_per_game_cheats.cpp
index 35310d094..47d6cb2a3 100644
--- a/src/citron/configuration/configure_per_game_cheats.cpp
+++ b/src/citron/configuration/configure_per_game_cheats.cpp
@@ -8,14 +8,21 @@
#include
#include
-#include
+#include
+#include
#include
+#include
+#include
+#include
#include
#include
#include
#include
#include
+#include
+#include "citron/configuration/configure_per_game_cheats.h"
+#include "common/fs/path_util.h"
#include "common/hex_util.h"
#include "common/settings.h"
#include "common/string_util.h"
@@ -28,7 +35,6 @@
#include "core/loader/loader.h"
#include "core/memory/cheat_engine.h"
#include "ui_configure_per_game_cheats.h"
-#include "citron/configuration/configure_per_game_cheats.h"
ConfigurePerGameCheats::ConfigurePerGameCheats(Core::System& system_, QWidget* parent)
: QWidget(parent), ui{std::make_unique()}, system{system_} {
@@ -46,7 +52,10 @@ ConfigurePerGameCheats::ConfigurePerGameCheats(Core::System& system_, QWidget* p
tree_view->setSortingEnabled(true);
tree_view->setEditTriggers(QHeaderView::NoEditTriggers);
tree_view->setUniformRowHeights(true);
- tree_view->setContextMenuPolicy(Qt::NoContextMenu);
+ tree_view->setContextMenuPolicy(Qt::CustomContextMenu);
+
+ connect(tree_view, &QTreeView::customContextMenuRequested, this,
+ &ConfigurePerGameCheats::OnContextMenu);
item_model->insertColumns(0, 1);
item_model->setHeaderData(0, Qt::Horizontal, tr("Cheat Name"));
@@ -64,9 +73,11 @@ ConfigurePerGameCheats::ConfigurePerGameCheats(Core::System& system_, QWidget* p
enable_all_button = new QPushButton(tr("Enable All"));
disable_all_button = new QPushButton(tr("Disable All"));
save_button = new QPushButton(tr("Save"));
+ refresh_button = new QPushButton(tr("Refresh"));
button_layout->addWidget(enable_all_button);
button_layout->addWidget(disable_all_button);
+ button_layout->addWidget(refresh_button);
button_layout->addStretch();
button_layout->addWidget(save_button);
@@ -83,8 +94,8 @@ ConfigurePerGameCheats::ConfigurePerGameCheats(Core::System& system_, QWidget* p
&ConfigurePerGameCheats::EnableAllCheats);
connect(disable_all_button, &QPushButton::clicked, this,
&ConfigurePerGameCheats::DisableAllCheats);
- connect(save_button, &QPushButton::clicked, this,
- &ConfigurePerGameCheats::SaveCheatSettings);
+ connect(save_button, &QPushButton::clicked, this, &ConfigurePerGameCheats::SaveCheatSettings);
+ connect(refresh_button, &QPushButton::clicked, this, &ConfigurePerGameCheats::RefreshCheats);
}
ConfigurePerGameCheats::~ConfigurePerGameCheats() = default;
@@ -194,7 +205,8 @@ void ConfigurePerGameCheats::LoadConfiguration() {
FileSys::XCI xci(file, title_id, 0);
if (xci.GetStatus() == Loader::ResultStatus::Success) {
auto program_nca = xci.GetNCAByType(FileSys::NCAContentType::Program);
- if (program_nca && program_nca->GetStatus() == Loader::ResultStatus::Success) {
+ if (program_nca &&
+ program_nca->GetStatus() == Loader::ResultStatus::Success) {
auto exefs = program_nca->GetExeFS();
if (exefs) {
main_nso = exefs->GetFile("main");
@@ -241,7 +253,8 @@ void ConfigurePerGameCheats::LoadConfiguration() {
if (load_dir) {
auto patch_dirs = load_dir->GetSubdirectories();
for (const auto& subdir : patch_dirs) {
- if (!subdir) continue;
+ if (!subdir)
+ continue;
// Use case-insensitive directory search (same as FindSubdirectoryCaseless)
FileSys::VirtualDir cheats_dir;
@@ -288,8 +301,10 @@ void ConfigurePerGameCheats::LoadConfiguration() {
try {
// Pad to full 64 chars (32 bytes) with zeros
// Keep the case as-is from the filename
- auto full_build_id_hex = potential_build_id + std::string(48, '0');
- auto build_id_bytes = Common::HexStringToArray<0x20>(full_build_id_hex);
+ auto full_build_id_hex =
+ potential_build_id + std::string(48, '0');
+ auto build_id_bytes =
+ Common::HexStringToArray<0x20>(full_build_id_hex);
// Verify the result is not all zeros
bool is_valid_result = false;
@@ -311,7 +326,8 @@ void ConfigurePerGameCheats::LoadConfiguration() {
}
}
}
- if (has_build_id) break;
+ if (has_build_id)
+ break;
}
}
}
@@ -353,9 +369,9 @@ void ConfigurePerGameCheats::LoadConfiguration() {
// Add cheats to tree view
for (const auto& cheat : cheats) {
// Extract cheat name from readable_name (null-terminated)
- const std::string cheat_name_str(cheat.definition.readable_name.data(),
- strnlen(cheat.definition.readable_name.data(),
- cheat.definition.readable_name.size()));
+ const std::string cheat_name_str(
+ cheat.definition.readable_name.data(),
+ strnlen(cheat.definition.readable_name.data(), cheat.definition.readable_name.size()));
// Skip empty cheat names or cheats with no opcodes
if (cheat_name_str.empty() || cheat.definition.num_opcodes == 0) {
@@ -369,7 +385,8 @@ void ConfigurePerGameCheats::LoadConfiguration() {
cheat_item->setCheckable(true);
// Check if cheat is disabled
- const bool cheat_disabled = disabled_cheats_set.find(cheat_name_str) != disabled_cheats_set.end();
+ const bool cheat_disabled =
+ disabled_cheats_set.find(cheat_name_str) != disabled_cheats_set.end();
cheat_item->setCheckState(cheat_disabled ? Qt::Unchecked : Qt::Checked);
list_items.push_back(QList{cheat_item});
@@ -474,3 +491,26 @@ void ConfigurePerGameCheats::ReloadCheatEngine() const {
const auto cheats = pm.CreateCheatList(current_build_id);
cheat_engine->Reload(cheats);
}
+
+void ConfigurePerGameCheats::OnContextMenu(const QPoint& pos) {
+ const auto index = tree_view->indexAt(pos);
+ if (!index.isValid()) {
+ return;
+ }
+
+ QMenu context_menu;
+
+ auto* open_folder_action = context_menu.addAction(tr("Open Cheats Folder"));
+ connect(open_folder_action, &QAction::triggered, this, [this] {
+ const auto cheats_dir = Common::FS::GetCitronPath(Common::FS::CitronPath::LoadDir) /
+ fmt::format("{:016X}", title_id) / "cheats";
+ QDir().mkpath(QString::fromStdString(cheats_dir.string()));
+ QDesktopServices::openUrl(QUrl::fromLocalFile(QString::fromStdString(cheats_dir.string())));
+ });
+
+ context_menu.exec(tree_view->viewport()->mapToGlobal(pos));
+}
+
+void ConfigurePerGameCheats::RefreshCheats() {
+ LoadConfiguration();
+}
diff --git a/src/citron/configuration/configure_per_game_cheats.h b/src/citron/configuration/configure_per_game_cheats.h
index 1794b69c5..53cfe3de9 100644
--- a/src/citron/configuration/configure_per_game_cheats.h
+++ b/src/citron/configuration/configure_per_game_cheats.h
@@ -51,6 +51,8 @@ private:
void SaveCheatSettings();
void SetAllCheats(bool enabled);
void ReloadCheatEngine() const;
+ void OnContextMenu(const QPoint& pos);
+ void RefreshCheats();
std::unique_ptr ui;
FileSys::VirtualFile file;
@@ -61,6 +63,7 @@ private:
QPushButton* enable_all_button;
QPushButton* disable_all_button;
QPushButton* save_button;
+ QPushButton* refresh_button;
QVBoxLayout* layout;
QTreeView* tree_view;
diff --git a/src/citron/configuration/qt_config.h b/src/citron/configuration/qt_config.h
index 583985780..3d9eda4b5 100644
--- a/src/citron/configuration/qt_config.h
+++ b/src/citron/configuration/qt_config.h
@@ -44,7 +44,7 @@ protected:
void SaveUIValues() override;
void SaveUIGamelistValues() override;
void SaveUILayoutValues() override;
- void SaveMultiplayerValues();
+ void SaveMultiplayerValues() override;
void SaveNetworkValues();
public:
diff --git a/src/citron/game_list.cpp b/src/citron/game_list.cpp
index 6320ad473..bd13eae1a 100644
--- a/src/citron/game_list.cpp
+++ b/src/citron/game_list.cpp
@@ -2,64 +2,62 @@
// SPDX-FileCopyrightText: 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
+#include
#include
+#include
#include
#include
+#include
+#include
#include
+#include
+#include
+#include
#include
+#include
+#include
#include
#include
#include
#include
#include
+#include
#include
#include
-#include
-#include
#include
-#include
#include
#include
-#include
-#include
-#include
-#include
#include
-#include
-#include
+#include
+#include
+#include
+#include
#include
+#include
+#include
#include
#include
+#include
#include
#include
#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
#include
-#include "common/common_types.h"
-#include "common/logging/log.h"
-#include "common/string_util.h"
-#include "core/core.h"
-#include "core/file_sys/patch_manager.h"
-#include "core/file_sys/registered_cache.h"
-#include "core/file_sys/savedata_factory.h"
#include "citron/compatibility_list.h"
-#include "common/fs/path_util.h"
-#include "core/hle/service/acc/profile_manager.h"
#include "citron/game_list.h"
#include "citron/game_list_p.h"
#include "citron/game_list_worker.h"
#include "citron/main.h"
#include "citron/uisettings.h"
#include "citron/util/controller_navigation.h"
+#include "common/common_types.h"
+#include "common/fs/path_util.h"
+#include "common/logging/log.h"
+#include "common/string_util.h"
+#include "core/core.h"
+#include "core/file_sys/patch_manager.h"
+#include "core/file_sys/registered_cache.h"
+#include "core/file_sys/savedata_factory.h"
+#include "core/hle/service/acc/profile_manager.h"
// A helper struct to cleanly pass game data
struct SurpriseGame {
@@ -84,7 +82,9 @@ public:
update();
}
- qreal getScrollOffset() const { return m_scroll_offset; }
+ qreal getScrollOffset() const {
+ return m_scroll_offset;
+ }
void setScrollOffset(qreal offset) {
m_scroll_offset = offset;
update();
@@ -92,7 +92,8 @@ public:
protected:
void paintEvent(QPaintEvent* event) override {
- if (m_games.isEmpty()) return;
+ if (m_games.isEmpty())
+ return;
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
@@ -108,7 +109,8 @@ protected:
painter.fillRect(widget_center_x - 2, 0, 4, height(), highlight_color);
for (int i = 0; i < m_games.size(); ++i) {
- const qreal icon_x_position = (widget_center_x - icon_size / 2) + (i * total_slot_width) - m_scroll_offset;
+ const qreal icon_x_position =
+ (widget_center_x - icon_size / 2) + (i * total_slot_width) - m_scroll_offset;
const int draw_x = static_cast(icon_x_position);
const int draw_y = widget_center_y - (icon_size / 2);
@@ -139,7 +141,8 @@ class SurpriseMeDialog : public QDialog {
public:
explicit SurpriseMeDialog(QVector games, QWidget* parent = nullptr)
- : QDialog(parent), m_available_games(games), m_last_choice({QString(), QString(), 0, QPixmap()}) {
+ : QDialog(parent), m_available_games(games),
+ m_last_choice({QString(), QString(), 0, QPixmap()}) {
setWindowTitle(tr("Surprise Me!"));
setModal(true);
setFixedSize(540, 280);
@@ -189,7 +192,9 @@ public:
QTimer::singleShot(100, this, &SurpriseMeDialog::startRoll);
}
- const SurpriseGame& getFinalChoice() const { return m_last_choice; }
+ const SurpriseGame& getFinalChoice() const {
+ return m_last_choice;
+ }
private slots:
void startRoll() {
@@ -214,9 +219,11 @@ private slots:
QVector reel;
if (!m_available_games.isEmpty()) {
std::uniform_int_distribution<> filler_distrib(0, m_available_games.size() - 1);
- for (int i = 0; i < 20; ++i) reel.push_back(m_available_games.at(filler_distrib(gen)));
+ for (int i = 0; i < 20; ++i)
+ reel.push_back(m_available_games.at(filler_distrib(gen)));
reel.push_back(winner);
- for (int i = 0; i < 20; ++i) reel.push_back(m_available_games.at(filler_distrib(gen)));
+ for (int i = 0; i < 20; ++i)
+ reel.push_back(m_available_games.at(filler_distrib(gen)));
} else {
reel.push_back(winner);
}
@@ -254,7 +261,9 @@ private slots:
}
}
- void onLaunch() { accept(); }
+ void onLaunch() {
+ accept();
+ }
private:
QVector m_available_games;
@@ -267,7 +276,8 @@ private:
};
// Static helper for Save Detection
-static QString GetDetectedEmulatorName(const QString& path, u64 program_id, const QString& citron_nand_base) {
+static QString GetDetectedEmulatorName(const QString& path, u64 program_id,
+ const QString& citron_nand_base) {
QString abs_path = QDir(path).absolutePath();
QString citron_abs_base = QDir(citron_nand_base).absolutePath();
QString tid_str = QStringLiteral("%1").arg(program_id, 16, 16, QLatin1Char('0'));
@@ -279,21 +289,29 @@ static QString GetDetectedEmulatorName(const QString& path, u64 program_id, cons
// Ryujinx
if (abs_path.contains(QStringLiteral("bis/user/save"), Qt::CaseInsensitive)) {
- if (abs_path.contains(QStringLiteral("ryubing"), Qt::CaseInsensitive)) return QStringLiteral("Ryubing");
- if (abs_path.contains(QStringLiteral("ryujinx"), Qt::CaseInsensitive)) return QStringLiteral("Ryujinx");
+ if (abs_path.contains(QStringLiteral("ryubing"), Qt::CaseInsensitive))
+ return QStringLiteral("Ryubing");
+ if (abs_path.contains(QStringLiteral("ryujinx"), Qt::CaseInsensitive))
+ return QStringLiteral("Ryujinx");
// Fallback if it's a generic Ryujinx-structure folder
- return abs_path.contains(tid_str, Qt::CaseInsensitive) ? QStringLiteral("Ryujinx/Ryubing") : QStringLiteral("Ryujinx/Ryubing (Manual Slot)");
+ return abs_path.contains(tid_str, Qt::CaseInsensitive)
+ ? QStringLiteral("Ryujinx/Ryubing")
+ : QStringLiteral("Ryujinx/Ryubing (Manual Slot)");
}
// Fork
if (abs_path.contains(QStringLiteral("nand/user/save"), Qt::CaseInsensitive) ||
abs_path.contains(QStringLiteral("nand/system/Containers"), Qt::CaseInsensitive)) {
- if (abs_path.contains(QStringLiteral("eden"), Qt::CaseInsensitive)) return QStringLiteral("Eden");
- if (abs_path.contains(QStringLiteral("suyu"), Qt::CaseInsensitive)) return QStringLiteral("Suyu");
- if (abs_path.contains(QStringLiteral("sudachi"), Qt::CaseInsensitive)) return QStringLiteral("Sudachi");
- if (abs_path.contains(QStringLiteral("yuzu"), Qt::CaseInsensitive)) return QStringLiteral("Yuzu");
+ if (abs_path.contains(QStringLiteral("eden"), Qt::CaseInsensitive))
+ return QStringLiteral("Eden");
+ if (abs_path.contains(QStringLiteral("suyu"), Qt::CaseInsensitive))
+ return QStringLiteral("Suyu");
+ if (abs_path.contains(QStringLiteral("sudachi"), Qt::CaseInsensitive))
+ return QStringLiteral("Sudachi");
+ if (abs_path.contains(QStringLiteral("yuzu"), Qt::CaseInsensitive))
+ return QStringLiteral("Yuzu");
return QStringLiteral("another emulator");
}
@@ -302,7 +320,7 @@ static QString GetDetectedEmulatorName(const QString& path, u64 program_id, cons
}
GameListSearchField::KeyReleaseEater::KeyReleaseEater(GameList* gamelist_, QObject* parent)
-: QObject(parent), gamelist{gamelist_} {}
+ : QObject(parent), gamelist{gamelist_} {}
// EventFilter in order to process systemkeys while editing the searchfield
bool GameListSearchField::KeyReleaseEater::eventFilter(QObject* obj, QEvent* event) {
@@ -317,29 +335,29 @@ bool GameListSearchField::KeyReleaseEater::eventFilter(QObject* obj, QEvent* eve
// If no function key changes the searchfield's text the filter doesn't need to get reloaded
if (edit_filter_text == edit_filter_text_old) {
switch (keyEvent->key()) {
- case Qt::Key_Escape: {
- if (edit_filter_text_old.isEmpty()) {
- return QObject::eventFilter(obj, event);
- } else {
- gamelist->search_field->edit_filter->clear();
- edit_filter_text.clear();
- }
- break;
- }
- case Qt::Key_Return:
- case Qt::Key_Enter: {
- if (gamelist->search_field->visible == 1) {
- const QString file_path = gamelist->GetLastFilterResultItem();
- gamelist->search_field->edit_filter->clear();
- edit_filter_text.clear();
- emit gamelist->GameChosen(file_path);
- } else {
- return QObject::eventFilter(obj, event);
- }
- break;
- }
- default:
+ case Qt::Key_Escape: {
+ if (edit_filter_text_old.isEmpty()) {
return QObject::eventFilter(obj, event);
+ } else {
+ gamelist->search_field->edit_filter->clear();
+ edit_filter_text.clear();
+ }
+ break;
+ }
+ case Qt::Key_Return:
+ case Qt::Key_Enter: {
+ if (gamelist->search_field->visible == 1) {
+ const QString file_path = gamelist->GetLastFilterResultItem();
+ gamelist->search_field->edit_filter->clear();
+ edit_filter_text.clear();
+ emit gamelist->GameChosen(file_path);
+ } else {
+ return QObject::eventFilter(obj, event);
+ }
+ break;
+ }
+ default:
+ return QObject::eventFilter(obj, event);
}
}
edit_filter_text_old = edit_filter_text;
@@ -398,8 +416,8 @@ GameListSearchField::GameListSearchField(GameList* parent) : QWidget{parent} {
button_filter_close = new QToolButton(this);
button_filter_close->setText(QStringLiteral("X"));
button_filter_close->setCursor(Qt::ArrowCursor);
- button_filter_close->setStyleSheet(
- QStringLiteral("QToolButton{ border: 1px solid palette(mid); border-radius: 4px; padding: 4px 8px; color: "
+ button_filter_close->setStyleSheet(QStringLiteral(
+ "QToolButton{ border: 1px solid palette(mid); border-radius: 4px; padding: 4px 8px; color: "
"palette(text); font-weight: bold; background: palette(button); }"
"QToolButton:hover{ border: 1px solid palette(highlight); color: "
"palette(highlighted-text); background: palette(highlight)}"));
@@ -422,8 +440,8 @@ static bool ContainsAllWords(const QString& haystack, const QString& userinput)
void GameList::OnItemExpanded(const QModelIndex& item) {
const auto type = item.data(GameListItem::TypeRole).value();
const bool is_dir = type == GameListItemType::CustomDir || type == GameListItemType::SdmcDir ||
- type == GameListItemType::UserNandDir ||
- type == GameListItemType::SysNandDir;
+ type == GameListItemType::UserNandDir ||
+ type == GameListItemType::SysNandDir;
const bool is_fave = type == GameListItemType::Favorites;
if (!is_dir && !is_fave) {
return;
@@ -469,23 +487,33 @@ void GameList::FilterGridView(const QString& filter_text) {
int total_count = 0;
for (int i = 0; i < hierarchical_model->rowCount(); ++i) {
QStandardItem* folder = hierarchical_model->item(i, 0);
- if (!folder || folder->data(GameListItem::TypeRole).value() == GameListItemType::AddDir) {
+ if (!folder || folder->data(GameListItem::TypeRole).value() ==
+ GameListItemType::AddDir) {
continue;
}
for (int j = 0; j < folder->rowCount(); ++j) {
QStandardItem* game_item = folder->child(j, 0);
- if (!game_item || game_item->data(GameListItem::TypeRole).value() != GameListItemType::Game) continue;
+ if (!game_item || game_item->data(GameListItem::TypeRole).value() !=
+ GameListItemType::Game)
+ continue;
total_count++;
const QString full_path = game_item->data(GameListItemPath::FullPathRole).toString();
bool should_show = !UISettings::values.hidden_paths.contains(full_path);
if (should_show && !filter_text.isEmpty()) {
- const QString file_title = game_item->data(GameListItemPath::TitleRole).toString().toLower();
- const auto program_id = game_item->data(GameListItemPath::ProgramIdRole).toULongLong();
- const QString file_program_id = QStringLiteral("%1").arg(program_id, 16, 16, QLatin1Char('0'));
- const QString file_name = full_path.mid(full_path.lastIndexOf(QLatin1Char{'/'}) + 1).toLower() + QLatin1Char{' '} + file_title;
- should_show = ContainsAllWords(file_name, filter_text) || (file_program_id.size() == 16 && file_program_id.contains(filter_text));
+ const QString file_title =
+ game_item->data(GameListItemPath::TitleRole).toString().toLower();
+ const auto program_id =
+ game_item->data(GameListItemPath::ProgramIdRole).toULongLong();
+ const QString file_program_id =
+ QStringLiteral("%1").arg(program_id, 16, 16, QLatin1Char('0'));
+ const QString file_name =
+ full_path.mid(full_path.lastIndexOf(QLatin1Char{'/'}) + 1).toLower() +
+ QLatin1Char{' '} + file_title;
+ should_show =
+ ContainsAllWords(file_name, filter_text) ||
+ (file_program_id.size() == 16 && file_program_id.contains(filter_text));
}
if (should_show) {
@@ -515,10 +543,11 @@ void GameList::FilterGridView(const QString& filter_text) {
if (icon_data.isValid() && icon_data.canConvert()) {
QPixmap pixmap = icon_data.value();
if (!pixmap.isNull()) {
- #ifdef __linux__
- QPixmap scaled = pixmap.scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
+#ifdef __linux__
+ QPixmap scaled = pixmap.scaled(icon_size, icon_size, Qt::IgnoreAspectRatio,
+ Qt::SmoothTransformation);
item->setData(scaled, Qt::DecorationRole);
- #else
+#else
QPixmap rounded(icon_size, icon_size);
rounded.fill(Qt::transparent);
QPainter painter(&rounded);
@@ -527,10 +556,11 @@ void GameList::FilterGridView(const QString& filter_text) {
QPainterPath path;
path.addRoundedRect(0, 0, icon_size, icon_size, radius, radius);
painter.setClipPath(path);
- QPixmap scaled = pixmap.scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
+ QPixmap scaled = pixmap.scaled(icon_size, icon_size, Qt::IgnoreAspectRatio,
+ Qt::SmoothTransformation);
painter.drawPixmap(0, 0, scaled);
item->setData(rounded, Qt::DecorationRole);
- #endif
+#endif
}
}
}
@@ -542,16 +572,20 @@ void GameList::FilterTreeView(const QString& filter_text) {
int visible_count = 0;
int total_count = 0;
- tree_view->setRowHidden(0, item_model->invisibleRootItem()->index(), filter_text.isEmpty() ? (UISettings::values.favorited_ids.size() == 0) : true);
+ tree_view->setRowHidden(0, item_model->invisibleRootItem()->index(),
+ filter_text.isEmpty() ? (UISettings::values.favorited_ids.size() == 0)
+ : true);
for (int i = 0; i < item_model->rowCount(); ++i) {
QStandardItem* folder = item_model->item(i, 0);
- if (!folder) continue;
+ if (!folder)
+ continue;
const QModelIndex folder_index = folder->index();
for (int j = 0; j < folder->rowCount(); ++j) {
const QStandardItem* child = folder->child(j, 0);
- if (!child) continue;
+ if (!child)
+ continue;
total_count++;
const QString full_path = child->data(GameListItemPath::FullPathRole).toString();
@@ -560,10 +594,16 @@ void GameList::FilterTreeView(const QString& filter_text) {
if (!filter_text.isEmpty()) {
const auto program_id = child->data(GameListItemPath::ProgramIdRole).toULongLong();
- const QString file_title = child->data(GameListItemPath::TitleRole).toString().toLower();
- const QString file_program_id = QStringLiteral("%1").arg(program_id, 16, 16, QLatin1Char('0'));
- const QString file_name = full_path.mid(full_path.lastIndexOf(QLatin1Char{'/'}) + 1).toLower() + QLatin1Char{' '} + file_title;
- matches_filter = ContainsAllWords(file_name, filter_text) || (file_program_id.size() == 16 && file_program_id.contains(filter_text));
+ const QString file_title =
+ child->data(GameListItemPath::TitleRole).toString().toLower();
+ const QString file_program_id =
+ QStringLiteral("%1").arg(program_id, 16, 16, QLatin1Char('0'));
+ const QString file_name =
+ full_path.mid(full_path.lastIndexOf(QLatin1Char{'/'}) + 1).toLower() +
+ QLatin1Char{' '} + file_title;
+ matches_filter =
+ ContainsAllWords(file_name, filter_text) ||
+ (file_program_id.size() == 16 && file_program_id.contains(filter_text));
}
if (!is_hidden_by_user && matches_filter) {
@@ -582,29 +622,55 @@ void GameList::OnUpdateThemedIcons() {
QStandardItem* child = item_model->invisibleRootItem()->child(i);
const int icon_size = UISettings::values.folder_icon_size.GetValue();
switch (child->data(GameListItem::TypeRole).value()) {
- case GameListItemType::SdmcDir:
- child->setData(QIcon::fromTheme(QStringLiteral("sd_card")).pixmap(icon_size).scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation), Qt::DecorationRole);
- break;
- case GameListItemType::UserNandDir:
- child->setData(QIcon::fromTheme(QStringLiteral("chip")).pixmap(icon_size).scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation), Qt::DecorationRole);
- break;
- case GameListItemType::SysNandDir:
- child->setData(QIcon::fromTheme(QStringLiteral("chip")).pixmap(icon_size).scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation), Qt::DecorationRole);
- break;
- case GameListItemType::CustomDir: {
- const UISettings::GameDir& game_dir = UISettings::values.game_dirs[child->data(GameListDir::GameDirRole).toInt()];
- const QString icon_name = QFileInfo::exists(QString::fromStdString(game_dir.path)) ? QStringLiteral("folder") : QStringLiteral("bad_folder");
- child->setData(QIcon::fromTheme(icon_name).pixmap(icon_size).scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation), Qt::DecorationRole);
- break;
- }
- case GameListItemType::AddDir:
- child->setData(QIcon::fromTheme(QStringLiteral("list-add")).pixmap(icon_size).scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation), Qt::DecorationRole);
- break;
- case GameListItemType::Favorites:
- child->setData(QIcon::fromTheme(QStringLiteral("star")).pixmap(icon_size).scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation), Qt::DecorationRole);
- break;
- default:
- break;
+ case GameListItemType::SdmcDir:
+ child->setData(
+ QIcon::fromTheme(QStringLiteral("sd_card"))
+ .pixmap(icon_size)
+ .scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation),
+ Qt::DecorationRole);
+ break;
+ case GameListItemType::UserNandDir:
+ child->setData(
+ QIcon::fromTheme(QStringLiteral("chip"))
+ .pixmap(icon_size)
+ .scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation),
+ Qt::DecorationRole);
+ break;
+ case GameListItemType::SysNandDir:
+ child->setData(
+ QIcon::fromTheme(QStringLiteral("chip"))
+ .pixmap(icon_size)
+ .scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation),
+ Qt::DecorationRole);
+ break;
+ case GameListItemType::CustomDir: {
+ const UISettings::GameDir& game_dir =
+ UISettings::values.game_dirs[child->data(GameListDir::GameDirRole).toInt()];
+ const QString icon_name = QFileInfo::exists(QString::fromStdString(game_dir.path))
+ ? QStringLiteral("folder")
+ : QStringLiteral("bad_folder");
+ child->setData(
+ QIcon::fromTheme(icon_name).pixmap(icon_size).scaled(
+ icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation),
+ Qt::DecorationRole);
+ break;
+ }
+ case GameListItemType::AddDir:
+ child->setData(
+ QIcon::fromTheme(QStringLiteral("list-add"))
+ .pixmap(icon_size)
+ .scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation),
+ Qt::DecorationRole);
+ break;
+ case GameListItemType::Favorites:
+ child->setData(
+ QIcon::fromTheme(QStringLiteral("star"))
+ .pixmap(icon_size)
+ .scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation),
+ Qt::DecorationRole);
+ break;
+ default:
+ break;
}
}
}
@@ -613,11 +679,12 @@ void GameList::OnFilterCloseClicked() {
main_window->filterBarSetChecked(false);
}
-GameList::GameList(std::shared_ptr vfs_, FileSys::ManualContentProvider* provider_,
+GameList::GameList(std::shared_ptr vfs_,
+ FileSys::ManualContentProvider* provider_,
PlayTime::PlayTimeManager& play_time_manager_, Core::System& system_,
GMainWindow* parent)
-: QWidget{parent}, vfs{std::move(vfs_)}, provider{provider_},
-play_time_manager{play_time_manager_}, system{system_} {
+ : QWidget{parent}, vfs{std::move(vfs_)}, provider{provider_},
+ play_time_manager{play_time_manager_}, system{system_} {
watcher = new QFileSystemWatcher(this);
connect(watcher, &QFileSystemWatcher::directoryChanged, this, &GameList::RefreshGameDirectory);
@@ -649,7 +716,9 @@ play_time_manager{play_time_manager_}, system{system_} {
list_view->setHorizontalScrollMode(QAbstractItemView::ScrollPerPixel);
list_view->setEditTriggers(QAbstractItemView::NoEditTriggers);
list_view->setContextMenuPolicy(Qt::CustomContextMenu);
- list_view->setStyleSheet(QStringLiteral("QListView{ border: none; background: transparent; } QListView::item { text-align: center; padding: 5px; }"));
+ list_view->setStyleSheet(
+ QStringLiteral("QListView{ border: none; background: transparent; } QListView::item { "
+ "text-align: center; padding: 5px; }"));
list_view->setGridSize(QSize(140, 160));
list_view->setSpacing(10);
list_view->setWordWrap(true);
@@ -671,27 +740,29 @@ play_time_manager{play_time_manager_}, system{system_} {
connect(tree_view, &QTreeView::expanded, this, &GameList::OnItemExpanded);
connect(tree_view, &QTreeView::collapsed, this, &GameList::OnItemExpanded);
// Sync sort button with Name column header sort order
- connect(tree_view->header(), &QHeaderView::sortIndicatorChanged, [this](int logicalIndex, Qt::SortOrder order) {
- if (logicalIndex == COLUMN_NAME) {
- current_sort_order = order;
- UpdateSortButtonIcon();
- }
- });
+ connect(tree_view->header(), &QHeaderView::sortIndicatorChanged,
+ [this](int logicalIndex, Qt::SortOrder order) {
+ if (logicalIndex == COLUMN_NAME) {
+ current_sort_order = order;
+ UpdateSortButtonIcon();
+ }
+ });
connect(list_view, &QListView::activated, this, &GameList::ValidateEntry);
connect(list_view, &QListView::customContextMenuRequested, this, &GameList::PopupContextMenu);
- connect(controller_navigation, &ControllerNavigation::TriggerKeyboardEvent, [this](Qt::Key key) {
- if (system.IsPoweredOn() || !this->isActiveWindow()) {
- return;
- }
- QKeyEvent* event = new QKeyEvent(QEvent::KeyPress, key, Qt::NoModifier);
- if (tree_view->isVisible() && tree_view->model()) {
- QCoreApplication::postEvent(tree_view, event);
- }
- if (list_view->isVisible() && list_view->model()) {
- QKeyEvent* list_event = new QKeyEvent(QEvent::KeyPress, key, Qt::NoModifier);
- QCoreApplication::postEvent(list_view, list_event);
- }
- });
+ connect(controller_navigation, &ControllerNavigation::TriggerKeyboardEvent,
+ [this](Qt::Key key) {
+ if (system.IsPoweredOn() || !this->isActiveWindow()) {
+ return;
+ }
+ QKeyEvent* event = new QKeyEvent(QEvent::KeyPress, key, Qt::NoModifier);
+ if (tree_view->isVisible() && tree_view->model()) {
+ QCoreApplication::postEvent(tree_view, event);
+ }
+ if (list_view->isVisible() && list_view->model()) {
+ QKeyEvent* list_event = new QKeyEvent(QEvent::KeyPress, key, Qt::NoModifier);
+ QCoreApplication::postEvent(list_view, list_event);
+ }
+ });
qRegisterMetaType>("QList");
qRegisterMetaType>>("std::map>");
@@ -718,20 +789,18 @@ play_time_manager{play_time_manager_}, system{system_} {
btn_list_view->setAutoRaise(true);
btn_list_view->setIconSize(QSize(16, 16));
btn_list_view->setFixedSize(32, 32);
- btn_list_view->setStyleSheet(QStringLiteral(
- "QToolButton {"
- " border: 1px solid palette(mid);"
- " border-radius: 4px;"
- " background: palette(button);"
- "}"
- "QToolButton:hover {"
- " background: palette(light);"
- "}"
- "QToolButton:checked {"
- " background: palette(highlight);"
- " border-color: palette(highlight);"
- "}"
- ));
+ btn_list_view->setStyleSheet(QStringLiteral("QToolButton {"
+ " border: 1px solid palette(mid);"
+ " border-radius: 4px;"
+ " background: palette(button);"
+ "}"
+ "QToolButton:hover {"
+ " background: palette(light);"
+ "}"
+ "QToolButton:checked {"
+ " background: palette(highlight);"
+ " border-color: palette(highlight);"
+ "}"));
connect(btn_list_view, &QToolButton::clicked, [this]() {
SetViewMode(false);
btn_list_view->setChecked(true);
@@ -754,20 +823,18 @@ play_time_manager{play_time_manager_}, system{system_} {
btn_grid_view->setAutoRaise(true);
btn_grid_view->setIconSize(QSize(16, 16));
btn_grid_view->setFixedSize(32, 32);
- btn_grid_view->setStyleSheet(QStringLiteral(
- "QToolButton {"
- " border: 1px solid palette(mid);"
- " border-radius: 4px;"
- " background: palette(button);"
- "}"
- "QToolButton:hover {"
- " background: palette(light);"
- "}"
- "QToolButton:checked {"
- " background: palette(highlight);"
- " border-color: palette(highlight);"
- "}"
- ));
+ btn_grid_view->setStyleSheet(QStringLiteral("QToolButton {"
+ " border: 1px solid palette(mid);"
+ " border-radius: 4px;"
+ " background: palette(button);"
+ "}"
+ "QToolButton:hover {"
+ " background: palette(light);"
+ "}"
+ "QToolButton:checked {"
+ " background: palette(highlight);"
+ " border-color: palette(highlight);"
+ "}"));
connect(btn_grid_view, &QToolButton::clicked, [this]() {
SetViewMode(true);
btn_list_view->setChecked(false);
@@ -782,94 +849,101 @@ play_time_manager{play_time_manager_}, system{system_} {
slider_title_size->setToolTip(tr("Game Icon Size"));
slider_title_size->setMaximumWidth(120);
slider_title_size->setMinimumWidth(120);
- slider_title_size->setStyleSheet(QStringLiteral(
- "QSlider::groove:horizontal {"
- " border: 1px solid palette(mid);"
- " height: 4px;"
- " background: palette(base);"
- " border-radius: 2px;"
- "}"
- "QSlider::handle:horizontal {"
- " background: palette(button);"
- " border: 1px solid palette(mid);"
- " width: 12px;"
- " height: 12px;"
- " margin: -4px 0;"
- " border-radius: 6px;"
- "}"
- "QSlider::handle:horizontal:hover {"
- " background: palette(light);"
- "}"
- ));
+ slider_title_size->setStyleSheet(QStringLiteral("QSlider::groove:horizontal {"
+ " border: 1px solid palette(mid);"
+ " height: 4px;"
+ " background: palette(base);"
+ " border-radius: 2px;"
+ "}"
+ "QSlider::handle:horizontal {"
+ " background: palette(button);"
+ " border: 1px solid palette(mid);"
+ " width: 12px;"
+ " height: 12px;"
+ " margin: -4px 0;"
+ " border-radius: 6px;"
+ "}"
+ "QSlider::handle:horizontal:hover {"
+ " background: palette(light);"
+ "}"));
connect(slider_title_size, &QSlider::valueChanged, [this](int value) {
- // Update title font size in tree view
- QFont font = tree_view->font();
- font.setPointSize(qBound(8, value / 8, 24));
- tree_view->setFont(font);
+ // Update title font size in tree view
+ QFont font = tree_view->font();
+ font.setPointSize(qBound(8, value / 8, 24));
+ tree_view->setFont(font);
#ifndef __linux__
- // On non-Linux platforms, also update game icon size and repaint grid view
- UISettings::values.game_icon_size.SetValue(static_cast(value));
- if (list_view->isVisible()) {
- QAbstractItemModel* current_model = list_view->model();
- if (current_model && current_model != item_model) {
- QStandardItemModel* flat_model = qobject_cast(current_model);
- if (flat_model) {
- const u32 icon_size = static_cast(value);
- list_view->setGridSize(QSize(icon_size + 60, icon_size + 80));
- int scroll_position = list_view->verticalScrollBar()->value();
- QModelIndex current_index = list_view->currentIndex();
+ // On non-Linux platforms, also update game icon size and repaint grid view
+ UISettings::values.game_icon_size.SetValue(static_cast(value));
+ if (list_view->isVisible()) {
+ QAbstractItemModel* current_model = list_view->model();
+ if (current_model && current_model != item_model) {
+ QStandardItemModel* flat_model = qobject_cast(current_model);
+ if (flat_model) {
+ const u32 icon_size = static_cast(value);
+ list_view->setGridSize(QSize(icon_size + 60, icon_size + 80));
+ int scroll_position = list_view->verticalScrollBar()->value();
+ QModelIndex current_index = list_view->currentIndex();
- for (int i = 0; i < flat_model->rowCount(); ++i) {
- QStandardItem* item = flat_model->item(i);
- if (item) {
- u64 program_id = item->data(GameListItemPath::ProgramIdRole).toULongLong();
- QStandardItem* original_item = nullptr;
- for (int folder_idx = 0; folder_idx < item_model->rowCount(); ++folder_idx) {
- QStandardItem* folder = item_model->item(folder_idx, 0);
- if (!folder) continue;
- for (int game_idx = 0; game_idx < folder->rowCount(); ++game_idx) {
- QStandardItem* game = folder->child(game_idx, 0);
- if (game && game->data(GameListItemPath::ProgramIdRole).toULongLong() == program_id) {
- original_item = game;
- break;
+ for (int i = 0; i < flat_model->rowCount(); ++i) {
+ QStandardItem* item = flat_model->item(i);
+ if (item) {
+ u64 program_id =
+ item->data(GameListItemPath::ProgramIdRole).toULongLong();
+ QStandardItem* original_item = nullptr;
+ for (int folder_idx = 0; folder_idx < item_model->rowCount();
+ ++folder_idx) {
+ QStandardItem* folder = item_model->item(folder_idx, 0);
+ if (!folder)
+ continue;
+ for (int game_idx = 0; game_idx < folder->rowCount(); ++game_idx) {
+ QStandardItem* game = folder->child(game_idx, 0);
+ if (game &&
+ game->data(GameListItemPath::ProgramIdRole).toULongLong() ==
+ program_id) {
+ original_item = game;
+ break;
+ }
}
+ if (original_item)
+ break;
}
- if (original_item) break;
- }
- if (original_item) {
- QVariant orig_icon_data = original_item->data(Qt::DecorationRole);
- if (orig_icon_data.isValid() && orig_icon_data.type() == QVariant::Pixmap) {
- QPixmap orig_pixmap = orig_icon_data.value();
- QPixmap rounded(icon_size, icon_size);
- rounded.fill(Qt::transparent);
- QPainter painter(&rounded);
- painter.setRenderHint(QPainter::Antialiasing);
- const int radius = icon_size / 8;
- QPainterPath path;
- path.addRoundedRect(0, 0, icon_size, icon_size, radius, radius);
- painter.setClipPath(path);
- QPixmap scaled = orig_pixmap.scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
- painter.drawPixmap(0, 0, scaled);
- item->setData(rounded, Qt::DecorationRole);
+ if (original_item) {
+ QVariant orig_icon_data = original_item->data(Qt::DecorationRole);
+ if (orig_icon_data.isValid() &&
+ orig_icon_data.type() == QVariant::Pixmap) {
+ QPixmap orig_pixmap = orig_icon_data.value();
+ QPixmap rounded(icon_size, icon_size);
+ rounded.fill(Qt::transparent);
+ QPainter painter(&rounded);
+ painter.setRenderHint(QPainter::Antialiasing);
+ const int radius = icon_size / 8;
+ QPainterPath path;
+ path.addRoundedRect(0, 0, icon_size, icon_size, radius, radius);
+ painter.setClipPath(path);
+ QPixmap scaled = orig_pixmap.scaled(icon_size, icon_size,
+ Qt::IgnoreAspectRatio,
+ Qt::SmoothTransformation);
+ painter.drawPixmap(0, 0, scaled);
+ item->setData(rounded, Qt::DecorationRole);
+ }
}
}
}
+ if (scroll_position >= 0) {
+ list_view->verticalScrollBar()->setValue(scroll_position);
+ }
+ if (current_index.isValid() && current_index.row() < flat_model->rowCount()) {
+ list_view->setCurrentIndex(flat_model->index(current_index.row(), 0));
+ }
}
- if (scroll_position >= 0) {
- list_view->verticalScrollBar()->setValue(scroll_position);
- }
- if (current_index.isValid() && current_index.row() < flat_model->rowCount()) {
- list_view->setCurrentIndex(flat_model->index(current_index.row(), 0));
- }
+ } else {
+ PopulateGridView();
}
- } else {
- PopulateGridView();
}
- }
#endif
-});
+ });
// A-Z sort button - positioned after slider
btn_sort_az = new QToolButton(toolbar);
@@ -878,16 +952,14 @@ play_time_manager{play_time_manager_}, system{system_} {
btn_sort_az->setAutoRaise(true);
btn_sort_az->setIconSize(QSize(16, 16));
btn_sort_az->setFixedSize(32, 32);
- btn_sort_az->setStyleSheet(QStringLiteral(
- "QToolButton {"
- " border: 1px solid palette(mid);"
- " border-radius: 4px;"
- " background: palette(button);"
- "}"
- "QToolButton:hover {"
- " background: palette(light);"
- "}"
- ));
+ btn_sort_az->setStyleSheet(QStringLiteral("QToolButton {"
+ " border: 1px solid palette(mid);"
+ " border-radius: 4px;"
+ " background: palette(button);"
+ "}"
+ "QToolButton:hover {"
+ " background: palette(light);"
+ "}"));
connect(btn_sort_az, &QToolButton::clicked, this, &GameList::ToggleSortOrder);
// Surprise Me button - positioned after sort button
@@ -908,16 +980,14 @@ play_time_manager{play_time_manager_}, system{system_} {
btn_surprise_me->setAutoRaise(true);
btn_surprise_me->setIconSize(QSize(16, 16));
btn_surprise_me->setFixedSize(32, 32);
- btn_surprise_me->setStyleSheet(QStringLiteral(
- "QToolButton {"
- " border: 1px solid palette(mid);"
- " border-radius: 4px;"
- " background: palette(button);"
- "}"
- "QToolButton:hover {"
- " background: palette(light);"
- "}"
- ));
+ btn_surprise_me->setStyleSheet(QStringLiteral("QToolButton {"
+ " border: 1px solid palette(mid);"
+ " border-radius: 4px;"
+ " background: palette(button);"
+ "}"
+ "QToolButton:hover {"
+ " background: palette(light);"
+ "}"));
connect(btn_surprise_me, &QToolButton::clicked, this, &GameList::onSurpriseMeClicked);
// Create progress bar
@@ -925,10 +995,9 @@ play_time_manager{play_time_manager_}, system{system_} {
progress_bar->setVisible(false);
progress_bar->setFixedHeight(4);
progress_bar->setTextVisible(false);
- progress_bar->setStyleSheet(QStringLiteral(
- "QProgressBar { border: none; background: transparent; } "
- "QProgressBar::chunk { background-color: #0078d4; }"
- ));
+ progress_bar->setStyleSheet(
+ QStringLiteral("QProgressBar { border: none; background: transparent; } "
+ "QProgressBar::chunk { background-color: #0078d4; }"));
// Add widgets to toolbar
toolbar_layout->addWidget(btn_list_view);
@@ -958,7 +1027,8 @@ play_time_manager{play_time_manager_}, system{system_} {
connect(&config_update_timer, &QTimer::timeout, this, &GameList::UpdateOnlineStatus);
// This connection handles live updates when OK/Apply is clicked in the config window.
- connect(main_window, &GMainWindow::ConfigurationSaved, this, &GameList::UpdateAccentColorStyles);
+ connect(main_window, &GMainWindow::ConfigurationSaved, this,
+ &GameList::UpdateAccentColorStyles);
network_manager = new QNetworkAccessManager(this);
@@ -1012,7 +1082,9 @@ void GameList::WorkerEvent() {
void GameList::AddDirEntry(GameListDir* entry_items) {
item_model->invisibleRootItem()->appendRow(entry_items);
- tree_view->setExpanded(entry_items->index(), UISettings::values.game_dirs[entry_items->data(GameListDir::GameDirRole).toInt()].expanded);
+ tree_view->setExpanded(
+ entry_items->index(),
+ UISettings::values.game_dirs[entry_items->data(GameListDir::GameDirRole).toInt()].expanded);
}
void GameList::AddEntry(const QList& entry_items, GameListDir* parent) {
@@ -1027,10 +1099,11 @@ void GameList::UpdateOnlineStatus() {
// A watcher gets the result back on the main thread safely
auto online_status_watcher = new QFutureWatcher>>(this);
- connect(online_status_watcher, &QFutureWatcher>>::finished, this, [this, online_status_watcher]() {
- OnOnlineStatusUpdated(online_status_watcher->result());
- online_status_watcher->deleteLater(); // Clean up the watcher
- });
+ connect(online_status_watcher, &QFutureWatcher>>::finished,
+ this, [this, online_status_watcher]() {
+ OnOnlineStatusUpdated(online_status_watcher->result());
+ online_status_watcher->deleteLater(); // Clean up the watcher
+ });
// Run the blocking network call in a background thread using QtConcurrent
QFuture>> future = QtConcurrent::run([session]() {
@@ -1061,11 +1134,14 @@ void GameList::OnOnlineStatusUpdated(const std::map>& o
for (int i = 0; i < item_model->rowCount(); ++i) {
QStandardItem* folder = item_model->item(i, 0);
- if (!folder) continue;
+ if (!folder)
+ continue;
for (int j = 0; j < folder->rowCount(); ++j) {
QStandardItem* game_item = folder->child(j, COLUMN_NAME);
- if (!game_item || game_item->data(GameListItem::TypeRole).value() != GameListItemType::Game) continue;
+ if (!game_item || game_item->data(GameListItem::TypeRole).value() !=
+ GameListItemType::Game)
+ continue;
u64 program_id = game_item->data(GameListItemPath::ProgramIdRole).toULongLong();
QString online_text = QStringLiteral("N/A");
@@ -1073,7 +1149,8 @@ void GameList::OnOnlineStatusUpdated(const std::map>& o
auto it_stats = online_stats.find(program_id);
if (it_stats != online_stats.end()) {
const auto& stats = it_stats->second;
- online_text = QStringLiteral("Players: %1 | Servers: %2").arg(stats.first).arg(stats.second);
+ online_text =
+ QStringLiteral("Players: %1 | Servers: %2").arg(stats.first).arg(stats.second);
}
QStandardItem* online_item = folder->child(j, COLUMN_ONLINE);
@@ -1088,13 +1165,15 @@ void GameList::OnOnlineStatusUpdated(const std::map>& o
void GameList::StartLaunchAnimation(const QModelIndex& item) {
const QString file_path = item.data(GameListItemPath::FullPathRole).toString();
- if (file_path.isEmpty()) return;
+ if (file_path.isEmpty())
+ return;
u64 program_id = item.data(GameListItemPath::ProgramIdRole).toULongLong();
QStandardItem* original_item = nullptr;
for (int folder_idx = 0; folder_idx < item_model->rowCount(); ++folder_idx) {
QStandardItem* folder = item_model->item(folder_idx, 0);
- if (!folder) continue;
+ if (!folder)
+ continue;
for (int game_idx = 0; game_idx < folder->rowCount(); ++game_idx) {
QStandardItem* game = folder->child(game_idx, 0);
if (game && game->data(GameListItemPath::ProgramIdRole).toULongLong() == program_id) {
@@ -1102,12 +1181,19 @@ void GameList::StartLaunchAnimation(const QModelIndex& item) {
break;
}
}
- if (original_item) break;
+ if (original_item)
+ break;
}
QPixmap icon;
if (original_item) {
- icon = original_item->data(Qt::DecorationRole).value();
+ icon = original_item->data(GameListItemPath::HighResIconRole).value();
+ if (icon.isNull()) {
+ icon = original_item->data(Qt::DecorationRole).value();
+ } else {
+ // Apply rounded corners to the high-res icon
+ icon = CreateRoundIcon(icon, 256);
+ }
} else {
// Fallback for safety
icon = item.data(Qt::DecorationRole).value();
@@ -1167,87 +1253,136 @@ void GameList::StartLaunchAnimation(const QModelIndex& item) {
zoom_anim->setEasingCurve(QEasingCurve::OutCubic);
auto* fly_fade_group = new QParallelAnimationGroup;
- auto* effect = new QGraphicsOpacityEffect(animation_label);
- animation_label->setGraphicsEffect(effect);
+ auto* icon_effect = new QGraphicsOpacityEffect(animation_label);
+ animation_label->setGraphicsEffect(icon_effect);
auto* fly_anim = new QPropertyAnimation(animation_label, "geometry");
fly_anim->setDuration(350);
fly_anim->setStartValue(zoom_end_geom);
fly_anim->setEndValue(fly_end_geom);
fly_anim->setEasingCurve(QEasingCurve::InQuad);
- auto* fade_anim = new QPropertyAnimation(effect, "opacity");
- fade_anim->setDuration(350);
- fade_anim->setStartValue(1.0f);
- fade_anim->setEndValue(0.0f);
- fade_anim->setEasingCurve(QEasingCurve::InQuad);
+ auto* icon_fade_anim = new QPropertyAnimation(icon_effect, "opacity");
+ icon_fade_anim->setDuration(350);
+ icon_fade_anim->setStartValue(1.0f);
+ icon_fade_anim->setEndValue(0.0f);
+ icon_fade_anim->setEasingCurve(QEasingCurve::InQuad);
fly_fade_group->addAnimation(fly_anim);
- fly_fade_group->addAnimation(fade_anim);
+ fly_fade_group->addAnimation(icon_fade_anim);
- auto* main_group = new QSequentialAnimationGroup(animation_label);
+ // --- 4. CITRON LOGO TRANSITION ---
+ auto* logo_label = new QLabel(main_window);
+ QPixmap logo_pixmap(QStringLiteral(":/citron.svg"));
+ logo_label->setPixmap(
+ logo_pixmap.scaled(400, 400, Qt::KeepAspectRatio, Qt::SmoothTransformation));
+ logo_label->setFixedSize(400, 400);
+ logo_label->move(center_point.x() - 200, center_point.y() - 200);
+ logo_label->hide();
+
+ auto* logo_effect = new QGraphicsOpacityEffect(logo_label);
+ logo_label->setGraphicsEffect(logo_effect);
+ logo_effect->setOpacity(0.0f);
+
+ auto* logo_fade_in = new QPropertyAnimation(logo_effect, "opacity");
+ logo_fade_in->setDuration(500);
+ logo_fade_in->setStartValue(0.0f);
+ logo_fade_in->setEndValue(1.0f);
+ logo_fade_in->setEasingCurve(QEasingCurve::InOutQuad);
+
+ auto* logo_fade_out = new QPropertyAnimation(logo_effect, "opacity");
+ logo_fade_out->setDuration(500);
+ logo_fade_out->setStartValue(1.0f);
+ logo_fade_out->setEndValue(0.0f);
+ logo_fade_out->setEasingCurve(QEasingCurve::InOutQuad);
+
+ // Overlap the icon "fly-away" and the logo "fade-in"
+ auto* overlap_group = new QParallelAnimationGroup;
+ overlap_group->addAnimation(fly_fade_group);
+
+ auto* logo_fade_in_seq = new QSequentialAnimationGroup;
+ logo_fade_in_seq->addPause(100); // 100ms delay so it starts mid-fly
+ logo_fade_in_seq->addAnimation(logo_fade_in);
+ overlap_group->addAnimation(logo_fade_in_seq);
+
+ auto* main_group = new QSequentialAnimationGroup(this);
main_group->addAnimation(zoom_anim);
main_group->addPause(50);
- main_group->addAnimation(fly_fade_group);
- // When the icon animation finishes, launch the game and clean up.
- // The black overlay will remain until OnEmulationEnded is called.
- connect(main_group, &QSequentialAnimationGroup::finished, this, [this, file_path, title_id, animation_label]() {
- search_field->clear();
- emit GameChosen(file_path, title_id);
- animation_label->deleteLater();
+ // Show logo once zoom is finished, just before fly/fade starts
+ connect(zoom_anim, &QPropertyAnimation::finished, [logo_label]() {
+ logo_label->show();
+ logo_label->raise();
});
+ main_group->addAnimation(overlap_group);
+ main_group->addPause(1000); // Shorter 1 second pause
+ main_group->addAnimation(logo_fade_out);
+
+ // When the animation finishes, launch the game and clean up.
+ connect(main_group, &QSequentialAnimationGroup::finished, this,
+ [this, file_path, title_id, animation_label, logo_label]() {
+ search_field->clear();
+ emit GameChosen(file_path, title_id);
+ animation_label->deleteLater();
+ logo_label->deleteLater();
+ });
+
main_group->start(QAbstractAnimation::DeleteWhenStopped);
}
void GameList::ValidateEntry(const QModelIndex& item) {
const auto selected = item.sibling(item.row(), 0);
switch (selected.data(GameListItem::TypeRole).value()) {
- case GameListItemType::Game: {
- const QString file_path = selected.data(GameListItemPath::FullPathRole).toString();
- if (file_path.isEmpty()) return;
- const QFileInfo file_info(file_path);
- if (!file_info.exists()) return;
+ case GameListItemType::Game: {
+ const QString file_path = selected.data(GameListItemPath::FullPathRole).toString();
+ if (file_path.isEmpty())
+ return;
+ const QFileInfo file_info(file_path);
+ if (!file_info.exists())
+ return;
- // If the entry is a directory (e.g., for homebrew), launch it directly without animation.
- if (file_info.isDir()) {
- const QDir dir{file_path};
- const QStringList matching_main = dir.entryList({QStringLiteral("main")}, QDir::Files);
- if (matching_main.size() == 1) {
- emit GameChosen(dir.path() + QDir::separator() + matching_main[0]);
- }
- return; // Exit here for directories
+ // If the entry is a directory (e.g., for homebrew), launch it directly without animation.
+ if (file_info.isDir()) {
+ const QDir dir{file_path};
+ const QStringList matching_main = dir.entryList({QStringLiteral("main")}, QDir::Files);
+ if (matching_main.size() == 1) {
+ emit GameChosen(dir.path() + QDir::separator() + matching_main[0]);
}
-
- // If it's a standard game file, trigger the new launch animation.
- // The animation function will handle emitting GameChosen when it's finished.
- StartLaunchAnimation(selected);
- break;
+ return; // Exit here for directories
}
- case GameListItemType::AddDir:
- emit AddDirectory();
- if (UISettings::values.prompt_for_autoloader) {
- QMessageBox msg_box(this);
- msg_box.setWindowTitle(tr("Autoloader"));
- msg_box.setText(
- tr("Would you like to use the Autoloader to install all Updates/DLC within your game directories?\n\n"
- "If not now, you can always go to Emulation -> Configure -> Filesystem in order to use this feature. Also, if you have multiple update files for a single game, you can use the Update Manager "
- "in File -> Install Updates with Update Manager."));
- msg_box.setStandardButtons(QMessageBox::Yes | QMessageBox::No);
- QCheckBox* check_box = new QCheckBox(tr("Do not ask me again"));
- msg_box.setCheckBox(check_box);
+ // If it's a standard game file, trigger the new launch animation.
+ // The animation function will handle emitting GameChosen when it's finished.
+ StartLaunchAnimation(selected);
+ break;
+ }
+ case GameListItemType::AddDir:
+ emit AddDirectory();
- if (msg_box.exec() == QMessageBox::Yes) {
- emit RunAutoloaderRequested();
- }
+ if (UISettings::values.prompt_for_autoloader) {
+ QMessageBox msg_box(this);
+ msg_box.setWindowTitle(tr("Autoloader"));
+ msg_box.setText(
+ tr("Would you like to use the Autoloader to install all Updates/DLC within your "
+ "game directories?\n\n"
+ "If not now, you can always go to Emulation -> Configure -> Filesystem in order "
+ "to use this feature. Also, if you have multiple update files for a single "
+ "game, you can use the Update Manager "
+ "in File -> Install Updates with Update Manager."));
+ msg_box.setStandardButtons(QMessageBox::Yes | QMessageBox::No);
+ QCheckBox* check_box = new QCheckBox(tr("Do not ask me again"));
+ msg_box.setCheckBox(check_box);
- if (check_box->isChecked()) {
- UISettings::values.prompt_for_autoloader = false;
- emit SaveConfig();
- }
+ if (msg_box.exec() == QMessageBox::Yes) {
+ emit RunAutoloaderRequested();
}
- break;
- default:
- break;
+
+ if (check_box->isChecked()) {
+ UISettings::values.prompt_for_autoloader = false;
+ emit SaveConfig();
+ }
+ }
+ break;
+ default:
+ break;
}
}
@@ -1255,7 +1390,9 @@ bool GameList::IsEmpty() const {
for (int i = 0; i < item_model->rowCount(); i++) {
const QStandardItem* child = item_model->invisibleRootItem()->child(i);
const auto type = static_cast(child->type());
- if (!child->hasChildren() && (type == GameListItemType::SdmcDir || type == GameListItemType::UserNandDir || type == GameListItemType::SysNandDir)) {
+ if (!child->hasChildren() &&
+ (type == GameListItemType::SdmcDir || type == GameListItemType::UserNandDir ||
+ type == GameListItemType::SysNandDir)) {
item_model->invisibleRootItem()->removeRow(child->row());
i--;
}
@@ -1270,8 +1407,10 @@ void GameList::DonePopulating(const QStringList& watch_list) {
emit ShowList(!IsEmpty());
item_model->invisibleRootItem()->appendRow(new GameListAddDir());
item_model->invisibleRootItem()->insertRow(0, new GameListFavorites());
- tree_view->setRowHidden(0, item_model->invisibleRootItem()->index(), UISettings::values.favorited_ids.size() == 0);
- tree_view->setExpanded(item_model->invisibleRootItem()->child(0)->index(), UISettings::values.favorites_expanded.GetValue());
+ tree_view->setRowHidden(0, item_model->invisibleRootItem()->index(),
+ UISettings::values.favorited_ids.size() == 0);
+ tree_view->setExpanded(item_model->invisibleRootItem()->child(0)->index(),
+ UISettings::values.favorites_expanded.GetValue());
for (const auto id : UISettings::values.favorited_ids) {
AddFavorite(id);
}
@@ -1295,7 +1434,8 @@ void GameList::DonePopulating(const QStringList& watch_list) {
if (children_total > 0) {
search_field->setFocus();
}
- item_model->sort(tree_view->header()->sortIndicatorSection(), tree_view->header()->sortIndicatorOrder());
+ item_model->sort(tree_view->header()->sortIndicatorSection(),
+ tree_view->header()->sortIndicatorOrder());
if (list_view->isVisible()) {
// Preserve filter when repopulating
QString filter_text = search_field->filterText();
@@ -1304,6 +1444,8 @@ void GameList::DonePopulating(const QStringList& watch_list) {
} else {
PopulateGridView();
}
+ } else {
+ FilterTreeView(search_field->filterText());
}
// Only sync if we aren't rebuilding the UI and the game isn't running.
@@ -1316,7 +1458,8 @@ void GameList::DonePopulating(const QStringList& watch_list) {
LOG_INFO(Frontend, "Mirroring: Startup sync already performed this session. Skipping.");
}
} else {
- LOG_INFO(Frontend, "Mirroring: Startup sync skipped (Reason: UI Busy or Game is Emulating).");
+ LOG_INFO(Frontend,
+ "Mirroring: Startup sync skipped (Reason: UI Busy or Game is Emulating).");
}
// Automatically refresh compatibility data from GitHub if enabled
@@ -1334,31 +1477,34 @@ void GameList::PopupContextMenu(const QPoint& menu_location) {
} else {
item = list_view->indexAt(menu_location);
}
- if (!item.isValid()) return;
+ if (!item.isValid())
+ return;
const auto selected = item.sibling(item.row(), 0);
QMenu context_menu;
switch (selected.data(GameListItem::TypeRole).value()) {
- case GameListItemType::Game: {
- const u64 program_id = selected.data(GameListItemPath::ProgramIdRole).toULongLong();
- const std::string path = selected.data(GameListItemPath::FullPathRole).toString().toStdString();
- const QString game_name = selected.data(GameListItemPath::TitleRole).toString();
- AddGamePopup(context_menu, program_id, path, game_name);
- break;
- }
- case GameListItemType::CustomDir:
- AddPermDirPopup(context_menu, selected);
- AddCustomDirPopup(context_menu, selected);
- break;
- case GameListItemType::SdmcDir:
- case GameListItemType::UserNandDir:
- case GameListItemType::SysNandDir:
- AddPermDirPopup(context_menu, selected);
- break;
- case GameListItemType::Favorites:
- AddFavoritesPopup(context_menu);
- break;
- default:
- break;
+ case GameListItemType::Game: {
+ const u64 program_id = selected.data(GameListItemPath::ProgramIdRole).toULongLong();
+ const std::string path =
+ selected.data(GameListItemPath::FullPathRole).toString().toStdString();
+ const QString game_name = selected.data(GameListItemPath::TitleRole).toString();
+ AddGamePopup(context_menu, program_id, path, game_name);
+ break;
+ }
+ case GameListItemType::CustomDir:
+ AddPermDirPopup(context_menu, selected);
+ AddCustomDirPopup(context_menu, selected,
+ false); // Pass false to skip adding "Show Hidden Games"
+ break;
+ case GameListItemType::SdmcDir:
+ case GameListItemType::UserNandDir:
+ case GameListItemType::SysNandDir:
+ AddPermDirPopup(context_menu, selected);
+ break;
+ case GameListItemType::Favorites:
+ AddFavoritesPopup(context_menu);
+ break;
+ default:
+ break;
}
if (tree_view->isVisible()) {
context_menu.exec(tree_view->viewport()->mapToGlobal(menu_location));
@@ -1367,28 +1513,85 @@ void GameList::PopupContextMenu(const QPoint& menu_location) {
}
}
-void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::string& path_str, const QString& game_name) {
+void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::string& path_str,
+ const QString& game_name) {
const QString path = QString::fromStdString(path_str);
const bool is_mirrored = Settings::values.mirrored_save_paths.count(program_id);
const bool has_custom_path = Settings::values.custom_save_paths.count(program_id);
QString mirror_base_path;
+ auto calculateTotalSize = [](const QString& dirPath) -> qint64 {
+ qint64 totalSize = 0;
+ QDirIterator size_it(dirPath, QDirIterator::Subdirectories);
+ while (size_it.hasNext()) {
+ size_it.next();
+ QFileInfo fileInfo = size_it.fileInfo();
+ if (fileInfo.isFile()) {
+ totalSize += fileInfo.size();
+ }
+ }
+ return totalSize;
+ };
+
+ auto copyWithProgress = [calculateTotalSize](const QString& sourceDir, const QString& destDir,
+ QWidget* parent) -> bool {
+ QProgressDialog progress(tr("Moving Save Data..."), QString(), 0, 100, parent);
+ progress.setWindowFlags(Qt::Window | Qt::WindowTitleHint | Qt::CustomizeWindowHint);
+ progress.setWindowModality(Qt::WindowModal);
+ progress.setMinimumDuration(0);
+ progress.setValue(0);
+ qint64 totalSize = calculateTotalSize(sourceDir);
+ qint64 copiedSize = 0;
+ QDir dir(sourceDir);
+ if (!dir.exists())
+ return false;
+ QDir dest_dir(destDir);
+ if (!dest_dir.exists())
+ dest_dir.mkpath(QStringLiteral("."));
+ QDirIterator dir_iter(sourceDir, QDirIterator::Subdirectories);
+ while (dir_iter.hasNext()) {
+ dir_iter.next();
+ const QFileInfo file_info = dir_iter.fileInfo();
+ const QString relative_path = dir.relativeFilePath(file_info.absoluteFilePath());
+ const QString dest_path = QDir(destDir).filePath(relative_path);
+ if (file_info.isDir()) {
+ dest_dir.mkpath(dest_path);
+ } else if (file_info.isFile()) {
+ if (QFile::exists(dest_path))
+ QFile::remove(dest_path);
+ if (!QFile::copy(file_info.absoluteFilePath(), dest_path))
+ return false;
+ copiedSize += file_info.size();
+ if (totalSize > 0) {
+ progress.setValue(static_cast((copiedSize * 100) / totalSize));
+ }
+ }
+ QCoreApplication::processEvents();
+ }
+ progress.setValue(100);
+ return true;
+ };
+
QAction* favorite = context_menu.addAction(tr("Favorite"));
QAction* hide_game = context_menu.addAction(tr("Hide Game"));
context_menu.addSeparator();
QAction* start_game = context_menu.addAction(tr("Start Game"));
- QAction* start_game_global = context_menu.addAction(tr("Start Game without Custom Configuration"));
+ QAction* start_game_global =
+ context_menu.addAction(tr("Start Game without Custom Configuration"));
context_menu.addSeparator();
QAction* open_save_location = context_menu.addAction(tr("Open Save Data Location"));
QAction* open_nand_location = context_menu.addAction(tr("Open NAND Location"));
+ QAction* open_file_location = context_menu.addAction(tr("Open File Location"));
QAction* set_custom_save_path = context_menu.addAction(tr("Set Custom Save Path"));
QAction* remove_custom_save_path = context_menu.addAction(tr("Revert to NAND Save Path"));
QAction* disable_mirroring = context_menu.addAction(tr("Disable Mirroring"));
QAction* open_mod_location = context_menu.addAction(tr("Open Mod Data Location"));
QMenu* open_sdmc_mod_menu = context_menu.addMenu(tr("Open SDMC Mod Data Location"));
- QAction* open_current_game_sdmc = open_sdmc_mod_menu->addAction(tr("Open Current Game Location"));
+ QAction* open_current_game_sdmc =
+ open_sdmc_mod_menu->addAction(tr("Open Current Game Location"));
QAction* open_full_sdmc = open_sdmc_mod_menu->addAction(tr("Open Full Location"));
- QAction* open_transferable_shader_cache = context_menu.addAction(tr("Open Transferable Pipeline Cache"));
+ QAction* open_transferable_shader_cache =
+ context_menu.addAction(tr("Open Transferable Pipeline Cache"));
context_menu.addSeparator();
QMenu* remove_menu = context_menu.addMenu(tr("Remove"));
QAction* remove_update = remove_menu->addAction(tr("Remove Installed Update"));
@@ -1407,11 +1610,12 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri
QAction* verify_integrity = context_menu.addAction(tr("Verify Integrity"));
QAction* copy_tid = context_menu.addAction(tr("Copy Title ID to Clipboard"));
QAction* submit_compat_report = context_menu.addAction(tr("Submit Compatibility Report"));
- #if !defined(__APPLE__)
+#if !defined(__APPLE__)
QMenu* shortcut_menu = context_menu.addMenu(tr("Create Shortcut"));
QAction* create_desktop_shortcut = shortcut_menu->addAction(tr("Add to Desktop"));
- QAction* create_applications_menu_shortcut = shortcut_menu->addAction(tr("Add to Applications Menu"));
- #endif
+ QAction* create_applications_menu_shortcut =
+ shortcut_menu->addAction(tr("Add to Applications Menu"));
+#endif
context_menu.addSeparator();
QAction* properties = context_menu.addAction(tr("Properties"));
@@ -1428,7 +1632,8 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri
open_save_location->setVisible(program_id != 0);
open_nand_location->setVisible(is_mirrored);
- open_nand_location->setToolTip(tr("Citron uses your NAND while syncing. If you need to make save data modifications, do so in here."));
+ open_nand_location->setToolTip(tr("Citron uses your NAND while syncing. If you need to make "
+ "save data modifications, do so in here."));
set_custom_save_path->setVisible(program_id != 0 && !is_mirrored);
remove_custom_save_path->setVisible(program_id != 0 && has_custom_path);
disable_mirroring->setVisible(is_mirrored);
@@ -1448,21 +1653,28 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri
if (has_global_path) {
open_nand_location->setText(tr("Open Global Save Path Location"));
- open_nand_location->setToolTip(tr("The global save path is being used as the base for save data mirroring."));
- mirror_base_path = QString::fromStdString(Settings::values.global_custom_save_path.GetValue());
+ open_nand_location->setToolTip(
+ tr("The global save path is being used as the base for save data mirroring."));
+ mirror_base_path =
+ QString::fromStdString(Settings::values.global_custom_save_path.GetValue());
} else {
- open_nand_location->setToolTip(tr("Citron's default NAND is being used as the base for save data mirroring."));
- mirror_base_path = QString::fromStdString(Common::FS::GetCitronPathString(Common::FS::CitronPath::NANDDir));
+ open_nand_location->setToolTip(
+ tr("Citron's default NAND is being used as the base for save data mirroring."));
+ mirror_base_path = QString::fromStdString(
+ Common::FS::GetCitronPathString(Common::FS::CitronPath::NANDDir));
}
connect(open_nand_location, &QAction::triggered, [this, program_id, mirror_base_path]() {
const auto user_id = system.GetProfileManager().GetLastOpenedUser().AsU128();
- const std::string relative_save_path = fmt::format("user/save/{:016X}/{:016X}{:016X}/{:016X}", 0, user_id[1], user_id[0], program_id);
- const auto full_save_path = std::filesystem::path(mirror_base_path.toStdString()) / relative_save_path;
+ const std::string relative_save_path = fmt::format(
+ "user/save/{:016X}/{:016X}{:016X}/{:016X}", 0, user_id[1], user_id[0], program_id);
+ const auto full_save_path =
+ std::filesystem::path(mirror_base_path.toStdString()) / relative_save_path;
if (!std::filesystem::exists(full_save_path.parent_path())) {
std::filesystem::create_directories(full_save_path.parent_path());
}
- QDesktopServices::openUrl(QUrl::fromLocalFile(QString::fromStdString(full_save_path.string())));
+ QDesktopServices::openUrl(
+ QUrl::fromLocalFile(QString::fromStdString(full_save_path.string())));
});
}
@@ -1470,58 +1682,20 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri
connect(favorite, &QAction::triggered, [this, program_id]() { ToggleFavorite(program_id); });
connect(hide_game, &QAction::triggered, [this, path]() { ToggleHidden(path); });
- connect(open_save_location, &QAction::triggered, [this, program_id, path_str]() { emit OpenFolderRequested(program_id, GameListOpenTarget::SaveData, path_str); });
-
- auto calculateTotalSize = [](const QString& dirPath) -> qint64 {
- qint64 totalSize = 0;
- QDirIterator size_it(dirPath, QDirIterator::Subdirectories);
- while (size_it.hasNext()) {
- size_it.next();
- QFileInfo fileInfo = size_it.fileInfo();
- if (fileInfo.isFile()) {
- totalSize += fileInfo.size();
- }
- }
- return totalSize;
- };
-
- auto copyWithProgress = [calculateTotalSize](const QString& sourceDir, const QString& destDir, QWidget* parent) -> bool {
- QProgressDialog progress(tr("Moving Save Data..."), QString(), 0, 100, parent);
- progress.setWindowFlags(Qt::Window | Qt::WindowTitleHint | Qt::CustomizeWindowHint);
- progress.setWindowModality(Qt::WindowModal);
- progress.setMinimumDuration(0);
- progress.setValue(0);
- qint64 totalSize = calculateTotalSize(sourceDir);
- qint64 copiedSize = 0;
- QDir dir(sourceDir);
- if (!dir.exists()) return false;
- QDir dest_dir(destDir);
- if (!dest_dir.exists()) dest_dir.mkpath(QStringLiteral("."));
- QDirIterator dir_iter(sourceDir, QDirIterator::Subdirectories);
- while (dir_iter.hasNext()) {
- dir_iter.next();
- const QFileInfo file_info = dir_iter.fileInfo();
- const QString relative_path = dir.relativeFilePath(file_info.absoluteFilePath());
- const QString dest_path = QDir(destDir).filePath(relative_path);
- if (file_info.isDir()) {
- dest_dir.mkpath(dest_path);
- } else if (file_info.isFile()) {
- if (QFile::exists(dest_path)) QFile::remove(dest_path);
- if (!QFile::copy(file_info.absoluteFilePath(), dest_path)) return false;
- copiedSize += file_info.size();
- if (totalSize > 0) {
- progress.setValue(static_cast((copiedSize * 100) / totalSize));
- }
- }
- QCoreApplication::processEvents();
- }
- progress.setValue(100);
- return true;
- };
+ connect(open_file_location, &QAction::triggered, [path_str]() {
+ const QString qpath = QString::fromStdString(path_str);
+ const QFileInfo file_info(qpath);
+ QDesktopServices::openUrl(QUrl::fromLocalFile(file_info.absolutePath()));
+ });
+ connect(open_save_location, &QAction::triggered, [this, program_id, path_str]() {
+ emit OpenFolderRequested(program_id, GameListOpenTarget::SaveData, path_str);
+ });
connect(set_custom_save_path, &QAction::triggered, [this, program_id, copyWithProgress]() {
- const QString new_path = QFileDialog::getExistingDirectory(this, tr("Select Custom Save Data Location"));
- if (new_path.isEmpty()) return;
+ const QString new_path =
+ QFileDialog::getExistingDirectory(this, tr("Select Custom Save Data Location"));
+ if (new_path.isEmpty())
+ return;
std::string base_save_path_str;
if (Settings::values.global_custom_save_path_enabled.GetValue() &&
!Settings::values.global_custom_save_path.GetValue().empty()) {
@@ -1531,17 +1705,24 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri
}
const QString base_dir = QString::fromStdString(base_save_path_str);
const auto user_id = system.GetProfileManager().GetLastOpenedUser().AsU128();
- const std::string relative_save_path = fmt::format("user/save/{:016X}/{:016X}{:016X}/{:016X}", 0, user_id[1], user_id[0], program_id);
- const QString internal_save_path = QDir(base_dir).filePath(QString::fromStdString(relative_save_path));
+ const std::string relative_save_path = fmt::format(
+ "user/save/{:016X}/{:016X}{:016X}/{:016X}", 0, user_id[1], user_id[0], program_id);
+ const QString internal_save_path =
+ QDir(base_dir).filePath(QString::fromStdString(relative_save_path));
bool mirroring_enabled = false;
QString detected_emu = GetDetectedEmulatorName(new_path, program_id, base_dir);
if (!detected_emu.isEmpty()) {
- QMessageBox::StandardButton mirror_reply = QMessageBox::question(this, tr("Enable Save Mirroring?"),
- tr("Citron has detected a %1 save structure.\n\n"
- "Would you like to enable 'Intelligent Mirroring'? This will pull the data into Citron's internal save directory "
- "(currently set to '%2') and keep both locations synced whenever you play. A backup of your existing Citron data "
- "will be created. BE WARNED: Please do not have both emulators open during this process.").arg(detected_emu, base_dir),
- QMessageBox::Yes | QMessageBox::No);
+ QMessageBox::StandardButton mirror_reply =
+ QMessageBox::question(this, tr("Enable Save Mirroring?"),
+ tr("Citron has detected a %1 save structure.\n\n"
+ "Would you like to enable 'Intelligent Mirroring'? This "
+ "will pull the data into Citron's internal save directory "
+ "(currently set to '%2') and keep both locations synced "
+ "whenever you play. A backup of your existing Citron data "
+ "will be created. BE WARNED: Please do not have both "
+ "emulators open during this process.")
+ .arg(detected_emu, base_dir),
+ QMessageBox::Yes | QMessageBox::No);
if (mirror_reply == QMessageBox::Yes) {
mirroring_enabled = true;
@@ -1550,35 +1731,50 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri
QDir internal_dir(internal_save_path);
if (internal_dir.exists() && !internal_dir.isEmpty()) {
if (mirroring_enabled) {
- QString timestamp = QDateTime::currentDateTime().toString(QStringLiteral("yyyy-MM-dd_hh-mm-ss"));
- QString backup_path = internal_save_path + QStringLiteral("_mirror_backup_") + timestamp;
+ QString timestamp =
+ QDateTime::currentDateTime().toString(QStringLiteral("yyyy-MM-dd_hh-mm-ss"));
+ QString backup_path =
+ internal_save_path + QStringLiteral("_mirror_backup_") + timestamp;
QDir().mkpath(QFileInfo(backup_path).absolutePath());
if (QDir().rename(internal_save_path, backup_path)) {
- LOG_INFO(Frontend, "Safety: Existing internal data moved to backup: {}", backup_path.toStdString());
+ LOG_INFO(Frontend, "Safety: Existing internal data moved to backup: {}",
+ backup_path.toStdString());
}
} else {
- QMessageBox::StandardButton reply = QMessageBox::question(this, tr("Move Save Data"),
- tr("You have existing save data in your internal save directory. Would you like to move it to the new custom save path?"),
+ QMessageBox::StandardButton reply = QMessageBox::question(
+ this, tr("Move Save Data"),
+ tr("You have existing save data in your internal save directory. Would you "
+ "like to move it to the new custom save path?"),
QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel);
- if (reply == QMessageBox::Cancel) return;
+ if (reply == QMessageBox::Cancel)
+ return;
if (reply == QMessageBox::Yes) {
- const QString full_dest_path = QDir(new_path).filePath(QString::fromStdString(relative_save_path));
+ const QString full_dest_path =
+ QDir(new_path).filePath(QString::fromStdString(relative_save_path));
if (copyWithProgress(internal_save_path, full_dest_path, this)) {
QDir(internal_save_path).removeRecursively();
- QMessageBox::information(this, tr("Success"), tr("Successfully moved save data to the new location."));
+ QMessageBox::information(
+ this, tr("Success"),
+ tr("Successfully moved save data to the new location."));
} else {
- QMessageBox::warning(this, tr("Error"), tr("Failed to move save data. Please see the log for more details."));
+ QMessageBox::warning(
+ this, tr("Error"),
+ tr("Failed to move save data. Please see the log for more details."));
}
}
}
}
if (mirroring_enabled) {
if (copyWithProgress(new_path, internal_save_path, this)) {
- Settings::values.mirrored_save_paths.insert_or_assign(program_id, new_path.toStdString());
+ Settings::values.mirrored_save_paths.insert_or_assign(program_id,
+ new_path.toStdString());
Settings::values.custom_save_paths.erase(program_id);
- QMessageBox::information(this, tr("Success"), tr("Mirroring established. Your data has been pulled into the internal Citron save directory."));
+ QMessageBox::information(this, tr("Success"),
+ tr("Mirroring established. Your data has been pulled into "
+ "the internal Citron save directory."));
} else {
- QMessageBox::warning(this, tr("Error"), tr("Failed to pull data from the mirror source."));
+ QMessageBox::warning(this, tr("Error"),
+ tr("Failed to pull data from the mirror source."));
return;
}
} else {
@@ -1590,20 +1786,24 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri
connect(disable_mirroring, &QAction::triggered, [this, program_id]() {
if (QMessageBox::question(this, tr("Disable Mirroring"),
- tr("Are you sure you want to disable mirroring for this game?\n\nThe directories will no longer be synced."),
+ tr("Are you sure you want to disable mirroring for this "
+ "game?\n\nThe directories will no longer be synced."),
QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
Settings::values.mirrored_save_paths.erase(program_id);
emit SaveConfig();
QMessageBox::information(this, tr("Mirroring Disabled"),
- tr("Mirroring has been disabled for this game. It will now use the save data from the NAND."));
+ tr("Mirroring has been disabled for this game. It will now "
+ "use the save data from the NAND."));
}
});
connect(open_current_game_sdmc, &QAction::triggered, [program_id]() {
const auto sdmc_path = Common::FS::GetCitronPath(Common::FS::CitronPath::SDMCDir);
- const auto full_path = sdmc_path / "atmosphere" / "contents" / fmt::format("{:016X}", program_id);
+ const auto full_path =
+ sdmc_path / "atmosphere" / "contents" / fmt::format("{:016X}", program_id);
const QString qpath = QString::fromStdString(Common::FS::PathToUTF8String(full_path));
QDir dir(qpath);
- if (!dir.exists()) dir.mkpath(QStringLiteral("."));
+ if (!dir.exists())
+ dir.mkpath(QStringLiteral("."));
QDesktopServices::openUrl(QUrl::fromLocalFile(qpath));
});
connect(open_full_sdmc, &QAction::triggered, []() {
@@ -1611,35 +1811,68 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri
const auto full_path = sdmc_path / "atmosphere" / "contents";
const QString qpath = QString::fromStdString(Common::FS::PathToUTF8String(full_path));
QDir dir(qpath);
- if (!dir.exists()) dir.mkpath(QStringLiteral("."));
+ if (!dir.exists())
+ dir.mkpath(QStringLiteral("."));
QDesktopServices::openUrl(QUrl::fromLocalFile(qpath));
});
- connect(start_game, &QAction::triggered, [this, path_str]() { emit BootGame(QString::fromStdString(path_str), StartGameType::Normal); });
- connect(start_game_global, &QAction::triggered, [this, path_str]() { emit BootGame(QString::fromStdString(path_str), StartGameType::Global); });
- connect(open_mod_location, &QAction::triggered, [this, program_id, path_str]() { emit OpenFolderRequested(program_id, GameListOpenTarget::ModData, path_str); });
- connect(open_transferable_shader_cache, &QAction::triggered, [this, program_id]() { emit OpenTransferableShaderCacheRequested(program_id); });
- connect(remove_all_content, &QAction::triggered, [this, program_id]() { emit RemoveInstalledEntryRequested(program_id, InstalledEntryType::Game); });
- connect(remove_update, &QAction::triggered, [this, program_id]() { emit RemoveInstalledEntryRequested(program_id, InstalledEntryType::Update); });
- connect(remove_dlc, &QAction::triggered, [this, program_id]() { emit RemoveInstalledEntryRequested(program_id, InstalledEntryType::AddOnContent); });
- connect(remove_gl_shader_cache, &QAction::triggered, [this, program_id, path_str]() { emit RemoveFileRequested(program_id, GameListRemoveTarget::GlShaderCache, path_str); });
- connect(remove_vk_shader_cache, &QAction::triggered, [this, program_id, path_str]() { emit RemoveFileRequested(program_id, GameListRemoveTarget::VkShaderCache, path_str); });
- connect(remove_shader_cache, &QAction::triggered, [this, program_id, path_str]() { emit RemoveFileRequested(program_id, GameListRemoveTarget::AllShaderCache, path_str); });
- connect(remove_custom_config, &QAction::triggered, [this, program_id, path_str]() { emit RemoveFileRequested(program_id, GameListRemoveTarget::CustomConfiguration, path_str); });
- connect(remove_play_time_data, &QAction::triggered, [this, program_id]() { emit RemovePlayTimeRequested(program_id); });
- connect(remove_cache_storage, &QAction::triggered, [this, program_id, path_str] { emit RemoveFileRequested(program_id, GameListRemoveTarget::CacheStorage, path_str); });
- connect(dump_romfs, &QAction::triggered, [this, program_id, path_str]() { emit DumpRomFSRequested(program_id, path_str, DumpRomFSTarget::Normal); });
- connect(dump_romfs_sdmc, &QAction::triggered, [this, program_id, path_str]() { emit DumpRomFSRequested(program_id, path_str, DumpRomFSTarget::SDMC); });
- connect(verify_integrity, &QAction::triggered, [this, path_str]() { emit VerifyIntegrityRequested(path_str); });
- connect(copy_tid, &QAction::triggered, [this, program_id]() { emit CopyTIDRequested(program_id); });
+ connect(start_game, &QAction::triggered, [this, path_str]() {
+ emit BootGame(QString::fromStdString(path_str), StartGameType::Normal);
+ });
+ connect(start_game_global, &QAction::triggered, [this, path_str]() {
+ emit BootGame(QString::fromStdString(path_str), StartGameType::Global);
+ });
+ connect(open_mod_location, &QAction::triggered, [this, program_id, path_str]() {
+ emit OpenFolderRequested(program_id, GameListOpenTarget::ModData, path_str);
+ });
+ connect(open_transferable_shader_cache, &QAction::triggered,
+ [this, program_id]() { emit OpenTransferableShaderCacheRequested(program_id); });
+ connect(remove_all_content, &QAction::triggered, [this, program_id]() {
+ emit RemoveInstalledEntryRequested(program_id, InstalledEntryType::Game);
+ });
+ connect(remove_update, &QAction::triggered, [this, program_id]() {
+ emit RemoveInstalledEntryRequested(program_id, InstalledEntryType::Update);
+ });
+ connect(remove_dlc, &QAction::triggered, [this, program_id]() {
+ emit RemoveInstalledEntryRequested(program_id, InstalledEntryType::AddOnContent);
+ });
+ connect(remove_gl_shader_cache, &QAction::triggered, [this, program_id, path_str]() {
+ emit RemoveFileRequested(program_id, GameListRemoveTarget::GlShaderCache, path_str);
+ });
+ connect(remove_vk_shader_cache, &QAction::triggered, [this, program_id, path_str]() {
+ emit RemoveFileRequested(program_id, GameListRemoveTarget::VkShaderCache, path_str);
+ });
+ connect(remove_shader_cache, &QAction::triggered, [this, program_id, path_str]() {
+ emit RemoveFileRequested(program_id, GameListRemoveTarget::AllShaderCache, path_str);
+ });
+ connect(remove_custom_config, &QAction::triggered, [this, program_id, path_str]() {
+ emit RemoveFileRequested(program_id, GameListRemoveTarget::CustomConfiguration, path_str);
+ });
+ connect(remove_play_time_data, &QAction::triggered,
+ [this, program_id]() { emit RemovePlayTimeRequested(program_id); });
+ connect(remove_cache_storage, &QAction::triggered, [this, program_id, path_str] {
+ emit RemoveFileRequested(program_id, GameListRemoveTarget::CacheStorage, path_str);
+ });
+ connect(dump_romfs, &QAction::triggered, [this, program_id, path_str]() {
+ emit DumpRomFSRequested(program_id, path_str, DumpRomFSTarget::Normal);
+ });
+ connect(dump_romfs_sdmc, &QAction::triggered, [this, program_id, path_str]() {
+ emit DumpRomFSRequested(program_id, path_str, DumpRomFSTarget::SDMC);
+ });
+ connect(verify_integrity, &QAction::triggered,
+ [this, path_str]() { emit VerifyIntegrityRequested(path_str); });
+ connect(copy_tid, &QAction::triggered,
+ [this, program_id]() { emit CopyTIDRequested(program_id); });
connect(submit_compat_report, &QAction::triggered, [this, program_id, game_name]() {
- const auto reply = QMessageBox::question(this, tr("GitHub Account Required"),
+ const auto reply = QMessageBox::question(
+ this, tr("GitHub Account Required"),
tr("In order to submit a compatibility report, you must have a GitHub account.\n\n"
"If you do not have one, this feature will not work. Would you like to proceed?"),
QMessageBox::Yes | QMessageBox::No);
if (reply != QMessageBox::Yes) {
return;
}
- const QString clean_tid = QStringLiteral("%1").arg(program_id, 16, 16, QLatin1Char('0')).toUpper();
+ const QString clean_tid =
+ QStringLiteral("%1").arg(program_id, 16, 16, QLatin1Char('0')).toUpper();
QUrl url(QStringLiteral("https://github.com/CollectingW/Citron-Compatability/issues/new"));
QUrlQuery query;
query.addQueryItem(QStringLiteral("template"), QStringLiteral("compat.yml"));
@@ -1648,35 +1881,45 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri
url.setQuery(query);
QDesktopServices::openUrl(url);
});
- #if !defined(__APPLE__)
- connect(create_desktop_shortcut, &QAction::triggered, [this, program_id, path_str]() { emit CreateShortcut(program_id, path_str, GameListShortcutTarget::Desktop); });
- connect(create_applications_menu_shortcut, &QAction::triggered, [this, program_id, path_str]() { emit CreateShortcut(program_id, path_str, GameListShortcutTarget::Applications); });
- #endif
- connect(properties, &QAction::triggered, [this, path_str]() { emit OpenPerGameGeneralRequested(path_str); });
+#if !defined(__APPLE__)
+ connect(create_desktop_shortcut, &QAction::triggered, [this, program_id, path_str]() {
+ emit CreateShortcut(program_id, path_str, GameListShortcutTarget::Desktop);
+ });
+ connect(create_applications_menu_shortcut, &QAction::triggered, [this, program_id, path_str]() {
+ emit CreateShortcut(program_id, path_str, GameListShortcutTarget::Applications);
+ });
+#endif
+ connect(properties, &QAction::triggered,
+ [this, path_str]() { emit OpenPerGameGeneralRequested(path_str); });
}
-void GameList::AddCustomDirPopup(QMenu& context_menu, QModelIndex selected) {
- UISettings::GameDir& game_dir = UISettings::values.game_dirs[selected.data(GameListDir::GameDirRole).toInt()];
- QAction* show_hidden = context_menu.addAction(tr("Show Hidden Games"));
+void GameList::AddCustomDirPopup(QMenu& context_menu, QModelIndex selected,
+ bool show_hidden_action) {
+ UISettings::GameDir& game_dir =
+ UISettings::values.game_dirs[selected.data(GameListDir::GameDirRole).toInt()];
+ if (show_hidden_action) {
+ QAction* show_hidden = context_menu.addAction(tr("Show Hidden Games"));
+ connect(show_hidden, &QAction::triggered, [this, selected] {
+ QStandardItem* folder = item_model->itemFromIndex(selected);
+ bool changed = false;
+ for (int i = 0; i < folder->rowCount(); ++i) {
+ const QString path =
+ folder->child(i)->data(GameListItemPath::FullPathRole).toString();
+ if (UISettings::values.hidden_paths.removeOne(path)) {
+ changed = true;
+ }
+ }
+ if (changed) {
+ OnTextChanged(search_field->filterText());
+ emit SaveConfig();
+ }
+ });
+ }
context_menu.addSeparator();
QAction* deep_scan = context_menu.addAction(tr("Scan Subfolders"));
QAction* delete_dir = context_menu.addAction(tr("Remove Game Directory"));
deep_scan->setCheckable(true);
deep_scan->setChecked(game_dir.deep_scan);
- connect(show_hidden, &QAction::triggered, [this, selected] {
- QStandardItem* folder = item_model->itemFromIndex(selected);
- bool changed = false;
- for (int i = 0; i < folder->rowCount(); ++i) {
- const QString path = folder->child(i)->data(GameListItemPath::FullPathRole).toString();
- if (UISettings::values.hidden_paths.removeOne(path)) {
- changed = true;
- }
- }
- if (changed) {
- OnTextChanged(search_field->filterText());
- emit SaveConfig();
- }
- });
connect(deep_scan, &QAction::triggered, [this, &game_dir] {
game_dir.deep_scan = !game_dir.deep_scan;
PopulateAsync(UISettings::values.game_dirs);
@@ -1714,23 +1957,32 @@ void GameList::AddPermDirPopup(QMenu& context_menu, QModelIndex selected) {
});
connect(move_up, &QAction::triggered, [this, selected, row, game_dir_index] {
const int other_index = selected.sibling(row - 1, 0).data(GameListDir::GameDirRole).toInt();
- std::swap(UISettings::values.game_dirs[game_dir_index], UISettings::values.game_dirs[other_index]);
+ std::swap(UISettings::values.game_dirs[game_dir_index],
+ UISettings::values.game_dirs[other_index]);
item_model->setData(selected, QVariant(other_index), GameListDir::GameDirRole);
- item_model->setData(selected.sibling(row - 1, 0), QVariant(game_dir_index), GameListDir::GameDirRole);
+ item_model->setData(selected.sibling(row - 1, 0), QVariant(game_dir_index),
+ GameListDir::GameDirRole);
QList item = item_model->takeRow(row);
item_model->invisibleRootItem()->insertRow(row - 1, item);
- tree_view->setExpanded(selected.sibling(row - 1, 0), UISettings::values.game_dirs[other_index].expanded);
+ tree_view->setExpanded(selected.sibling(row - 1, 0),
+ UISettings::values.game_dirs[other_index].expanded);
});
connect(move_down, &QAction::triggered, [this, selected, row, game_dir_index] {
const int other_index = selected.sibling(row + 1, 0).data(GameListDir::GameDirRole).toInt();
- std::swap(UISettings::values.game_dirs[game_dir_index], UISettings::values.game_dirs[other_index]);
+ std::swap(UISettings::values.game_dirs[game_dir_index],
+ UISettings::values.game_dirs[other_index]);
item_model->setData(selected, QVariant(other_index), GameListDir::GameDirRole);
- item_model->setData(selected.sibling(row + 1, 0), QVariant(game_dir_index), GameListDir::GameDirRole);
+ item_model->setData(selected.sibling(row + 1, 0), QVariant(game_dir_index),
+ GameListDir::GameDirRole);
const QList item = item_model->takeRow(row);
item_model->invisibleRootItem()->insertRow(row + 1, item);
- tree_view->setExpanded(selected.sibling(row + 1, 0), UISettings::values.game_dirs[other_index].expanded);
+ tree_view->setExpanded(selected.sibling(row + 1, 0),
+ UISettings::values.game_dirs[other_index].expanded);
+ });
+ connect(open_directory_location, &QAction::triggered, [this, game_dir_index] {
+ emit OpenDirectory(
+ QString::fromStdString(UISettings::values.game_dirs[game_dir_index].path));
});
- connect(open_directory_location, &QAction::triggered, [this, game_dir_index] { emit OpenDirectory(QString::fromStdString(UISettings::values.game_dirs[game_dir_index].path)); });
}
void GameList::AddFavoritesPopup(QMenu& context_menu) {
@@ -1749,7 +2001,8 @@ void GameList::LoadCompatibilityList() {
compatibility_list.clear();
// Look for a downloaded list in the config directory first
- const auto config_dir = QString::fromStdString(Common::FS::GetCitronPathString(Common::FS::CitronPath::ConfigDir));
+ const auto config_dir =
+ QString::fromStdString(Common::FS::GetCitronPathString(Common::FS::CitronPath::ConfigDir));
const QString local_path = QDir(config_dir).filePath(QStringLiteral("compatibility_list.json"));
QFile compat_list;
@@ -1780,7 +2033,8 @@ void GameList::LoadCompatibilityList() {
const QString compatibility_key = QStringLiteral("compatibility");
// Match the legacy parser logic
- if (!game.contains(compatibility_key)) continue;
+ if (!game.contains(compatibility_key))
+ continue;
const int compatibility = game[compatibility_key].toInt();
const QString directory = game[QStringLiteral("directory")].toString();
@@ -1789,10 +2043,12 @@ void GameList::LoadCompatibilityList() {
for (const QJsonValue id_ref : ids) {
const QJsonObject id_object = id_ref.toObject();
const QString id = id_object[QStringLiteral("id")].toString();
- if (id.isEmpty()) continue;
+ if (id.isEmpty())
+ continue;
- compatibility_list.insert_or_assign(id.toUpper().toStdString(),
- std::make_pair(QString::number(compatibility), directory));
+ compatibility_list.insert_or_assign(
+ id.toUpper().toStdString(),
+ std::make_pair(QString::number(compatibility), directory));
}
}
LOG_INFO(Frontend, "Loaded {} compatibility entries.", compatibility_list.size());
@@ -1850,11 +2106,15 @@ void GameList::PopulateAsync(QVector& game_dirs) {
progress_bar->setVisible(true);
}
- current_worker = std::make_unique(vfs, provider, game_dirs, compatibility_list, play_time_manager, system, main_window->GetMultiplayerState()->GetSession());
- connect(current_worker.get(), &GameListWorker::DataAvailable, this, &GameList::WorkerEvent, Qt::QueuedConnection);
+ current_worker = std::make_unique(
+ vfs, provider, game_dirs, compatibility_list, play_time_manager, system,
+ main_window->GetMultiplayerState()->GetSession());
+ connect(current_worker.get(), &GameListWorker::DataAvailable, this, &GameList::WorkerEvent,
+ Qt::QueuedConnection);
if (progress_bar) {
- connect(current_worker.get(), &GameListWorker::ProgressUpdated, progress_bar, &QProgressBar::setValue, Qt::QueuedConnection);
+ connect(current_worker.get(), &GameListWorker::ProgressUpdated, progress_bar,
+ &QProgressBar::setValue, Qt::QueuedConnection);
}
QThreadPool::globalInstance()->start(current_worker.get());
@@ -1874,9 +2134,8 @@ void GameList::LoadInterfaceLayout() {
}
const QStringList GameList::supported_file_extensions = {
- QStringLiteral("xci"), QStringLiteral("nsp"),
- QStringLiteral("nso"), QStringLiteral("nro"), QStringLiteral("kip")
-};
+ QStringLiteral("xci"), QStringLiteral("nsp"), QStringLiteral("nso"), QStringLiteral("nro"),
+ QStringLiteral("kip")};
void GameList::RefreshGameDirectory() {
if (!UISettings::values.game_dirs.empty() && current_worker != nullptr) {
@@ -1885,275 +2144,293 @@ void GameList::RefreshGameDirectory() {
}
}
- void GameList::ToggleFavorite(u64 program_id) {
- if (!UISettings::values.favorited_ids.contains(program_id)) {
- tree_view->setRowHidden(0, item_model->invisibleRootItem()->index(), !search_field->filterText().isEmpty());
- UISettings::values.favorited_ids.append(program_id);
- AddFavorite(program_id);
- item_model->sort(tree_view->header()->sortIndicatorSection(), tree_view->header()->sortIndicatorOrder());
+void GameList::ToggleFavorite(u64 program_id) {
+ if (!UISettings::values.favorited_ids.contains(program_id)) {
+ tree_view->setRowHidden(0, item_model->invisibleRootItem()->index(),
+ !search_field->filterText().isEmpty());
+ UISettings::values.favorited_ids.append(program_id);
+ AddFavorite(program_id);
+ item_model->sort(tree_view->header()->sortIndicatorSection(),
+ tree_view->header()->sortIndicatorOrder());
+ } else {
+ UISettings::values.favorited_ids.removeOne(program_id);
+ RemoveFavorite(program_id);
+ if (UISettings::values.favorited_ids.size() == 0) {
+ tree_view->setRowHidden(0, item_model->invisibleRootItem()->index(), true);
+ }
+ }
+ if (list_view->isVisible()) {
+ // Preserve filter when updating favorites
+ QString filter_text = search_field->filterText();
+ if (!filter_text.isEmpty()) {
+ FilterGridView(filter_text);
} else {
- UISettings::values.favorited_ids.removeOne(program_id);
- RemoveFavorite(program_id);
- if (UISettings::values.favorited_ids.size() == 0) {
- tree_view->setRowHidden(0, item_model->invisibleRootItem()->index(), true);
- }
+ PopulateGridView();
}
- if (list_view->isVisible()) {
- // Preserve filter when updating favorites
- QString filter_text = search_field->filterText();
- if (!filter_text.isEmpty()) {
- FilterGridView(filter_text);
- } else {
- PopulateGridView();
- }
- }
- SaveConfig();
}
+ SaveConfig();
+}
- void GameList::AddFavorite(u64 program_id) {
- auto* favorites_row = item_model->item(0);
- for (int i = 1; i < item_model->rowCount() - 1; i++) {
- const auto* folder = item_model->item(i);
- for (int j = 0; j < folder->rowCount(); j++) {
- if (folder->child(j)->data(GameListItemPath::ProgramIdRole).toULongLong() == program_id) {
- QList list;
- for (int k = 0; k < COLUMN_COUNT; k++) {
- list.append(folder->child(j, k)->clone());
- }
- list[0]->setData(folder->child(j)->data(GameListItem::SortRole), GameListItem::SortRole);
- list[0]->setText(folder->child(j)->data(Qt::DisplayRole).toString());
- favorites_row->appendRow(list);
- return;
+void GameList::AddFavorite(u64 program_id) {
+ auto* favorites_row = item_model->item(0);
+ for (int i = 1; i < item_model->rowCount() - 1; i++) {
+ const auto* folder = item_model->item(i);
+ for (int j = 0; j < folder->rowCount(); j++) {
+ if (folder->child(j)->data(GameListItemPath::ProgramIdRole).toULongLong() ==
+ program_id) {
+ QList list;
+ for (int k = 0; k < COLUMN_COUNT; k++) {
+ list.append(folder->child(j, k)->clone());
}
- }
- }
- }
-
- void GameList::RemoveFavorite(u64 program_id) {
- auto* favorites_row = item_model->item(0);
- for (int i = 0; i < favorites_row->rowCount(); i++) {
- const auto* game = favorites_row->child(i);
- if (game->data(GameListItemPath::ProgramIdRole).toULongLong() == program_id) {
- favorites_row->removeRow(i);
+ list[0]->setData(folder->child(j)->data(GameListItem::SortRole),
+ GameListItem::SortRole);
+ list[0]->setText(folder->child(j)->data(Qt::DisplayRole).toString());
+ favorites_row->appendRow(list);
return;
}
}
}
+}
- GameListPlaceholder::GameListPlaceholder(GMainWindow* parent) : QWidget{parent} {
- connect(parent, &GMainWindow::UpdateThemedIcons, this, &GameListPlaceholder::onUpdateThemedIcons);
- layout = new QVBoxLayout;
- image = new QLabel;
- text = new QLabel;
- layout->setAlignment(Qt::AlignCenter);
- image->setPixmap(QIcon::fromTheme(QStringLiteral("plus_folder")).pixmap(200));
- RetranslateUI();
- QFont font = text->font();
- font.setPointSize(20);
- text->setFont(font);
- text->setAlignment(Qt::AlignHCenter);
- image->setAlignment(Qt::AlignHCenter);
- layout->addWidget(image);
- layout->addWidget(text);
- setLayout(layout);
- }
-
- GameListPlaceholder::~GameListPlaceholder() = default;
-
- void GameListPlaceholder::onUpdateThemedIcons() {
- image->setPixmap(QIcon::fromTheme(QStringLiteral("plus_folder")).pixmap(200));
- }
-
- void GameListPlaceholder::mouseDoubleClickEvent(QMouseEvent* event) {
- emit GameListPlaceholder::AddDirectory();
- }
-
- void GameListPlaceholder::changeEvent(QEvent* event) {
- if (event->type() == QEvent::LanguageChange) {
- RetranslateUI();
+void GameList::RemoveFavorite(u64 program_id) {
+ auto* favorites_row = item_model->item(0);
+ for (int i = 0; i < favorites_row->rowCount(); i++) {
+ const auto* game = favorites_row->child(i);
+ if (game->data(GameListItemPath::ProgramIdRole).toULongLong() == program_id) {
+ favorites_row->removeRow(i);
+ return;
}
- QWidget::changeEvent(event);
}
+}
- void GameListPlaceholder::RetranslateUI() {
- text->setText(tr("Double-click to add a new folder to the game list"));
+GameListPlaceholder::GameListPlaceholder(GMainWindow* parent) : QWidget{parent} {
+ connect(parent, &GMainWindow::UpdateThemedIcons, this,
+ &GameListPlaceholder::onUpdateThemedIcons);
+ layout = new QVBoxLayout;
+ image = new QLabel;
+ text = new QLabel;
+ layout->setAlignment(Qt::AlignCenter);
+ image->setPixmap(QIcon::fromTheme(QStringLiteral("plus_folder")).pixmap(200));
+ RetranslateUI();
+ QFont font = text->font();
+ font.setPointSize(20);
+ text->setFont(font);
+ text->setAlignment(Qt::AlignHCenter);
+ image->setAlignment(Qt::AlignHCenter);
+ layout->addWidget(image);
+ layout->addWidget(text);
+ setLayout(layout);
+}
+
+GameListPlaceholder::~GameListPlaceholder() = default;
+
+void GameListPlaceholder::onUpdateThemedIcons() {
+ image->setPixmap(QIcon::fromTheme(QStringLiteral("plus_folder")).pixmap(200));
+}
+
+void GameListPlaceholder::mouseDoubleClickEvent(QMouseEvent* event) {
+ emit GameListPlaceholder::AddDirectory();
+}
+
+void GameListPlaceholder::changeEvent(QEvent* event) {
+ if (event->type() == QEvent::LanguageChange) {
+ RetranslateUI();
}
+ QWidget::changeEvent(event);
+}
- void GameList::SetViewMode(bool grid_view) {
- if (grid_view) {
- // Check if there's an active filter - if so, use FilterGridView instead
+void GameListPlaceholder::RetranslateUI() {
+ text->setText(tr("Double-click to add a new folder to the game list"));
+}
+
+void GameList::SetViewMode(bool grid_view) {
+ if (grid_view) {
+ // Check if there's an active filter - if so, use FilterGridView instead
+ QString filter_text = search_field->filterText();
+ if (!filter_text.isEmpty()) {
+ FilterGridView(filter_text);
+ } else {
+ PopulateGridView();
+ }
+ tree_view->setVisible(false);
+ list_view->setVisible(true);
+ if (list_view->model() && list_view->model()->rowCount() > 0) {
+ list_view->setCurrentIndex(list_view->model()->index(0, 0));
+ }
+ } else {
+ list_view->setVisible(false);
+ tree_view->setVisible(true);
+ if (item_model && item_model->rowCount() > 0) {
+ tree_view->setCurrentIndex(item_model->index(0, 0));
+ }
+ }
+ // Update button states
+ if (btn_list_view && btn_grid_view) {
+ btn_list_view->setChecked(!grid_view);
+ btn_grid_view->setChecked(grid_view);
+ }
+}
+
+void GameList::PopulateGridView() {
+ QStandardItemModel* hierarchical_model = item_model;
+ if (QAbstractItemModel* old_model = list_view->model()) {
+ if (old_model != item_model) {
+ old_model->deleteLater();
+ }
+ }
+ QStandardItemModel* flat_model = new QStandardItemModel(this);
+ flat_model->setSortRole(GameListItemPath::SortRole);
+ for (int i = 0; i < hierarchical_model->rowCount(); ++i) {
+ QStandardItem* folder = hierarchical_model->item(i, 0);
+ if (!folder)
+ continue;
+ const auto folder_type = folder->data(GameListItem::TypeRole).value();
+ if (folder_type == GameListItemType::AddDir) {
+ continue;
+ }
+ for (int j = 0; j < folder->rowCount(); ++j) {
+ QStandardItem* game_item = folder->child(j, 0);
+ if (!game_item)
+ continue;
+ const auto game_type =
+ game_item->data(GameListItem::TypeRole).value();
+ if (game_type == GameListItemType::Game) {
+ const QString full_path =
+ game_item->data(GameListItemPath::FullPathRole).toString();
+ if (UISettings::values.hidden_paths.contains(full_path)) {
+ continue;
+ }
+ QStandardItem* cloned_item = game_item->clone();
+ QString game_title = game_item->data(GameListItemPath::TitleRole).toString();
+ if (game_title.isEmpty()) {
+ std::string filename;
+ Common::SplitPath(
+ game_item->data(GameListItemPath::FullPathRole).toString().toStdString(),
+ nullptr, &filename, nullptr);
+ game_title = QString::fromStdString(filename);
+ }
+ cloned_item->setText(game_title);
+ flat_model->appendRow(cloned_item);
+ }
+ }
+ }
+ list_view->setModel(flat_model);
+ const u32 icon_size = UISettings::values.game_icon_size.GetValue();
+ list_view->setGridSize(QSize(icon_size + 60, icon_size + 80));
+ // Sort the grid view using current sort order
+ flat_model->sort(0, current_sort_order);
+ // Update icon sizes in the model - ensure all icons are consistently sized with rounded corners
+ for (int i = 0; i < flat_model->rowCount(); ++i) {
+ QStandardItem* item = flat_model->item(i);
+ if (item) {
+ QVariant icon_data = item->data(Qt::DecorationRole);
+ if (icon_data.isValid() && icon_data.canConvert()) {
+ QPixmap pixmap = icon_data.value();
+ if (!pixmap.isNull()) {
+#ifdef __linux__
+ // On Linux, use simple scaling to avoid QPainter bugs
+ QPixmap scaled = pixmap.scaled(icon_size, icon_size, Qt::IgnoreAspectRatio,
+ Qt::SmoothTransformation);
+ item->setData(scaled, Qt::DecorationRole);
+#else
+ // On other platforms, use the QPainter method for rounded corners
+ QPixmap rounded(icon_size, icon_size);
+ rounded.fill(Qt::transparent);
+
+ QPainter painter(&rounded);
+ painter.setRenderHint(QPainter::Antialiasing);
+
+ const int radius = icon_size / 8;
+ QPainterPath path;
+ path.addRoundedRect(0, 0, icon_size, icon_size, radius, radius);
+ painter.setClipPath(path);
+
+ QPixmap scaled = pixmap.scaled(icon_size, icon_size, Qt::IgnoreAspectRatio,
+ Qt::SmoothTransformation);
+ painter.drawPixmap(0, 0, scaled);
+
+ item->setData(rounded, Qt::DecorationRole);
+#endif
+ }
+ }
+ }
+ }
+}
+
+void GameList::ToggleViewMode() {
+ bool current_grid_view = UISettings::values.game_list_grid_view.GetValue();
+ UISettings::values.game_list_grid_view.SetValue(!current_grid_view);
+ SetViewMode(!current_grid_view);
+ // Button states are updated in SetViewMode
+}
+
+void GameList::SortAlphabetically() {
+ if (tree_view->isVisible()) {
+ // Sort tree view by name column using current sort order
+ tree_view->header()->setSortIndicator(COLUMN_NAME, current_sort_order);
+ item_model->sort(COLUMN_NAME, current_sort_order);
+ } else if (list_view->isVisible()) {
+ // Sort grid view alphabetically using current sort order
+ QAbstractItemModel* current_model = list_view->model();
+ if (current_model && current_model != item_model) {
+ // Sort the flat model used by list view (filtered or unfiltered)
+ QStandardItemModel* flat_model = qobject_cast(current_model);
+ if (flat_model) {
+ // Use SortRole for proper alphabetical sorting
+ flat_model->setSortRole(GameListItemPath::SortRole);
+ flat_model->sort(0, current_sort_order);
+ }
+ } else {
+ // If using item_model directly, repopulate grid view to apply sort
+ // Preserve filter if active
QString filter_text = search_field->filterText();
if (!filter_text.isEmpty()) {
FilterGridView(filter_text);
} else {
PopulateGridView();
}
- tree_view->setVisible(false);
- list_view->setVisible(true);
- if (list_view->model() && list_view->model()->rowCount() > 0) {
- list_view->setCurrentIndex(list_view->model()->index(0, 0));
- }
- } else {
- list_view->setVisible(false);
- tree_view->setVisible(true);
- if (item_model && item_model->rowCount() > 0) {
- tree_view->setCurrentIndex(item_model->index(0, 0));
- }
- }
- // Update button states
- if (btn_list_view && btn_grid_view) {
- btn_list_view->setChecked(!grid_view);
- btn_grid_view->setChecked(grid_view);
}
}
+ UpdateSortButtonIcon();
+}
- void GameList::PopulateGridView() {
- QStandardItemModel* hierarchical_model = item_model;
- if (QAbstractItemModel* old_model = list_view->model()) {
- if (old_model != item_model) {
- old_model->deleteLater();
- }
+void GameList::ToggleSortOrder() {
+ // Toggle between ascending and descending, just like clicking the Name column header
+ current_sort_order =
+ (current_sort_order == Qt::AscendingOrder) ? Qt::DescendingOrder : Qt::AscendingOrder;
+ SortAlphabetically();
+}
+
+void GameList::UpdateSortButtonIcon() {
+ if (!btn_sort_az)
+ return;
+
+ QIcon sort_icon;
+ if (current_sort_order == Qt::AscendingOrder) {
+ // Ascending (A-Z) - arrow up
+ sort_icon = QIcon::fromTheme(QStringLiteral("view-sort-ascending"));
+ if (sort_icon.isNull()) {
+ sort_icon = QIcon::fromTheme(QStringLiteral("sort-ascending"));
}
- QStandardItemModel* flat_model = new QStandardItemModel(this);
- flat_model->setSortRole(GameListItemPath::SortRole);
- for (int i = 0; i < hierarchical_model->rowCount(); ++i) {
- QStandardItem* folder = hierarchical_model->item(i, 0);
- if (!folder) continue;
- const auto folder_type = folder->data(GameListItem::TypeRole).value();
- if (folder_type == GameListItemType::AddDir) {
- continue;
- }
- for (int j = 0; j < folder->rowCount(); ++j) {
- QStandardItem* game_item = folder->child(j, 0);
- if (!game_item) continue;
- const auto game_type = game_item->data(GameListItem::TypeRole).value();
- if (game_type == GameListItemType::Game) {
- QStandardItem* cloned_item = game_item->clone();
- QString game_title = game_item->data(GameListItemPath::TitleRole).toString();
- if (game_title.isEmpty()) {
- std::string filename;
- Common::SplitPath(game_item->data(GameListItemPath::FullPathRole).toString().toStdString(), nullptr, &filename, nullptr);
- game_title = QString::fromStdString(filename);
- }
- cloned_item->setText(game_title);
- flat_model->appendRow(cloned_item);
- }
- }
+ if (sort_icon.isNull()) {
+ sort_icon = style()->standardIcon(QStyle::SP_ArrowUp);
}
- list_view->setModel(flat_model);
- const u32 icon_size = UISettings::values.game_icon_size.GetValue();
- list_view->setGridSize(QSize(icon_size + 60, icon_size + 80));
- // Sort the grid view using current sort order
- flat_model->sort(0, current_sort_order);
- // Update icon sizes in the model - ensure all icons are consistently sized with rounded corners
- for (int i = 0; i < flat_model->rowCount(); ++i) {
- QStandardItem* item = flat_model->item(i);
- if (item) {
- QVariant icon_data = item->data(Qt::DecorationRole);
- if (icon_data.isValid() && icon_data.canConvert()) {
- QPixmap pixmap = icon_data.value();
- if (!pixmap.isNull()) {
- #ifdef __linux__
- // On Linux, use simple scaling to avoid QPainter bugs
- QPixmap scaled = pixmap.scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
- item->setData(scaled, Qt::DecorationRole);
- #else
- // On other platforms, use the QPainter method for rounded corners
- QPixmap rounded(icon_size, icon_size);
- rounded.fill(Qt::transparent);
-
- QPainter painter(&rounded);
- painter.setRenderHint(QPainter::Antialiasing);
-
- const int radius = icon_size / 8;
- QPainterPath path;
- path.addRoundedRect(0, 0, icon_size, icon_size, radius, radius);
- painter.setClipPath(path);
-
- QPixmap scaled = pixmap.scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
- painter.drawPixmap(0, 0, scaled);
-
- item->setData(rounded, Qt::DecorationRole);
- #endif
- }
- }
- }
+ } else {
+ // Descending (Z-A) - arrow down
+ sort_icon = QIcon::fromTheme(QStringLiteral("view-sort-descending"));
+ if (sort_icon.isNull()) {
+ sort_icon = QIcon::fromTheme(QStringLiteral("sort-descending"));
+ }
+ if (sort_icon.isNull()) {
+ sort_icon = style()->standardIcon(QStyle::SP_ArrowDown);
}
}
-
- void GameList::ToggleViewMode() {
- bool current_grid_view = UISettings::values.game_list_grid_view.GetValue();
- UISettings::values.game_list_grid_view.SetValue(!current_grid_view);
- SetViewMode(!current_grid_view);
- // Button states are updated in SetViewMode
- }
-
- void GameList::SortAlphabetically() {
- if (tree_view->isVisible()) {
- // Sort tree view by name column using current sort order
- tree_view->header()->setSortIndicator(COLUMN_NAME, current_sort_order);
- item_model->sort(COLUMN_NAME, current_sort_order);
- } else if (list_view->isVisible()) {
- // Sort grid view alphabetically using current sort order
- QAbstractItemModel* current_model = list_view->model();
- if (current_model && current_model != item_model) {
- // Sort the flat model used by list view (filtered or unfiltered)
- QStandardItemModel* flat_model = qobject_cast(current_model);
- if (flat_model) {
- // Use SortRole for proper alphabetical sorting
- flat_model->setSortRole(GameListItemPath::SortRole);
- flat_model->sort(0, current_sort_order);
- }
- } else {
- // If using item_model directly, repopulate grid view to apply sort
- // Preserve filter if active
- QString filter_text = search_field->filterText();
- if (!filter_text.isEmpty()) {
- FilterGridView(filter_text);
- } else {
- PopulateGridView();
- }
- }
- }
- UpdateSortButtonIcon();
- }
-
- void GameList::ToggleSortOrder() {
- // Toggle between ascending and descending, just like clicking the Name column header
- current_sort_order = (current_sort_order == Qt::AscendingOrder)
- ? Qt::DescendingOrder
- : Qt::AscendingOrder;
- SortAlphabetically();
- }
-
- void GameList::UpdateSortButtonIcon() {
- if (!btn_sort_az) return;
-
- QIcon sort_icon;
- if (current_sort_order == Qt::AscendingOrder) {
- // Ascending (A-Z) - arrow up
- sort_icon = QIcon::fromTheme(QStringLiteral("view-sort-ascending"));
- if (sort_icon.isNull()) {
- sort_icon = QIcon::fromTheme(QStringLiteral("sort-ascending"));
- }
- if (sort_icon.isNull()) {
- sort_icon = style()->standardIcon(QStyle::SP_ArrowUp);
- }
- } else {
- // Descending (Z-A) - arrow down
- sort_icon = QIcon::fromTheme(QStringLiteral("view-sort-descending"));
- if (sort_icon.isNull()) {
- sort_icon = QIcon::fromTheme(QStringLiteral("sort-descending"));
- }
- if (sort_icon.isNull()) {
- sort_icon = style()->standardIcon(QStyle::SP_ArrowDown);
- }
- }
- btn_sort_az->setIcon(sort_icon);
- }
+ btn_sort_az->setIcon(sort_icon);
+}
void GameList::UpdateProgressBarColor() {
- if (!progress_bar) return;
+ if (!progress_bar)
+ return;
// Convert the Hex String from settings to a QColor
QColor accent(QString::fromStdString(UISettings::values.accent_color.GetValue()));
@@ -2165,18 +2442,18 @@ void GameList::UpdateProgressBarColor() {
"background: qlineargradient(x1:0, y1:0, x2:1, y2:0, "
"stop:0 #ff0000, stop:0.16 #ffff00, stop:0.33 #00ff00, "
"stop:0.5 #00ffff, stop:0.66 #0000ff, stop:0.83 #ff00ff, stop:1 #ff0000); "
- "}"
- ));
+ "}"));
} else {
- progress_bar->setStyleSheet(QStringLiteral(
- "QProgressBar { border: none; background: transparent; } "
- "QProgressBar::chunk { background-color: %1; }"
- ).arg(accent.name()));
+ progress_bar->setStyleSheet(
+ QStringLiteral("QProgressBar { border: none; background: transparent; } "
+ "QProgressBar::chunk { background-color: %1; }")
+ .arg(accent.name()));
}
}
void GameList::RefreshCompatibilityList() {
- const QUrl url(QStringLiteral("https://raw.githubusercontent.com/CollectingW/Citron-Compatability/refs/heads/main/compatibility_list.json"));
+ const QUrl url(QStringLiteral("https://raw.githubusercontent.com/CollectingW/"
+ "Citron-Compatability/refs/heads/main/compatibility_list.json"));
QNetworkRequest request(url);
QNetworkReply* reply = network_manager->get(request);
@@ -2185,8 +2462,10 @@ void GameList::RefreshCompatibilityList() {
if (reply->error() == QNetworkReply::NoError) {
const QByteArray json_data = reply->readAll();
- const auto config_dir = QString::fromStdString(Common::FS::GetCitronPathString(Common::FS::CitronPath::ConfigDir));
- const QString local_path = QDir(config_dir).filePath(QStringLiteral("compatibility_list.json"));
+ const auto config_dir = QString::fromStdString(
+ Common::FS::GetCitronPathString(Common::FS::CitronPath::ConfigDir));
+ const QString local_path =
+ QDir(config_dir).filePath(QStringLiteral("compatibility_list.json"));
QFile file(local_path);
if (file.open(QFile::WriteOnly)) {
@@ -2199,24 +2478,30 @@ void GameList::RefreshCompatibilityList() {
// Refresh the UI by replacing the old compatibility items with new ones
for (int i = 0; i < item_model->rowCount(); ++i) {
QStandardItem* folder = item_model->item(i, 0);
- if (!folder) continue;
+ if (!folder)
+ continue;
for (int j = 0; j < folder->rowCount(); ++j) {
QStandardItem* game_item = folder->child(j, 0);
- if (!game_item || game_item->data(GameListItem::TypeRole).value() != GameListItemType::Game) {
+ if (!game_item ||
+ game_item->data(GameListItem::TypeRole).value() !=
+ GameListItemType::Game) {
continue;
}
- u64 program_id = game_item->data(GameListItemPath::ProgramIdRole).toULongLong();
+ u64 program_id =
+ game_item->data(GameListItemPath::ProgramIdRole).toULongLong();
auto it = FindMatchingCompatibilityEntry(compatibility_list, program_id);
if (it != compatibility_list.end()) {
- folder->setChild(j, COLUMN_COMPATIBILITY, new GameListItemCompat(it->second.first));
+ folder->setChild(j, COLUMN_COMPATIBILITY,
+ new GameListItemCompat(it->second.first));
}
}
}
}
} else {
- LOG_ERROR(Frontend, "Failed to download compatibility list: {}", reply->errorString().toStdString());
+ LOG_ERROR(Frontend, "Failed to download compatibility list: {}",
+ reply->errorString().toStdString());
}
reply->deleteLater();
});
@@ -2228,38 +2513,48 @@ void GameList::onSurpriseMeClicked() {
// Go through the list and gather info for every game (name, icon, path)
for (int i = 0; i < item_model->rowCount(); ++i) {
QStandardItem* folder = item_model->item(i, 0);
- if (!folder || folder->data(GameListItem::TypeRole).value() == GameListItemType::AddDir) {
+ if (!folder || folder->data(GameListItem::TypeRole).value() ==
+ GameListItemType::AddDir) {
continue;
}
for (int j = 0; j < folder->rowCount(); ++j) {
QStandardItem* game_item = folder->child(j, 0);
- if (game_item && game_item->data(GameListItem::TypeRole).value() == GameListItemType::Game) {
+ if (game_item && game_item->data(GameListItem::TypeRole).value() ==
+ GameListItemType::Game) {
QString game_title = game_item->data(GameListItemPath::TitleRole).toString();
if (game_title.isEmpty()) {
std::string filename;
- Common::SplitPath(game_item->data(GameListItemPath::FullPathRole).toString().toStdString(), nullptr, &filename, nullptr);
+ Common::SplitPath(
+ game_item->data(GameListItemPath::FullPathRole).toString().toStdString(),
+ nullptr, &filename, nullptr);
game_title = QString::fromStdString(filename);
}
QPixmap icon = game_item->data(Qt::DecorationRole).value();
if (icon.isNull()) {
// Use a generic icon if a game is missing one
- icon = QIcon::fromTheme(QStringLiteral("application-x-executable")).pixmap(128, 128);
+ icon = QIcon::fromTheme(QStringLiteral("application-x-executable"))
+ .pixmap(128, 128);
}
- all_games.append({
- game_title,
- game_item->data(GameListItemPath::FullPathRole).toString(),
- static_cast(game_item->data(GameListItemPath::ProgramIdRole).toULongLong()),
- icon
- });
+ if (UISettings::values.hidden_paths.contains(
+ game_item->data(GameListItemPath::FullPathRole).toString())) {
+ continue;
+ }
+
+ all_games.append(
+ {game_title, game_item->data(GameListItemPath::FullPathRole).toString(),
+ static_cast(
+ game_item->data(GameListItemPath::ProgramIdRole).toULongLong()),
+ icon});
}
}
}
if (all_games.empty()) {
- QMessageBox::information(this, tr("Surprise Me!"), tr("No games available to choose from!"));
+ QMessageBox::information(this, tr("Surprise Me!"),
+ tr("No games available to choose from!"));
return;
}
@@ -2289,100 +2584,101 @@ void GameList::UpdateAccentColorStyles() {
QColor selection_background_color = accent_color;
selection_background_color.setAlphaF(0.25f); // 25% opacity for a clear selection
const QString selection_background_color_name = QStringLiteral("rgba(%1, %2, %3, %4)")
- .arg(selection_background_color.red())
- .arg(selection_background_color.green())
- .arg(selection_background_color.blue())
- .arg(selection_background_color.alpha());
+ .arg(selection_background_color.red())
+ .arg(selection_background_color.green())
+ .arg(selection_background_color.blue())
+ .arg(selection_background_color.alpha());
// Create a MORE subtle semi-transparent version for the HOVER effect
QColor hover_background_color = accent_color;
hover_background_color.setAlphaF(0.15f); // 15% opacity for a subtle hover
const QString hover_background_color_name = QStringLiteral("rgba(%1, %2, %3, %4)")
- .arg(hover_background_color.red())
- .arg(hover_background_color.green())
- .arg(hover_background_color.blue())
- .arg(hover_background_color.alpha());
+ .arg(hover_background_color.red())
+ .arg(hover_background_color.green())
+ .arg(hover_background_color.blue())
+ .arg(hover_background_color.alpha());
- QString accent_style = QStringLiteral(
- /* Tree View (List View) Selection & Hover Style */
- "QTreeView::item:hover {"
- " background-color: %3;" /* Use the new accent-based hover color */
- " border-radius: 4px;" /* Add a subtle rounding to the hover effect */
- "}"
- "QTreeView::item:selected {"
- " background-color: %2;" /* Use the accent-based selection color */
- " color: palette(text);"
- " border: none;" /* NO BORDER */
- " border-radius: 4px;" /* Keep rounding consistent */
- "}"
- "QTreeView::item:selected:!active {"
- " background-color: palette(light);" /* Use a muted color when window is not focused */
- " border: none;" /* NO BORDER */
- "}"
+ QString accent_style =
+ QStringLiteral(
+ /* Tree View (List View) Selection & Hover Style */
+ "QTreeView::item:hover {"
+ " background-color: %3;" /* Use the new accent-based hover color */
+ " border-radius: 4px;" /* Add a subtle rounding to the hover effect */
+ "}"
+ "QTreeView::item:selected {"
+ " background-color: %2;" /* Use the accent-based selection color */
+ " color: palette(text);"
+ " border: none;" /* NO BORDER */
+ " border-radius: 4px;" /* Keep rounding consistent */
+ "}"
+ "QTreeView::item:selected:!active {"
+ " background-color: palette(light);" /* Use a muted color when window is not focused
+ */
+ " border: none;" /* NO BORDER */
+ "}"
- /* List View (Grid View) Selection Style */
- "QListView::item:selected {"
- " background-color: palette(light);"
- " border: 3px solid %1;"
- " border-radius: 12px;"
- "}"
- "QListView::item:selected:!active {"
- " background-color: transparent;"
- " border: 3px solid palette(mid);"
- "}"
+ /* List View (Grid View) Selection Style */
+ "QListView::item:selected {"
+ " background-color: palette(light);"
+ " border: 3px solid %1;"
+ " border-radius: 12px;"
+ "}"
+ "QListView::item:selected:!active {"
+ " background-color: transparent;"
+ " border: 3px solid palette(mid);"
+ "}"
- /* ScrollBar Styling */
- "QScrollBar:vertical {"
- " border: 1px solid black;"
- " background: palette(base);"
- " width: 12px;"
- " margin: 0px;"
- "}"
- "QScrollBar::handle:vertical {"
- " background: %1;"
- " min-height: 20px;"
- " border-radius: 5px;"
- " border: 1px solid black;"
- "}"
- ).arg(color_name, selection_background_color_name, hover_background_color_name);
+ /* ScrollBar Styling */
+ "QScrollBar:vertical {"
+ " border: 1px solid black;"
+ " background: palette(base);"
+ " width: 12px;"
+ " margin: 0px;"
+ "}"
+ "QScrollBar::handle:vertical {"
+ " background: %1;"
+ " min-height: 20px;"
+ " border-radius: 5px;"
+ " border: 1px solid black;"
+ "}")
+ .arg(color_name, selection_background_color_name, hover_background_color_name);
// Apply the combined base styles and new accent styles to each view
tree_view->setStyleSheet(QStringLiteral("QTreeView{ border: none; }") + accent_style);
- list_view->setStyleSheet(QStringLiteral("QListView{ border: none; background: transparent; } QListView::item { text-align: center; padding: 5px; }") + accent_style);
+ list_view->setStyleSheet(
+ QStringLiteral("QListView{ border: none; background: transparent; } QListView::item { "
+ "text-align: center; padding: 5px; }") +
+ accent_style);
// Update the toolbar buttons as before
- QString button_base_style = QStringLiteral(
- "QToolButton {"
- " border: 1px solid palette(mid);"
- " border-radius: 4px;"
- " background: palette(button);"
- "}"
- "QToolButton:hover {"
- " background: palette(light);"
- "}"
- );
- QString button_checked_style = QStringLiteral(
- "QToolButton:checked {"
- " background: %1;"
- " border-color: %1;"
- "}"
- ).arg(color_name);
+ QString button_base_style = QStringLiteral("QToolButton {"
+ " border: 1px solid palette(mid);"
+ " border-radius: 4px;"
+ " background: palette(button);"
+ "}"
+ "QToolButton:hover {"
+ " background: palette(light);"
+ "}");
+ QString button_checked_style = QStringLiteral("QToolButton:checked {"
+ " background: %1;"
+ " border-color: %1;"
+ "}")
+ .arg(color_name);
btn_list_view->setStyleSheet(button_base_style + button_checked_style);
btn_grid_view->setStyleSheet(button_base_style + button_checked_style);
- search_field->setStyleSheet(QStringLiteral(
- "QLineEdit {"
- " border: 1px solid palette(mid);"
- " border-radius: 6px;"
- " padding: 4px 8px;"
- " background: palette(base);"
- "}"
- "QLineEdit:focus {"
- " border: 1px solid %1;"
- " background: palette(base);"
- "}"
- ).arg(color_name));
+ search_field->setStyleSheet(QStringLiteral("QLineEdit {"
+ " border: 1px solid palette(mid);"
+ " border-radius: 6px;"
+ " padding: 4px 8px;"
+ " background: palette(base);"
+ "}"
+ "QLineEdit:focus {"
+ " border: 1px solid %1;"
+ " background: palette(base);"
+ "}")
+ .arg(color_name));
}
void GameList::ToggleHidden(const QString& path) {
@@ -2419,9 +2715,7 @@ void GameList::OnEmulationEnded() {
fade_out_anim->setEasingCurve(QEasingCurve::OutQuad);
// When the fade-out is complete, hide the overlay widget
- connect(fade_out_anim, &QPropertyAnimation::finished, this, [this]() {
- fade_overlay->hide();
- });
+ connect(fade_out_anim, &QPropertyAnimation::finished, this, [this]() { fade_overlay->hide(); });
fade_out_anim->start(QAbstractAnimation::DeleteWhenStopped);
}
diff --git a/src/citron/game_list.h b/src/citron/game_list.h
index dc591957e..ec919bf25 100644
--- a/src/citron/game_list.h
+++ b/src/citron/game_list.h
@@ -12,27 +12,29 @@
#include
#include
#include
-#include
-#include
+#include
+#include
#include
+#include
+#include
+#include
#include
#include
-#include
#include
#include
#include
#include
#include
#include
-#include
-#include
+
+#include "citron/compatibility_list.h"
+#include "citron/multiplayer/state.h"
+#include "citron/play_time_manager.h"
#include "common/common_types.h"
#include "core/core.h"
#include "uisettings.h"
-#include "citron/compatibility_list.h"
-#include "citron/play_time_manager.h"
-#include "citron/multiplayer/state.h"
+
class ControllerNavigation;
class GameListWorker;
@@ -43,8 +45,8 @@ enum class AmLaunchType;
enum class StartGameType;
namespace FileSys {
- class ManualContentProvider;
- class VfsFilesystem;
+class ManualContentProvider;
+class VfsFilesystem;
} // namespace FileSys
enum class GameListOpenTarget {
@@ -190,8 +192,10 @@ private:
void FilterTreeView(const QString& filter_text);
void PopupContextMenu(const QPoint& menu_location);
- void AddGamePopup(QMenu& context_menu, u64 program_id, const std::string& path, const QString& game_name);
- void AddCustomDirPopup(QMenu& context_menu, QModelIndex selected);
+ void AddGamePopup(QMenu& context_menu, u64 program_id, const std::string& path,
+ const QString& game_name);
+ void AddCustomDirPopup(QMenu& context_menu, QModelIndex selected,
+ bool show_hidden_action = true);
void AddPermDirPopup(QMenu& context_menu, QModelIndex selected);
void AddFavoritesPopup(QMenu& context_menu);
diff --git a/src/citron/game_list_p.h b/src/citron/game_list_p.h
index 85a428d9e..dbbebdb1b 100644
--- a/src/citron/game_list_p.h
+++ b/src/citron/game_list_p.h
@@ -18,12 +18,13 @@
#include
#include
-#include "common/common_types.h"
-#include "common/logging/log.h"
-#include "common/string_util.h"
#include "citron/play_time_manager.h"
#include "citron/uisettings.h"
#include "citron/util/util.h"
+#include "common/common_types.h"
+#include "common/logging/log.h"
+#include "common/string_util.h"
+
enum class GameListItemType {
Game = QStandardItem::UserType + 1,
@@ -62,7 +63,8 @@ static QPixmap CreateRoundIcon(const QPixmap& pixmap, u32 size) {
painter.setRenderHint(QPainter::Antialiasing);
// Create a rounded rectangle clipping path
- const int radius = size / 8; // Adjust this value to control roundness (size/8 gives subtle rounding)
+ const int radius =
+ size / 8; // Adjust this value to control roundness (size/8 gives subtle rounding)
QPainterPath path;
path.addRoundedRect(0, 0, size, size, radius, radius);
painter.setClipPath(path);
@@ -98,6 +100,7 @@ public:
static constexpr int FullPathRole = SortRole + 2;
static constexpr int ProgramIdRole = SortRole + 3;
static constexpr int FileTypeRole = SortRole + 4;
+ static constexpr int HighResIconRole = SortRole + 5;
GameListItemPath() = default;
GameListItemPath(const QString& game_path, const std::vector& picture_data,
@@ -115,6 +118,9 @@ public:
picture = GetDefaultIcon(size);
}
+ // Store unscaled pixmap for high-quality animations
+ setData(picture, HighResIconRole);
+
// Create a round icon
QPixmap round_picture = CreateRoundIcon(picture, size);
diff --git a/src/citron/game_list_worker.cpp b/src/citron/game_list_worker.cpp
index b97225258..0a57cbaa8 100644
--- a/src/citron/game_list_worker.cpp
+++ b/src/citron/game_list_worker.cpp
@@ -11,16 +11,22 @@
#include
#include
+#include
#include
#include
#include
-#include
+#include
#include
#include
-#include
+#include
#include
-#include
+
+#include "citron/compatibility_list.h"
+#include "citron/game_list.h"
+#include "citron/game_list_p.h"
+#include "citron/game_list_worker.h"
+#include "citron/uisettings.h"
#include "common/fs/fs.h"
#include "common/fs/path_util.h"
#include "core/core.h"
@@ -33,11 +39,7 @@
#include "core/file_sys/registered_cache.h"
#include "core/file_sys/submission_package.h"
#include "core/loader/loader.h"
-#include "citron/compatibility_list.h"
-#include "citron/game_list.h"
-#include "citron/game_list_p.h"
-#include "citron/game_list_worker.h"
-#include "citron/uisettings.h"
+
namespace {
@@ -76,9 +78,8 @@ std::string GetCacheKey(const std::string& file_path) {
}
const auto path_str = Common::FS::PathToUTF8String(normalized_path);
- const auto hash = QCryptographicHash::hash(
- QByteArray::fromStdString(path_str),
- QCryptographicHash::Sha256);
+ const auto hash =
+ QCryptographicHash::hash(QByteArray::fromStdString(path_str), QCryptographicHash::Sha256);
return hash.toHex().toStdString();
}
@@ -90,7 +91,8 @@ void LoadGameMetadataCache() {
game_metadata_cache.clear();
- const auto cache_dir = Common::FS::GetCitronPath(Common::FS::CitronPath::CacheDir) / "game_list";
+ const auto cache_dir =
+ Common::FS::GetCitronPath(Common::FS::CitronPath::CacheDir) / "game_list";
const auto cache_file = Common::FS::PathToUTF8String(cache_dir / "game_metadata_cache.json");
if (!Common::FS::Exists(cache_file)) {
@@ -115,15 +117,19 @@ void LoadGameMetadataCache() {
const std::string key = entry[QStringLiteral("key")].toString().toStdString();
CachedGameMetadata metadata;
- metadata.program_id = entry[QStringLiteral("program_id")].toString().toULongLong(nullptr, 16);
- metadata.file_type = static_cast(entry[QStringLiteral("file_type")].toInt());
- metadata.file_size = static_cast(entry[QStringLiteral("file_size")].toVariant().toULongLong());
+ metadata.program_id =
+ entry[QStringLiteral("program_id")].toString().toULongLong(nullptr, 16);
+ metadata.file_type =
+ static_cast(entry[QStringLiteral("file_type")].toInt());
+ metadata.file_size =
+ static_cast(entry[QStringLiteral("file_size")].toVariant().toULongLong());
metadata.title = entry[QStringLiteral("title")].toString().toStdString();
metadata.file_path = entry[QStringLiteral("file_path")].toString().toStdString();
- metadata.modification_time = entry[QStringLiteral("modification_time")].toVariant().toLongLong();
+ metadata.modification_time =
+ entry[QStringLiteral("modification_time")].toVariant().toLongLong();
- const QByteArray icon_data = QByteArray::fromBase64(
- entry[QStringLiteral("icon")].toString().toUtf8());
+ const QByteArray icon_data =
+ QByteArray::fromBase64(entry[QStringLiteral("icon")].toString().toUtf8());
metadata.icon.assign(icon_data.begin(), icon_data.end());
if (metadata.IsValid()) {
@@ -138,7 +144,8 @@ void SaveGameMetadataCache() {
return;
}
- const auto cache_dir = Common::FS::GetCitronPath(Common::FS::CitronPath::CacheDir) / "game_list";
+ const auto cache_dir =
+ Common::FS::GetCitronPath(Common::FS::CitronPath::CacheDir) / "game_list";
const auto cache_file = Common::FS::PathToUTF8String(cache_dir / "game_metadata_cache.json");
void(Common::FS::CreateParentDirs(cache_file));
@@ -154,7 +161,8 @@ void SaveGameMetadataCache() {
entry[QStringLiteral("file_size")] = static_cast(metadata.file_size);
entry[QStringLiteral("title")] = QString::fromStdString(metadata.title);
entry[QStringLiteral("file_path")] = QString::fromStdString(metadata.file_path);
- entry[QStringLiteral("modification_time")] = static_cast(metadata.modification_time);
+ entry[QStringLiteral("modification_time")] =
+ static_cast(metadata.modification_time);
const QByteArray icon_data(reinterpret_cast(metadata.icon.data()),
static_cast(metadata.icon.size()));
@@ -189,8 +197,8 @@ const CachedGameMetadata* GetCachedGameMetadata(const std::string& file_path) {
return nullptr;
}
- const auto mod_time_seconds = std::chrono::duration_cast(
- mod_time.time_since_epoch()).count();
+ const auto mod_time_seconds =
+ std::chrono::duration_cast(mod_time.time_since_epoch()).count();
const std::string key = GetCacheKey(file_path);
const auto it = game_metadata_cache.find(key);
@@ -211,7 +219,8 @@ const CachedGameMetadata* GetCachedGameMetadata(const std::string& file_path) {
// Store game metadata in cache
void CacheGameMetadata(const std::string& file_path, u64 program_id, Loader::FileType file_type,
- std::size_t file_size, const std::string& title, const std::vector& icon) {
+ std::size_t file_size, const std::string& title,
+ const std::vector& icon) {
if (!UISettings::values.cache_game_list) {
return;
}
@@ -222,8 +231,8 @@ void CacheGameMetadata(const std::string& file_path, u64 program_id, Loader::Fil
return;
}
- const auto mod_time_seconds = std::chrono::duration_cast(
- mod_time.time_since_epoch()).count();
+ const auto mod_time_seconds =
+ std::chrono::duration_cast