From 97814d3e593b0827e0d79b9faa51431e55052dd2 Mon Sep 17 00:00:00 2001 From: flodavid Date: Thu, 18 Jan 2024 20:51:39 +0000 Subject: [PATCH] Load custom Qt themes from yuzu data directory - Directory is qt_themes, each theme must be in one folder - It should contain a file "style.qss" - It may contain an "icons" sub-directory, to override included icons (with files like mytheme/icons/colorful/48x48/star.png for example) - Directories ending by "_dark" are reserved for dark variant icons. They are not listed as themes in the UI. - If theme directory contains "dark" or "midnight", theme will be considered dark --- src/common/fs/fs_paths.h | 3 +- src/common/fs/path_util.cpp | 3 +- src/common/fs/path_util.h | 3 +- src/suyu/configuration/configure_ui.cpp | 27 +++++++++-- src/suyu/configuration/qt_config.cpp | 11 ++--- src/suyu/debugger/wait_tree.cpp | 5 +- src/suyu/main.cpp | 64 +++++++++++++++++-------- src/suyu/main.h | 6 +++ src/suyu/uisettings.cpp | 19 ++++---- src/suyu/uisettings.h | 23 ++++----- 10 files changed, 102 insertions(+), 62 deletions(-) diff --git a/src/common/fs/fs_paths.h b/src/common/fs/fs_paths.h index 3720976efe..de06571a6f 100644 --- a/src/common/fs/fs_paths.h +++ b/src/common/fs/fs_paths.h @@ -15,6 +15,7 @@ #define CONFIG_DIR "config" #define CRASH_DUMPS_DIR "crash_dumps" #define DUMP_DIR "dump" +#define ICONS_DIR "icons" #define KEYS_DIR "keys" #define LOAD_DIR "load" #define LOG_DIR "log" @@ -24,7 +25,7 @@ #define SDMC_DIR "sdmc" #define SHADER_DIR "shader" #define TAS_DIR "tas" -#define ICONS_DIR "icons" +#define THEMES_DIR "qt_themes" // suyu-specific files diff --git a/src/common/fs/path_util.cpp b/src/common/fs/path_util.cpp index e23f53fb9d..9362e18c39 100644 --- a/src/common/fs/path_util.cpp +++ b/src/common/fs/path_util.cpp @@ -121,6 +121,7 @@ public: GenerateSuyuPath(SuyuPath::ConfigDir, suyu_path_config); GenerateSuyuPath(SuyuPath::CrashDumpsDir, suyu_path / CRASH_DUMPS_DIR); GenerateSuyuPath(SuyuPath::DumpDir, suyu_path / DUMP_DIR); + GenerateSuyuPath(SuyuPath::IconsDir, suyu_path / ICONS_DIR); GenerateSuyuPath(SuyuPath::KeysDir, suyu_path / KEYS_DIR); GenerateSuyuPath(SuyuPath::LoadDir, suyu_path / LOAD_DIR); GenerateSuyuPath(SuyuPath::LogDir, suyu_path / LOG_DIR); @@ -130,7 +131,7 @@ public: GenerateSuyuPath(SuyuPath::SDMCDir, suyu_path / SDMC_DIR); GenerateSuyuPath(SuyuPath::ShaderDir, suyu_path / SHADER_DIR); GenerateSuyuPath(SuyuPath::TASDir, suyu_path / TAS_DIR); - GenerateSuyuPath(SuyuPath::IconsDir, suyu_path / ICONS_DIR); + GenerateSuyuPath(SuyuPath::ThemesDir, suyu_path / THEMES_DIR); } private: diff --git a/src/common/fs/path_util.h b/src/common/fs/path_util.h index 2076fbcd43..1ac4a26eea 100644 --- a/src/common/fs/path_util.h +++ b/src/common/fs/path_util.h @@ -17,6 +17,7 @@ enum class SuyuPath { ConfigDir, // Where config files are stored. CrashDumpsDir, // Where crash dumps are stored. DumpDir, // Where dumped data is stored. + IconsDir, // Where Icons for Windows shortcuts are stored. KeysDir, // Where key files are stored. LoadDir, // Where cheat/mod files are stored. LogDir, // Where log files are stored. @@ -26,7 +27,7 @@ enum class SuyuPath { SDMCDir, // Where the emulated SDMC is stored. ShaderDir, // Where shaders are stored. TASDir, // Where TAS scripts are stored. - IconsDir, // Where Icons for Windows shortcuts are stored. + ThemesDir, // Where users should put their custom themes }; /** diff --git a/src/suyu/configuration/configure_ui.cpp b/src/suyu/configuration/configure_ui.cpp index a3648c5b1c..046b34721e 100644 --- a/src/suyu/configuration/configure_ui.cpp +++ b/src/suyu/configuration/configure_ui.cpp @@ -106,11 +106,31 @@ ConfigureUi::ConfigureUi(Core::System& system_, QWidget* parent) InitializeLanguageComboBox(); - for (const auto& theme : UISettings::themes) { + for (const auto& theme : UISettings::included_themes) { ui->theme_combobox->addItem(QString::fromUtf8(theme.first), QString::fromUtf8(theme.second)); } + // Add custom styles stored in yuzu directory + const QDir themes_local_dir( + QString::fromStdString(Common::FS::GetSuyuPathString(Common::FS::SuyuPath::ThemesDir))); + for (const QString& theme_dir : + themes_local_dir.entryList(QDir::NoDot | QDir::NoDotDot | QDir::Dirs)) { + // folders ending with "_dark" are reserved for dark variant icons of other styles + if (theme_dir.endsWith(QStringLiteral("_dark"))) { + continue; + } + // Split at _ and capitalize words in name + QStringList cased_name; + for (QString word : theme_dir.split(QChar::fromLatin1('_'))) { + cased_name.append(word.at(0).toUpper() + word.mid(1)); + } + QString theme_name = cased_name.join(QChar::fromLatin1(' ')); + theme_name += QStringLiteral(" (%1)").arg(tr("Custom")); + + ui->theme_combobox->addItem(theme_name, themes_local_dir.filePath(theme_dir)); + } + InitializeIconSizeComboBox(); InitializeRowComboBoxes(); @@ -164,7 +184,7 @@ ConfigureUi::~ConfigureUi() = default; void ConfigureUi::ApplyConfiguration() { UISettings::values.theme = - ui->theme_combobox->itemData(ui->theme_combobox->currentIndex()).toString().toStdString(); + ui->theme_combobox->itemData(ui->theme_combobox->currentIndex()).toString(); UISettings::values.show_add_ons = ui->show_add_ons->isChecked(); UISettings::values.show_compat = ui->show_compat->isChecked(); UISettings::values.show_size = ui->show_size->isChecked(); @@ -191,8 +211,7 @@ void ConfigureUi::RequestGameListUpdate() { } void ConfigureUi::SetConfiguration() { - ui->theme_combobox->setCurrentIndex( - ui->theme_combobox->findData(QString::fromStdString(UISettings::values.theme))); + ui->theme_combobox->setCurrentIndex(ui->theme_combobox->findData(UISettings::values.theme)); ui->language_combobox->setCurrentIndex(ui->language_combobox->findData( QString::fromStdString(UISettings::values.language.GetValue()))); ui->show_add_ons->setChecked(UISettings::values.show_add_ons.GetValue()); diff --git a/src/suyu/configuration/qt_config.cpp b/src/suyu/configuration/qt_config.cpp index 37951b9c84..2868db4ebb 100644 --- a/src/suyu/configuration/qt_config.cpp +++ b/src/suyu/configuration/qt_config.cpp @@ -260,9 +260,8 @@ void QtConfig::ReadShortcutValues() { void QtConfig::ReadUIValues() { BeginGroup(Settings::TranslateCategory(Settings::Category::Ui)); - UISettings::values.theme = ReadStringSetting( - std::string("theme"), - std::string(UISettings::themes[static_cast(UISettings::default_theme)].second)); + UISettings::values.theme = QString::fromStdString( + ReadStringSetting(std::string("theme"), std::string(UISettings::default_theme))); ReadUIGamelistValues(); ReadUILayoutValues(); @@ -468,10 +467,8 @@ void QtConfig::SaveUIValues() { WriteCategory(Settings::Category::Ui); WriteCategory(Settings::Category::UiGeneral); - WriteStringSetting( - std::string("theme"), UISettings::values.theme, - std::make_optional(std::string( - UISettings::themes[static_cast(UISettings::default_theme)].second))); + WriteStringSetting(std::string("theme"), UISettings::values.theme.toStdString(), + std::make_optional(std::string(UISettings::default_theme))); SaveUIGamelistValues(); SaveUILayoutValues(); diff --git a/src/suyu/debugger/wait_tree.cpp b/src/suyu/debugger/wait_tree.cpp index b339862ba7..b5ee4bcfdf 100644 --- a/src/suyu/debugger/wait_tree.cpp +++ b/src/suyu/debugger/wait_tree.cpp @@ -35,9 +35,8 @@ constexpr std::array, 10> WaitTreeColors{{ }}; bool IsDarkTheme() { - const auto& theme = UISettings::values.theme; - return theme == std::string("qdarkstyle") || theme == std::string("qdarkstyle_midnight_blue") || - theme == std::string("colorful_dark") || theme == std::string("colorful_midnight_blue"); + return UISettings::values.theme.contains(QStringLiteral("dark")) || + UISettings::values.theme.contains(QStringLiteral("midnight")); } } // namespace diff --git a/src/suyu/main.cpp b/src/suyu/main.cpp index 9a3ee7f662..030e4ae511 100644 --- a/src/suyu/main.cpp +++ b/src/suyu/main.cpp @@ -3542,7 +3542,7 @@ void GMainWindow::ResetWindowSize1080() { } void GMainWindow::OnConfigure() { - const auto old_theme = UISettings::values.theme; + const QString old_theme = UISettings::values.theme; const bool old_discord_presence = UISettings::values.enable_discord_presence.GetValue(); const auto old_language_index = Settings::values.language_index.GetValue(); #ifdef __unix__ @@ -4812,9 +4812,8 @@ static void AdjustLinkColor() { } void GMainWindow::UpdateUITheme() { - const QString default_theme = QString::fromUtf8( - UISettings::themes[static_cast(UISettings::default_theme)].second); - QString current_theme = QString::fromStdString(UISettings::values.theme); + QString default_theme = QString::fromStdString(UISettings::default_theme.data()); + QString current_theme = UISettings::values.theme; if (current_theme.isEmpty()) { current_theme = default_theme; @@ -4825,6 +4824,7 @@ void GMainWindow::UpdateUITheme() { AdjustLinkColor(); #else if (current_theme == QStringLiteral("default") || current_theme == QStringLiteral("colorful")) { + LOG_INFO(Frontend, "Theme is default or colorful: {}", current_theme.toStdString()); QIcon::setThemeName(current_theme == QStringLiteral("colorful") ? current_theme : startup_icon_theme); QIcon::setThemeSearchPaths(QStringList(default_theme_paths)); @@ -4832,35 +4832,57 @@ void GMainWindow::UpdateUITheme() { current_theme = QStringLiteral("default_dark"); } } else { + LOG_INFO(Frontend, "Theme is NOT default or colorful: {}", current_theme.toStdString()); QIcon::setThemeName(current_theme); - QIcon::setThemeSearchPaths(QStringList(QStringLiteral(":/icons"))); + // Use icon resources from application binary and current theme subdirectory if it exists + QStringList theme_paths; + theme_paths << QString::fromStdString(":/icons") + << QStringLiteral("%1/%2/icons") + .arg(QString::fromStdString( + Common::FS::GetYuzuPathString(Common::FS::YuzuPath::ThemesDir)), + current_theme); + QIcon::setThemeSearchPaths(theme_paths); AdjustLinkColor(); } #endif if (current_theme != default_theme) { - QString theme_uri{QStringLiteral(":%1/style.qss").arg(current_theme)}; - QFile f(theme_uri); - if (!f.open(QFile::ReadOnly | QFile::Text)) { - LOG_ERROR(Frontend, "Unable to open style \"{}\", fallback to the default theme", - UISettings::values.theme); - current_theme = default_theme; + QString theme_uri{current_theme + QStringLiteral("style.qss")}; + if (tryLoadStylesheet(theme_uri)) { + return; } - } - QString theme_uri{QStringLiteral(":%1/style.qss").arg(current_theme)}; - QFile f(theme_uri); - if (f.open(QFile::ReadOnly | QFile::Text)) { - QTextStream ts(&f); - qApp->setStyleSheet(ts.readAll()); - setStyleSheet(ts.readAll()); - } else { + // Reading new theme failed, loading default stylesheet + LOG_ERROR(Frontend, "Unable to open style \"{}\", fallback to the default theme", + current_theme.toStdString()); + + current_theme = default_theme; + theme_uri = QStringLiteral(":%1/style.qss").arg(default_theme); + if (tryLoadStylesheet(theme_uri)) { + return; + } + + // Reading default failed, loading empty stylesheet LOG_ERROR(Frontend, "Unable to set style \"{}\", stylesheet file not found", - UISettings::values.theme); + current_theme.toStdString()); + qApp->setStyleSheet({}); setStyleSheet({}); } } +bool GMainWindow::tryLoadStylesheet(const QString& theme_path) { + QFile theme_file(theme_path); + if (theme_file.open(QFile::ReadOnly | QFile::Text)) { + LOG_INFO(Frontend, "Loading style in: {}", theme_path.toStdString()); + QTextStream ts(&theme_file); + qApp->setStyleSheet(ts.readAll()); + setStyleSheet(ts.readAll()); + return true; + } + // Opening the file failed + return false; +} + void GMainWindow::LoadTranslation() { bool loaded; @@ -4919,7 +4941,7 @@ void GMainWindow::changeEvent(QEvent* event) { // UpdateUITheme is a decent work around if (event->type() == QEvent::PaletteChange) { const QPalette test_palette(qApp->palette()); - const QString current_theme = QString::fromStdString(UISettings::values.theme); + const QString& current_theme = UISettings::values.theme; // Keeping eye on QPalette::Window to avoid looping. QPalette::Text might be useful too static QColor last_window_color; const QColor window_color = test_palette.color(QPalette::Active, QPalette::Window); diff --git a/src/suyu/main.h b/src/suyu/main.h index e20950e238..6bbcdd7c58 100644 --- a/src/suyu/main.h +++ b/src/suyu/main.h @@ -165,6 +165,12 @@ class GMainWindow : public QMainWindow { CREATE_SHORTCUT_MSGBOX_APPVOLATILE_WARNING, }; + /** + * Try to load a stylesheet from its path. If the path starts with ":/", its embedded in the app + * @returns true if the text file could be opened as read-only + */ + bool tryLoadStylesheet(const QString& theme_path); + public: void filterBarSetChecked(bool state); void UpdateUITheme(); diff --git a/src/suyu/uisettings.cpp b/src/suyu/uisettings.cpp index 60d4063c8c..e382afdf61 100644 --- a/src/suyu/uisettings.cpp +++ b/src/suyu/uisettings.cpp @@ -22,19 +22,18 @@ namespace FS = Common::FS; namespace UISettings { -const Themes themes{{ - {"Default", "default"}, - {"Default Colorful", "colorful"}, - {"Dark", "qdarkstyle"}, - {"Dark Colorful", "colorful_dark"}, - {"Midnight Blue", "qdarkstyle_midnight_blue"}, - {"Midnight Blue Colorful", "colorful_midnight_blue"}, +const Themes included_themes{{ + {"Default", ":/default/"}, + {"Default Colorful", ":/colorful/"}, + {"Dark", ":/qdarkstyle/"}, + {"Dark Colorful", ":/colorful_dark/"}, + {"Midnight Blue", ":/qdarkstyle_midnight_blue/"}, + {"Midnight Blue Colorful", ":/colorful_midnight_blue/"}, }}; bool IsDarkTheme() { - const auto& theme = UISettings::values.theme; - return theme == std::string("qdarkstyle") || theme == std::string("qdarkstyle_midnight_blue") || - theme == std::string("colorful_dark") || theme == std::string("colorful_midnight_blue"); + return UISettings::values.theme.contains(QStringLiteral("dark")) || + UISettings::values.theme.contains(QStringLiteral("midnight")); } Values values = {}; diff --git a/src/suyu/uisettings.h b/src/suyu/uisettings.h index cab889680f..7713c8c73a 100644 --- a/src/suyu/uisettings.h +++ b/src/suyu/uisettings.h @@ -35,6 +35,10 @@ extern template class Setting; namespace UISettings { +/** + * Check if the theme is dark + * @returns true if the current theme contains the string "dark" in its name + */ bool IsDarkTheme(); struct ContextualShortcut { @@ -50,25 +54,16 @@ struct Shortcut { ContextualShortcut shortcut; }; -enum class Theme { - Default, - DefaultColorful, - Dark, - DarkColorful, - MidnightBlue, - MidnightBlueColorful, -}; - -static constexpr Theme default_theme{ +static constexpr std::string_view default_theme{ #ifdef _WIN32 - Theme::DarkColorful + "colorful_dark" #else - Theme::DefaultColorful + "colorful" #endif }; using Themes = std::array, 6>; -extern const Themes themes; +extern const Themes included_themes; struct GameDir { std::string path; @@ -160,7 +155,7 @@ struct Values { QStringList recent_files; Setting language{linkage, {}, "language", Category::Paths}; - std::string theme; + QString theme; // Shortcut name std::vector shortcuts;