Prechádzať zdrojové kódy

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
flodavid 2 rokov pred
rodič
commit
c889f1bd36

+ 2 - 1
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
 

+ 2 - 1
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:

+ 2 - 1
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
 };
 
 /**

+ 23 - 4
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());

+ 4 - 7
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<size_t>(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<size_t>(UISettings::default_theme)].second)));
+    WriteStringSetting(std::string("theme"), UISettings::values.theme.toStdString(),
+                       std::make_optional(std::string(UISettings::default_theme)));
 
     SaveUIGamelistValues();
     SaveUILayoutValues();

+ 2 - 3
src/suyu/debugger/wait_tree.cpp

@@ -35,9 +35,8 @@ constexpr std::array<std::array<Qt::GlobalColor, 2>, 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

+ 43 - 21
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<size_t>(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);

+ 6 - 0
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();

+ 9 - 10
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 = {};

+ 9 - 14
src/suyu/uisettings.h

@@ -35,6 +35,10 @@ extern template class Setting<unsigned long long>;
 
 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<std::pair<const char*, const char*>, 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<std::string> language{linkage, {}, "language", Category::Paths};
 
-    std::string theme;
+    QString theme;
 
     // Shortcut name <Shortcut, context>
     std::vector<Shortcut> shortcuts;