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
This commit is contained in:
flodavid 2024-01-18 20:51:39 +00:00
parent 2ff45cd0da
commit ff04d62d1f
10 changed files with 109 additions and 55 deletions

View File

@ -15,6 +15,7 @@
#define CONFIG_DIR "config" #define CONFIG_DIR "config"
#define CRASH_DUMPS_DIR "crash_dumps" #define CRASH_DUMPS_DIR "crash_dumps"
#define DUMP_DIR "dump" #define DUMP_DIR "dump"
#define ICONS_DIR "icons"
#define KEYS_DIR "keys" #define KEYS_DIR "keys"
#define LOAD_DIR "load" #define LOAD_DIR "load"
#define LOG_DIR "log" #define LOG_DIR "log"
@ -24,7 +25,7 @@
#define SDMC_DIR "sdmc" #define SDMC_DIR "sdmc"
#define SHADER_DIR "shader" #define SHADER_DIR "shader"
#define TAS_DIR "tas" #define TAS_DIR "tas"
#define ICONS_DIR "icons" #define THEMES_DIR "qt_themes"
// yuzu-specific files // yuzu-specific files

View File

@ -121,6 +121,7 @@ public:
GenerateYuzuPath(YuzuPath::ConfigDir, yuzu_path_config); GenerateYuzuPath(YuzuPath::ConfigDir, yuzu_path_config);
GenerateYuzuPath(YuzuPath::CrashDumpsDir, yuzu_path / CRASH_DUMPS_DIR); GenerateYuzuPath(YuzuPath::CrashDumpsDir, yuzu_path / CRASH_DUMPS_DIR);
GenerateYuzuPath(YuzuPath::DumpDir, yuzu_path / DUMP_DIR); GenerateYuzuPath(YuzuPath::DumpDir, yuzu_path / DUMP_DIR);
GenerateYuzuPath(YuzuPath::IconsDir, yuzu_path / ICONS_DIR);
GenerateYuzuPath(YuzuPath::KeysDir, yuzu_path / KEYS_DIR); GenerateYuzuPath(YuzuPath::KeysDir, yuzu_path / KEYS_DIR);
GenerateYuzuPath(YuzuPath::LoadDir, yuzu_path / LOAD_DIR); GenerateYuzuPath(YuzuPath::LoadDir, yuzu_path / LOAD_DIR);
GenerateYuzuPath(YuzuPath::LogDir, yuzu_path / LOG_DIR); GenerateYuzuPath(YuzuPath::LogDir, yuzu_path / LOG_DIR);
@ -130,7 +131,7 @@ public:
GenerateYuzuPath(YuzuPath::SDMCDir, yuzu_path / SDMC_DIR); GenerateYuzuPath(YuzuPath::SDMCDir, yuzu_path / SDMC_DIR);
GenerateYuzuPath(YuzuPath::ShaderDir, yuzu_path / SHADER_DIR); GenerateYuzuPath(YuzuPath::ShaderDir, yuzu_path / SHADER_DIR);
GenerateYuzuPath(YuzuPath::TASDir, yuzu_path / TAS_DIR); GenerateYuzuPath(YuzuPath::TASDir, yuzu_path / TAS_DIR);
GenerateYuzuPath(YuzuPath::IconsDir, yuzu_path / ICONS_DIR); GenerateYuzuPath(YuzuPath::ThemesDir, yuzu_path / THEMES_DIR);
} }
private: private:

View File

