mirror of
https://git.eden-emu.dev/archive/citron
synced 2026-03-22 17:46:08 -04:00
Merge pull request 'feat(hotkeys): Introduce Hotkey_Profile_Manager' (#121) from hotkey-profile-manager into main
Reviewed-on: https://git.citron-emu.org/Citron/Emulator/pulls/121
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 <cmath>
|
||||
#include <memory>
|
||||
#include <QApplication>
|
||||
@@ -20,17 +19,12 @@
|
||||
#include <QString>
|
||||
#include <QTimer>
|
||||
#include <QVBoxLayout>
|
||||
#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<ConfigureHotkeys>(system_.HIDCore(), this)},
|
||||
hotkeys_tab{std::make_unique<ConfigureHotkeys>(registry, system_.HIDCore(), this)},
|
||||
input_tab{std::make_unique<ConfigureInput>(system_, this)},
|
||||
network_tab{std::make_unique<ConfigureNetwork>(system_, this)},
|
||||
profile_tab{std::make_unique<ConfigureProfileManager>(system_, this)},
|
||||
@@ -103,8 +103,8 @@ ConfigureDialog::ConfigureDialog(QWidget* parent, HotkeyRegistry& registry_,
|
||||
web_tab{std::make_unique<ConfigureWeb>(this)} {
|
||||
|
||||
if (auto* main_window = qobject_cast<GMainWindow*>(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<int>(&QButtonGroup::idClicked), this, &ConfigureDialog::AnimateTabSwitch);
|
||||
connect(tab_button_group.get(), qOverload<int>(&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<QGraphicsOpacityEffect*>(ui->buttonBox->graphicsEffect());
|
||||
auto* button_opacity_effect =
|
||||
qobject_cast<QGraphicsOpacityEffect*>(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()) {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
// SPDX-FileCopyrightText: 2017 Citra Emulator Project
|
||||
// SPDX-FileCopyrightText: 2026 Citron Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <QFileDialog>
|
||||
#include <QInputDialog>
|
||||
#include <QMenu>
|
||||
#include <QMessageBox>
|
||||
#include <QStandardItemModel>
|
||||
@@ -9,19 +12,21 @@
|
||||
#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<Ui::ConfigureHotkeys>()),
|
||||
ConfigureHotkeys::ConfigureHotkeys(HotkeyRegistry& registry_, Core::HID::HIDCore& hid_core,
|
||||
QWidget* parent)
|
||||
: QWidget(parent), ui(std::make_unique<Ui::ConfigureHotkeys>()), registry(registry_),
|
||||
controller(new Core::HID::EmulatedController(Core::HID::NpadIdType::Player1)),
|
||||
timeout_timer(std::make_unique<QTimer>()), poll_timer(std::make_unique<QTimer>()) {
|
||||
ui->setupUi(this);
|
||||
setFocusPolicy(Qt::ClickFocus);
|
||||
@@ -36,14 +41,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<int>(&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 +72,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 +83,177 @@ 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 auto& profiles = profile_manager.GetProfiles();
|
||||
const auto& current_profile_name = profiles.current_profile;
|
||||
|
||||
// Use default if current profile missing (safety)
|
||||
std::vector<Hotkey::BackendShortcut> current_shortcuts;
|
||||
if (profiles.profiles.count(current_profile_name)) {
|
||||
current_shortcuts = profiles.profiles.at(current_profile_name);
|
||||
} else if (profiles.profiles.count("Default")) {
|
||||
current_shortcuts = profiles.profiles.at("Default");
|
||||
}
|
||||
|
||||
// Map overrides for easy lookup: Key = Group + Name
|
||||
std::map<std::pair<std::string, std::string>, 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));
|
||||
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 controller_keyseq_str = QString::fromStdString(hotkey.controller_keyseq);
|
||||
|
||||
if (overrides.count({group_name, action_name})) {
|
||||
const auto& overridden = overrides.at({group_name, action_name});
|
||||
keyseq_str = QKeySequence(QString::fromStdString(overridden.shortcut.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));
|
||||
|
||||
auto* keyseq_item = new QStandardItem(keyseq_str);
|
||||
// Store raw keyseq string logic?
|
||||
// The system likely expects QKeySequence string format.
|
||||
keyseq_item->setData(keyseq_str, Qt::UserRole);
|
||||
keyseq_item->setEditable(false);
|
||||
|
||||
auto* controller_item = new QStandardItem(controller_keyseq_str);
|
||||
controller_item->setEditable(false);
|
||||
|
||||
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()) {
|
||||
if (profile_manager.ImportProfile(fileName.toStdString())) {
|
||||
UpdateProfileList();
|
||||
} else {
|
||||
QMessageBox::warning(this, tr("Error"), tr("Failed to import profile."));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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()) {
|
||||
if (!profile_manager.ExportProfile(current.toStdString(), fileName.toStdString())) {
|
||||
QMessageBox::warning(this, tr("Error"), tr("Failed to export profile."));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ConfigureHotkeys::OnProfileChanged(int index) {
|
||||
if (index == -1)
|
||||
return;
|
||||
const std::string name = ui->combo_box_profile->currentText().toStdString();
|
||||
profile_manager.SetCurrentProfile(name);
|
||||
Populate();
|
||||
}
|
||||
|
||||
void ConfigureHotkeys::changeEvent(QEvent* event) {
|
||||
@@ -108,6 +266,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++) {
|
||||
@@ -307,28 +466,67 @@ std::pair<bool, QString> 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. Update the runtime UISettings (Registry)
|
||||
// We iterate the model and match against UISettings::values.shortcuts
|
||||
const auto& children = model->invisibleRootItem();
|
||||
for (int group_row = 0; group_row < children->rowCount(); group_row++) {
|
||||
const auto& group_item = children->child(group_row);
|
||||
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);
|
||||
|
||||
const std::string group_name = group_item->data().toString().toStdString();
|
||||
const std::string action_name = action_item->data().toString().toStdString();
|
||||
|
||||
// Update UISettings (Runtime)
|
||||
for (auto& s : UISettings::values.shortcuts) {
|
||||
if (s.group == group_name && s.name == action_name) {
|
||||
s.shortcut.keyseq = keyseq_item->text().toStdString();
|
||||
s.shortcut.controller_keyseq = controller_item->text().toStdString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
registry.SaveHotkeys();
|
||||
// 2. Update the ProfileManager (Storage)
|
||||
const std::string current_profile_name = profile_manager.GetProfiles().current_profile;
|
||||
// We need to modify the profile in the manager. GetProfiles() returns const ref.
|
||||
// We need a method to UpdateProfile or we need to cast away const (bad) or rely on reference if
|
||||
// GetProfiles wasn't const. The previous implementation of GetProfiles was const.
|
||||
// ProfileManager needs a method `UpdateCurrentProfile(vector<BackendShortcut>)`?
|
||||
// Or we can just use the internal map if we were friends.
|
||||
// Reconstructing BackendShortcuts from UI
|
||||
std::vector<Hotkey::BackendShortcut> new_shortcuts;
|
||||
for (int group_row = 0; group_row < children->rowCount(); group_row++) {
|
||||
const auto& group_item = children->child(group_row);
|
||||
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_item->data().toString().toStdString();
|
||||
s.name = action_item->data().toString().toStdString();
|
||||
s.shortcut.keyseq = keyseq_item->text().toStdString();
|
||||
s.shortcut.controller_keyseq = controller_item->text().toStdString();
|
||||
// Context/Repeat need to be preserved from UserRole data
|
||||
// For now, let's grab from UISettings since we just updated it or match it.
|
||||
|
||||
for (const auto& original : UISettings::values.shortcuts) {
|
||||
if (original.group == s.group && original.name == s.name) {
|
||||
s.shortcut.context = original.shortcut.context;
|
||||
s.shortcut.repeat = original.shortcut.repeat;
|
||||
break;
|
||||
}
|
||||
}
|
||||
new_shortcuts.push_back(s);
|
||||
}
|
||||
}
|
||||
|
||||
profile_manager.SetProfileShortcuts(current_profile_name, new_shortcuts);
|
||||
profile_manager.Save();
|
||||
}
|
||||
|
||||
void ConfigureHotkeys::RestoreDefaults() {
|
||||
@@ -339,11 +537,10 @@ void ConfigureHotkeys::RestoreDefaults() {
|
||||
QStandardItem* parent = model->item(group_row, 0);
|
||||
|
||||
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!.
|
||||
// This bounds check prevents a crash.
|
||||
if (hotkey_index >= total_default_hotkeys) {
|
||||
QMessageBox::information(this, tr("Success!"),
|
||||
tr("Citron's Default hotkey entries have been restored!"));
|
||||
tr("Citron's Default hotkey entries have been restored!"));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
// SPDX-FileCopyrightText: 2017 Citra Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <QStandardItemModel>
|
||||
#include <QWidget>
|
||||
#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();
|
||||
|
||||
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);
|
||||
@@ -59,6 +65,8 @@ private:
|
||||
QString GetButtonCombinationName(Core::HID::NpadButton button, bool home, bool capture) const;
|
||||
|
||||
std::unique_ptr<Ui::ConfigureHotkeys> ui;
|
||||
Hotkey::ProfileManager profile_manager;
|
||||
HotkeyRegistry& registry;
|
||||
|
||||
QStandardItemModel* model;
|
||||
|
||||
|
||||
@@ -18,16 +18,65 @@
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_ProfileSelect">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_2">
|
||||
<widget class="QLabel" name="label_profile">
|
||||
<property name="text">
|
||||
<string>Double-click on a binding to change it.</string>
|
||||
<string>Hotkey Profile:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<widget class="QComboBox" name="combo_box_profile">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_Actions">
|
||||
<item>
|
||||
<widget class="QPushButton" name="button_new_profile">
|
||||
<property name="text">
|
||||
<string>New</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="button_delete_profile">
|
||||
<property name="text">
|
||||
<string>Delete</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="button_rename_profile">
|
||||
<property name="text">
|
||||
<string>Rename</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="button_import_profile">
|
||||
<property name="text">
|
||||
<string>Import</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="button_export_profile">
|
||||
<property name="text">
|
||||
<string>Export</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_Buttons">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
@@ -48,6 +97,24 @@
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="button_restore_defaults">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>139</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>139</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Restore Defaults</string>
|
||||
</property>
|
||||
@@ -55,6 +122,13 @@
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string>Double-click on a binding to change it.</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
|
||||
239
src/citron/hotkey_profile_manager.cpp
Normal file
239
src/citron/hotkey_profile_manager.cpp
Normal file
@@ -0,0 +1,239 @@
|
||||
// SPDX-FileCopyrightText: 2026 citron Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <algorithm>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QMap>
|
||||
#include <QString>
|
||||
|
||||
#include "citron/hotkey_profile_manager.h"
|
||||
#include "common/fs/path_util.h"
|
||||
#include "common/logging/log.h"
|
||||
|
||||
namespace Hotkey {
|
||||
|
||||
ProfileManager::ProfileManager() {
|
||||
Load();
|
||||
}
|
||||
|
||||
ProfileManager::~ProfileManager() = default;
|
||||
|
||||
static std::string GetSaveFilePath() {
|
||||
const auto save_dir =
|
||||
Common::FS::GetCitronPath(Common::FS::CitronPath::ConfigDir); // Saved in ConfigDir now
|
||||
return Common::FS::PathToUTF8String(save_dir / "hotkey_profiles.json");
|
||||
}
|
||||
|
||||
// JSON Serialization Helpers
|
||||
static QJsonObject SerializeShortcut(const BackendShortcut& shortcut) {
|
||||
QJsonObject obj;
|
||||
obj[QStringLiteral("name")] = QString::fromStdString(shortcut.name);
|
||||
obj[QStringLiteral("group")] = QString::fromStdString(shortcut.group);
|
||||
obj[QStringLiteral("keyseq")] = QString::fromStdString(shortcut.shortcut.keyseq);
|
||||
obj[QStringLiteral("controller_keyseq")] =
|
||||
QString::fromStdString(shortcut.shortcut.controller_keyseq);
|
||||
obj[QStringLiteral("context")] = shortcut.shortcut.context;
|
||||
obj[QStringLiteral("repeat")] = shortcut.shortcut.repeat;
|
||||
return obj;
|
||||
}
|
||||
|
||||
static BackendShortcut DeserializeShortcut(const QJsonObject& obj) {
|
||||
BackendShortcut s;
|
||||
s.name = obj[QStringLiteral("name")].toString().toStdString();
|
||||
s.group = obj[QStringLiteral("group")].toString().toStdString();
|
||||
s.shortcut.keyseq = obj[QStringLiteral("keyseq")].toString().toStdString();
|
||||
s.shortcut.controller_keyseq =
|
||||
obj[QStringLiteral("controller_keyseq")].toString().toStdString();
|
||||
s.shortcut.context = obj[QStringLiteral("context")].toInt();
|
||||
s.shortcut.repeat = obj[QStringLiteral("repeat")].toBool();
|
||||
return s;
|
||||
}
|
||||
|
||||
void ProfileManager::Load() {
|
||||
const auto path = GetSaveFilePath();
|
||||
QFile file(QString::fromStdString(path));
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
LOG_INFO(Config, "hotkey_profiles.json not found, creating new.");
|
||||
return;
|
||||
}
|
||||
|
||||
const QByteArray data = file.readAll();
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(data);
|
||||
const QJsonObject root = doc.object();
|
||||
|
||||
profiles.profiles.clear();
|
||||
|
||||
if (root.contains(QStringLiteral("current_profile"))) {
|
||||
profiles.current_profile = root[QStringLiteral("current_profile")].toString().toStdString();
|
||||
}
|
||||
|
||||
if (root.contains(QStringLiteral("profiles"))) {
|
||||
const QJsonObject profiles_obj = root[QStringLiteral("profiles")].toObject();
|
||||
for (auto it = profiles_obj.begin(); it != profiles_obj.end(); ++it) {
|
||||
const QString profile_name = it.key();
|
||||
const QJsonArray shortcuts_arr = it.value().toArray();
|
||||
std::vector<BackendShortcut> shortcuts;
|
||||
for (const auto& val : shortcuts_arr) {
|
||||
shortcuts.push_back(DeserializeShortcut(val.toObject()));
|
||||
}
|
||||
profiles.profiles[profile_name.toStdString()] = shortcuts;
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure default profile exists
|
||||
if (profiles.profiles.empty()) {
|
||||
profiles.profiles["Default"] = {};
|
||||
}
|
||||
}
|
||||
|
||||
void ProfileManager::Save() {
|
||||
const auto path = GetSaveFilePath();
|
||||
QFile file(QString::fromStdString(path));
|
||||
if (!file.open(QIODevice::WriteOnly)) {
|
||||
LOG_ERROR(Config, "Failed to open hotkey_profiles.json for writing.");
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject root;
|
||||
root[QStringLiteral("current_profile")] = QString::fromStdString(profiles.current_profile);
|
||||
|
||||
QJsonObject profiles_obj;
|
||||
for (const auto& [name, shortcuts] : profiles.profiles) {
|
||||
QJsonArray shortcuts_arr;
|
||||
for (const auto& s : shortcuts) {
|
||||
shortcuts_arr.append(SerializeShortcut(s));
|
||||
}
|
||||
profiles_obj[QString::fromStdString(name)] = shortcuts_arr;
|
||||
}
|
||||
root[QStringLiteral("profiles")] = profiles_obj;
|
||||
|
||||
file.write(QJsonDocument(root).toJson());
|
||||
}
|
||||
|
||||
bool ProfileManager::CreateProfile(const std::string& profile_name) {
|
||||
if (profile_name.empty())
|
||||
return false;
|
||||
|
||||
if (profiles.profiles.size() >= MAX_PROFILES) {
|
||||
return false;
|
||||
}
|
||||
if (profiles.profiles.count(profile_name)) {
|
||||
return false; // Already exists
|
||||
}
|
||||
|
||||
profiles.profiles[profile_name] = {}; // Create empty, populated later by UI
|
||||
Save();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ProfileManager::DeleteProfile(const std::string& profile_name) {
|
||||
if (profile_name == "Default")
|
||||
return false; // Cannot delete default
|
||||
|
||||
if (profiles.profiles.erase(profile_name)) {
|
||||
if (profiles.current_profile == profile_name) {
|
||||
profiles.current_profile = "Default";
|
||||
}
|
||||
Save();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ProfileManager::RenameProfile(const std::string& old_name, const std::string& new_name) {
|
||||
if (old_name == "Default")
|
||||
return false; // Cannot rename default
|
||||
if (new_name.empty())
|
||||
return false;
|
||||
|
||||
if (!profiles.profiles.count(old_name))
|
||||
return false;
|
||||
if (profiles.profiles.count(new_name))
|
||||
return false;
|
||||
|
||||
auto node = profiles.profiles.extract(old_name);
|
||||
node.key() = new_name;
|
||||
profiles.profiles.insert(std::move(node));
|
||||
|
||||
if (profiles.current_profile == old_name) {
|
||||
profiles.current_profile = new_name;
|
||||
}
|
||||
Save();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ProfileManager::SetCurrentProfile(const std::string& profile_name) {
|
||||
if (!profiles.profiles.count(profile_name))
|
||||
return false;
|
||||
|
||||
profiles.current_profile = profile_name;
|
||||
Save();
|
||||
return true;
|
||||
}
|
||||
|
||||
void ProfileManager::SetProfileShortcuts(const std::string& profile_name,
|
||||
const std::vector<BackendShortcut>& shortcuts) {
|
||||
if (profiles.profiles.count(profile_name)) {
|
||||
profiles.profiles[profile_name] = shortcuts;
|
||||
}
|
||||
}
|
||||
|
||||
bool ProfileManager::ExportProfile(const std::string& profile_name, const std::string& file_path) {
|
||||
if (!profiles.profiles.count(profile_name))
|
||||
return false;
|
||||
|
||||
QJsonObject root;
|
||||
root[QStringLiteral("name")] = QString::fromStdString(profile_name);
|
||||
|
||||
QJsonArray shortcuts_arr;
|
||||
for (const auto& s : profiles.profiles.at(profile_name)) {
|
||||
shortcuts_arr.append(SerializeShortcut(s));
|
||||
}
|
||||
root[QStringLiteral("shortcuts")] = shortcuts_arr;
|
||||
|
||||
QFile file(QString::fromStdString(file_path));
|
||||
if (!file.open(QIODevice::WriteOnly))
|
||||
return false;
|
||||
|
||||
file.write(QJsonDocument(root).toJson());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ProfileManager::ImportProfile(const std::string& file_path) {
|
||||
QFile file(QString::fromStdString(file_path));
|
||||
if (!file.open(QIODevice::ReadOnly))
|
||||
return false;
|
||||
|
||||
const QByteArray data = file.readAll();
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(data);
|
||||
const QJsonObject root = doc.object();
|
||||
|
||||
if (!root.contains(QStringLiteral("name")) || !root.contains(QStringLiteral("shortcuts")))
|
||||
return false;
|
||||
|
||||
std::string profile_name = root[QStringLiteral("name")].toString().toStdString();
|
||||
|
||||
// Handle name collision
|
||||
if (profiles.profiles.count(profile_name)) {
|
||||
profile_name += " (Imported)";
|
||||
}
|
||||
|
||||
if (profiles.profiles.size() >= MAX_PROFILES)
|
||||
return false;
|
||||
|
||||
std::vector<BackendShortcut> shortcuts;
|
||||
const QJsonArray arr = root[QStringLiteral("shortcuts")].toArray();
|
||||
for (const auto& val : arr) {
|
||||
shortcuts.push_back(DeserializeShortcut(val.toObject()));
|
||||
}
|
||||
|
||||
profiles.profiles[profile_name] = shortcuts;
|
||||
Save();
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace Hotkey
|
||||
69
src/citron/hotkey_profile_manager.h
Normal file
69
src/citron/hotkey_profile_manager.h
Normal file
@@ -0,0 +1,69 @@
|
||||
// SPDX-FileCopyrightText: 2026 citron Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include "common/uuid.h"
|
||||
|
||||
namespace Hotkey {
|
||||
|
||||
// A backend-only representation of a shortcut, free of any Qt types.
|
||||
struct BackendContextualShortcut {
|
||||
std::string keyseq;
|
||||
std::string controller_keyseq;
|
||||
int context;
|
||||
bool repeat;
|
||||
};
|
||||
|
||||
struct BackendShortcut {
|
||||
std::string name;
|
||||
std::string group;
|
||||
BackendContextualShortcut shortcut;
|
||||
};
|
||||
|
||||
// Contains all hotkey profile data for a single user
|
||||
struct UserHotkeyProfiles {
|
||||
std::map<std::string, std::vector<BackendShortcut>> profiles;
|
||||
std::string current_profile = "Default";
|
||||
};
|
||||
|
||||
class ProfileManager {
|
||||
public:
|
||||
ProfileManager();
|
||||
~ProfileManager();
|
||||
|
||||
// Profile Access
|
||||
const UserHotkeyProfiles& GetProfiles() const {
|
||||
return profiles;
|
||||
}
|
||||
|
||||
// Profile Management
|
||||
bool CreateProfile(const std::string& profile_name);
|
||||
bool DeleteProfile(const std::string& profile_name);
|
||||
bool RenameProfile(const std::string& old_name, const std::string& new_name);
|
||||
bool SetCurrentProfile(const std::string& profile_name);
|
||||
void SetProfileShortcuts(const std::string& profile_name,
|
||||
const std::vector<BackendShortcut>& shortcuts);
|
||||
|
||||
// Import/Export
|
||||
bool ExportProfile(const std::string& profile_name, const std::string& file_path);
|
||||
bool ImportProfile(const std::string& file_path);
|
||||
|
||||
// IO
|
||||
void Load();
|
||||
void Save();
|
||||
|
||||
// Constants
|
||||
static constexpr size_t MAX_PROFILES = 5;
|
||||
|
||||
private:
|
||||
// Global profiles data
|
||||
UserHotkeyProfiles profiles;
|
||||
};
|
||||
|
||||
} // namespace Hotkey
|
||||
Reference in New Issue
Block a user