diff --git a/src/common/fs/fs_paths.h b/src/common/fs/fs_paths.h index bcf4470892..e60311fcfe 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" // yuzu-specific files diff --git a/src/common/fs/path_util.cpp b/src/common/fs/path_util.cpp index 4f69db6f5c..ebbc3e0c7e 100644 --- a/src/common/fs/path_util.cpp +++ b/src/common/fs/path_util.cpp @@ -121,6 +121,7 @@ public: GenerateYuzuPath(YuzuPath::ConfigDir, yuzu_path_config); GenerateYuzuPath(YuzuPath::CrashDumpsDir, yuzu_path / CRASH_DUMPS_DIR); GenerateYuzuPath(YuzuPath::DumpDir, yuzu_path / DUMP_DIR); + GenerateYuzuPath(YuzuPath::IconsDir, yuzu_path / ICONS_DIR); GenerateYuzuPath(YuzuPath::KeysDir, yuzu_path / KEYS_DIR); GenerateYuzuPath(YuzuPath::LoadDir, yuzu_path / LOAD_DIR); GenerateYuzuPath(YuzuPath::LogDir, yuzu_path / LOG_DIR); @@ -130,7 +131,7 @@ public: GenerateYuzuPath(YuzuPath::SDMCDir, yuzu_path / SDMC_DIR); GenerateYuzuPath(YuzuPath::ShaderDir, yuzu_path / SHADER_DIR); GenerateYuzuPath(YuzuPath::TASDir, yuzu_path / TAS_DIR); - GenerateYuzuPath(YuzuPath::IconsDir, yuzu_path / ICONS_DIR); + GenerateYuzuPath(YuzuPath::ThemesDir, yuzu_path / THEMES_DIR); } private: diff --git a/src/common/fs/path_util.h b/src/common/fs/path_util.h index 59301e7edd..1b83a4daf9 100644 --- a/src/common/fs/path_util.h +++ b/src/common/fs/path_util.h @@ -17,6 +17,7 @@ enum class YuzuPath { 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 YuzuPath { 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/yuzu/configuration/configure_ui.cpp b/src/yuzu/configuration/configure_ui.cpp index f3c91586c4..9d99cb672c 100644 --- a/src/yuzu/configuration/configure_ui.cpp +++ b/src/yuzu/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 local_dir( + QString::fromStdString(Common::FS::GetYuzuPathString(Common::FS::YuzuPath::ThemesDir))); + for (const QString& theme_dir : + 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, 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/yuzu/configuration/qt_config.cpp b/src/yuzu/configuration/qt_config.cpp index 1051031f27..a52a6ef544 100644 --- a/src/yuzu/configuration/qt_config.cpp +++ b/src/yuzu/configuration/qt_config.cpp @@ -259,9 +259,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(); @@ -467,10 +466,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/yuzu/debugger/wait_tree.cpp b/src/yuzu/debugger/wait_tree.cpp index c05a05057a..6ec997d206 100644 --- a/src/yuzu/debugger/wait_tree.cpp +++ b/src/yuzu/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/yuzu/main.cpp b/src/yuzu/main.cpp index 13381fea83..c9964e8523 100644 --- a/src/yuzu/main.cpp +++ b/src/yuzu/main.cpp @@ -3727,7 +3727,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__ @@ -4816,9 +4816,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; @@ -4829,6 +4828,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)); @@ -4836,35 +4836,71 @@ 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; + 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 { + // New style not found in app, reading local directory + LOG_DEBUG(Frontend, "Style \"{}\" not found in app package, reading local directory", + current_theme.toStdString()); + + std::filesystem::path theme_path = + Common::FS::GetYuzuPath(Common::FS::YuzuPath::ThemesDir) / current_theme.toStdString() / + "style.qss"; + theme_uri = QString::fromStdString(theme_path.string()); + + // Try to load theme locally + if (tryLoadStylesheet(theme_uri)) { + return; + } + + // 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; @@ -4923,7 +4959,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/yuzu/main.h b/src/yuzu/main.h index aba61e3882..7a0edc7f9d 100644 --- a/src/yuzu/main.h +++ b/src/yuzu/main.h @@ -164,6 +164,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/yuzu/uisettings.cpp b/src/yuzu/uisettings.cpp index 7bb7e95af5..84050fcf28 100644 --- a/src/yuzu/uisettings.cpp +++ b/src/yuzu/uisettings.cpp @@ -22,7 +22,7 @@ namespace FS = Common::FS; namespace UISettings { -const Themes themes{{ +const Themes included_themes{{ {"Default", "default"}, {"Default Colorful", "colorful"}, {"Dark", "qdarkstyle"}, @@ -32,9 +32,8 @@ const Themes themes{{ }}; 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/yuzu/uisettings.h b/src/yuzu/uisettings.h index 03e42b9300..b8e222ed25 100644 --- a/src/yuzu/uisettings.h +++ b/src/yuzu/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;