@ -17,6 +17,7 @@ enum class YuzuPath {
ConfigDir, // Where config files are stored. ConfigDir, // Where config files are stored.
CrashDumpsDir, // Where crash dumps are stored. CrashDumpsDir, // Where crash dumps are stored.
DumpDir, // Where dumped data is stored. DumpDir, // Where dumped data is stored.
IconsDir, // Where Icons for Windows shortcuts are stored.
KeysDir, // Where key files are stored. KeysDir, // Where key files are stored.
LoadDir, // Where cheat/mod files are stored. LoadDir, // Where cheat/mod files are stored.
LogDir, // Where log files are stored. LogDir, // Where log files are stored.
@ -26,7 +27,7 @@ enum class YuzuPath {
SDMCDir, // Where the emulated SDMC is stored. SDMCDir, // Where the emulated SDMC is stored.
ShaderDir, // Where shaders are stored. ShaderDir, // Where shaders are stored.
TASDir, // Where TAS scripts are stored. TASDir, // Where TAS scripts are stored.
IconsDir, // Where Icons for Windows shortcuts are stored. ThemesDir, // Where users should put their custom themes
}; };
/** /**

View File

@ -106,11 +106,31 @@ ConfigureUi::ConfigureUi(Core::System& system_, QWidget* parent)
InitializeLanguageComboBox(); InitializeLanguageComboBox();
for (const auto& theme : UISettings::themes) { for (const auto& theme : UISettings::included_themes) {
ui->theme_combobox->addItem(QString::fromUtf8(theme.first), ui->theme_combobox->addItem(QString::fromUtf8(theme.first),
QString::fromUtf8(theme.second)); 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(); InitializeIconSizeComboBox();
InitializeRowComboBoxes(); InitializeRowComboBoxes();
@ -164,7 +184,7 @@ ConfigureUi::~ConfigureUi() = default;
void ConfigureUi::ApplyConfiguration() { void ConfigureUi::ApplyConfiguration() {
UISettings::values.theme = 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_add_ons = ui->show_add_ons->isChecked();
UISettings::values.show_compat = ui->show_compat->isChecked(); UISettings::values.show_compat = ui->show_compat->isChecked();
UISettings::values.show_size = ui->show_size->isChecked(); UISettings::values.show_size = ui->show_size->isChecked();
@ -191,8 +211,7 @@ void ConfigureUi::RequestGameListUpdate() {
} }
void ConfigureUi::SetConfiguration() { void ConfigureUi::SetConfiguration() {
ui->theme_combobox->setCurrentIndex( ui->theme_combobox->setCurrentIndex(ui->theme_combobox->findData(UISettings::values.theme));
ui->theme_combobox->findData(QString::fromStdString(UISettings::values.theme)));
ui->language_combobox->setCurrentIndex(ui->language_combobox->findData( ui->language_combobox->setCurrentIndex(ui->language_combobox->findData(
QString::fromStdString(UISettings::values.language.GetValue()))); QString::fromStdString(UISettings::values.language.GetValue())));
ui->show_add_ons->setChecked(UISettings::values.show_add_ons.GetValue()); ui->show_add_ons->setChecked(UISettings::values.show_add_ons.GetValue());

View File

@ -259,9 +259,8 @@ void QtConfig::ReadShortcutValues() {
void QtConfig::ReadUIValues() { void QtConfig::ReadUIValues() {
BeginGroup(Settings::TranslateCategory(Settings::Category::Ui)); BeginGroup(Settings::TranslateCategory(Settings::Category::Ui));
UISettings::values.theme = ReadStringSetting( UISettings::values.theme = QString::fromStdString(
std::string("theme"), ReadStringSetting(std::string("theme"), std::string(UISettings::default_theme)));
std::string(UISettings::themes[static_cast<size_t>(UISettings::default_theme)].second));
ReadUIGamelistValues(); ReadUIGamelistValues();
ReadUILayoutValues(); ReadUILayoutValues();
@ -467,10 +466,8 @@ void QtConfig::SaveUIValues() {
WriteCategory(Settings::Category::Ui); WriteCategory(Settings::Category::Ui);
WriteCategory(Settings::Category::UiGeneral); WriteCategory(Settings::Category::UiGeneral);
WriteStringSetting( WriteStringSetting(std::string("theme"), UISettings::values.theme.toStdString(),
std::string("theme"), UISettings::values.theme, std::make_optional(std::string(UISettings::default_theme)));
std::make_optional(std::string(
UISettings::themes[static_cast<size_t>(UISettings::default_theme)].second)));
SaveUIGamelistValues(); SaveUIGamelistValues();
SaveUILayoutValues(); SaveUILayoutValues();

View File

@ -35,9 +35,8 @@ constexpr std::array<std::array<Qt::GlobalColor, 2>, 10> WaitTreeColors{{
}}; }};
bool IsDarkTheme() { bool IsDarkTheme() {
const auto& theme = UISettings::values.theme; return UISettings::values.theme.contains(QStringLiteral("dark")) ||
return theme == std::string("qdarkstyle") || theme == std::string("qdarkstyle_midnight_blue") || UISettings::values.theme.contains(QStringLiteral("midnight"));
theme == std::string("colorful_dark") || theme == std::string("colorful_midnight_blue");
} }
} // namespace } // namespace

View File

@ -3727,7 +3727,7 @@ void GMainWindow::ResetWindowSize1080() {
} }
void GMainWindow::OnConfigure() { 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 bool old_discord_presence = UISettings::values.enable_discord_presence.GetValue();
const auto old_language_index = Settings::values.language_index.GetValue(); const auto old_language_index = Settings::values.language_index.GetValue();
#ifdef __unix__ #ifdef __unix__
@ -4816,9 +4816,8 @@ static void AdjustLinkColor() {
} }
void GMainWindow::UpdateUITheme() { void GMainWindow::UpdateUITheme() {
const QString default_theme = QString::fromUtf8( QString default_theme = QString::fromStdString(UISettings::default_theme.data());
UISettings::themes[static_cast<size_t>(UISettings::default_theme)].second); QString current_theme = UISettings::values.theme;
QString current_theme = QString::fromStdString(UISettings::values.theme);
if (current_theme.isEmpty()) { if (current_theme.isEmpty()) {
current_theme = default_theme; current_theme = default_theme;
@ -4829,6 +4828,7 @@ void GMainWindow::UpdateUITheme() {
AdjustLinkColor(); AdjustLinkColor();
#else #else
if (current_theme == QStringLiteral("default") || current_theme == QStringLiteral("colorful")) { 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 QIcon::setThemeName(current_theme == QStringLiteral("colorful") ? current_theme
: startup_icon_theme); : startup_icon_theme);
QIcon::setThemeSearchPaths(QStringList(default_theme_paths)); QIcon::setThemeSearchPaths(QStringList(default_theme_paths));
@ -4836,35 +4836,71 @@ void GMainWindow::UpdateUITheme() {
current_theme = QStringLiteral("default_dark"); current_theme = QStringLiteral("default_dark");
} }
} else { } else {
LOG_INFO(Frontend, "Theme is NOT default or colorful: {}", current_theme.toStdString());
QIcon::setThemeName(current_theme); 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(); AdjustLinkColor();
} }
#endif #endif
if (current_theme != default_theme) { if (current_theme != default_theme) {
QString theme_uri{QStringLiteral(":%1/style.qss").arg(current_theme)}; QString theme_uri{QStringLiteral(":%1/style.qss").arg(current_theme)};
QFile f(theme_uri); if (tryLoadStylesheet(theme_uri)) {
if (!f.open(QFile::ReadOnly | QFile::Text)) { return;
LOG_ERROR(Frontend, "Unable to open style \"{}\", fallback to the default theme",
UISettings::values.theme);
current_theme = default_theme;
}
} }
QString theme_uri{QStringLiteral(":%1/style.qss").arg(current_theme)}; // New style not found in app, reading local directory
QFile f(theme_uri); LOG_DEBUG(Frontend, "Style \"{}\" not found in app package, reading local directory",
if (f.open(QFile::ReadOnly | QFile::Text)) { current_theme.toStdString());
QTextStream ts(&f);
qApp->setStyleSheet(ts.readAll()); std::filesystem::path theme_path =
setStyleSheet(ts.readAll()); Common::FS::GetYuzuPath(Common::FS::YuzuPath::ThemesDir) / current_theme.toStdString() /
} else { "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", LOG_ERROR(Frontend, "Unable to set style \"{}\", stylesheet file not found",
UISettings::values.theme); current_theme.toStdString());
qApp->setStyleSheet({}); qApp->setStyleSheet({});
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() { void GMainWindow::LoadTranslation() {
bool loaded; bool loaded;
@ -4923,7 +4959,7 @@ void GMainWindow::changeEvent(QEvent* event) {
// UpdateUITheme is a decent work around // UpdateUITheme is a decent work around
if (event->type() == QEvent::PaletteChange) { if (event->type() == QEvent::PaletteChange) {
const QPalette test_palette(qApp->palette()); 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 // Keeping eye on QPalette::Window to avoid looping. QPalette::Text might be useful too
static QColor last_window_color; static QColor last_window_color;
const QColor window_color = test_palette.color(QPalette::Active, QPalette::Window); const QColor window_color = test_palette.color(QPalette::Active, QPalette::Window);

View File

@ -164,6 +164,12 @@ class GMainWindow : public QMainWindow {
CREATE_SHORTCUT_MSGBOX_APPVOLATILE_WARNING, 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: public:
void filterBarSetChecked(bool state); void filterBarSetChecked(bool state);
void UpdateUITheme(); void UpdateUITheme();

View File

@ -22,7 +22,7 @@ namespace FS = Common::FS;
namespace UISettings { namespace UISettings {
const Themes themes{{ const Themes included_themes{{
{"Default", "default"}, {"Default", "default"},
{"Default Colorful", "colorful"}, {"Default Colorful", "colorful"},
{"Dark", "qdarkstyle"}, {"Dark", "qdarkstyle"},
@ -32,9 +32,8 @@ const Themes themes{{
}}; }};
bool IsDarkTheme() { bool IsDarkTheme() {
const auto& theme = UISettings::values.theme; return UISettings::values.theme.contains(QStringLiteral("dark")) ||
return theme == std::string("qdarkstyle") || theme == std::string("qdarkstyle_midnight_blue") || UISettings::values.theme.contains(QStringLiteral("midnight"));
theme == std::string("colorful_dark") || theme == std::string("colorful_midnight_blue");
} }
Values values = {}; Values values = {};

View File

@ -35,6 +35,10 @@ extern template class Setting<unsigned long long>;
namespace UISettings { namespace UISettings {
/**
* Check if the theme is dark
* @returns true if the current theme contains the string "dark" in its name
*/
bool IsDarkTheme(); bool IsDarkTheme();
struct ContextualShortcut { struct ContextualShortcut {
@ -50,25 +54,16 @@ struct Shortcut {
ContextualShortcut shortcut; ContextualShortcut shortcut;
}; };
enum class Theme { static constexpr std::string_view default_theme{
Default,
DefaultColorful,
Dark,
DarkColorful,
MidnightBlue,
MidnightBlueColorful,
};
static constexpr Theme default_theme{
#ifdef _WIN32 #ifdef _WIN32
Theme::DarkColorful "colorful_dark"
#else #else
Theme::DefaultColorful "colorful"
#endif #endif
}; };
using Themes = std::array<std::pair<const char*, const char*>, 6>; using Themes = std::array<std::pair<const char*, const char*>, 6>;
extern const Themes themes; extern const Themes included_themes;
struct GameDir { struct GameDir {
std::string path; std::string path;
@ -160,7 +155,7 @@ struct Values {
QStringList recent_files; QStringList recent_files;
Setting<std::string> language{linkage, {}, "language", Category::Paths}; Setting<std::string> language{linkage, {}, "language", Category::Paths};
std::string theme; QString theme;
// Shortcut name <Shortcut, context> // Shortcut name <Shortcut, context>
std::vector<Shortcut> shortcuts; std::vector<Shortcut> shortcuts;