Add dark mode configuration setting in UI tab

- Allows to choose "Auto", "Always On" or "Always Off".
- If Auto is chosen, value is retrieved from OS
  - On Windows, the application needs a restart to apply the settings
  - On Windows, "Always On" is not available, the OS is responsible for
    selecting the dark palette
This commit is contained in:
flodavid 2024-02-12 04:01:25 +01:00
parent e88e93e055
commit f1e66ee0c7
9 changed files with 119 additions and 64 deletions

View File

@ -67,6 +67,7 @@ SWITCHABLE(u8, true);
// Used in UISettings // Used in UISettings
// TODO see if we can move this to uisettings.cpp // TODO see if we can move this to uisettings.cpp
SWITCHABLE(ConfirmStop, true); SWITCHABLE(ConfirmStop, true);
SWITCHABLE(DarkModeState, true);
#undef SETTING #undef SETTING
#undef SWITCHABLE #undef SWITCHABLE

View File

@ -88,6 +88,7 @@ SWITCHABLE(u8, true);
// Used in UISettings // Used in UISettings
// TODO see if we can move this to uisettings.h // TODO see if we can move this to uisettings.h
SWITCHABLE(ConfirmStop, true); SWITCHABLE(ConfirmStop, true);
SWITCHABLE(DarkModeState, true);
#undef SETTING #undef SETTING
#undef SWITCHABLE #undef SWITCHABLE

View File

@ -153,6 +153,8 @@ ENUM(ConsoleMode, Handheld, Docked);
ENUM(AppletMode, HLE, LLE); ENUM(AppletMode, HLE, LLE);
ENUM(DarkModeState, Off, On, Auto);
template <typename Type> template <typename Type>
inline std::string CanonicalizeEnum(Type id) { inline std::string CanonicalizeEnum(Type id) {
const auto group = EnumMetadata<Type>::Canonicalizations(); const auto group = EnumMetadata<Type>::Canonicalizations();

View File

@ -18,6 +18,7 @@
#include <QString> #include <QString>
#include <QToolButton> #include <QToolButton>
#include <QVariant> #include <QVariant>
#include <QtGlobal>
#include "common/common_types.h" #include "common/common_types.h"
#include "common/fs/path_util.h" #include "common/fs/path_util.h"
@ -29,6 +30,8 @@
#include "ui_configure_ui.h" #include "ui_configure_ui.h"
#include "yuzu/uisettings.h" #include "yuzu/uisettings.h"
using Settings::DarkModeState;
namespace { namespace {
constexpr std::array default_game_icon_sizes{ constexpr std::array default_game_icon_sizes{
std::make_pair(0, QT_TRANSLATE_NOOP("ConfigureUI", "None")), std::make_pair(0, QT_TRANSLATE_NOOP("ConfigureUI", "None")),
@ -131,6 +134,17 @@ ConfigureUi::ConfigureUi(Core::System& system_, QWidget* parent)
ui->theme_combobox->addItem(theme_name, theme_dir); ui->theme_combobox->addItem(theme_name, theme_dir);
} }
ui->dark_mode_combobox->addItem(tr("Auto"), QVariant::fromValue(DarkModeState::Auto));
// Windows dark mode is based on palette swap made by OS, so the "Always On" option disabled.
// We could check if the dark mode state is "On" and force a dark palette like on Linux to support
// "Always On"
#ifdef _WIN32
ui->dark_mode_label->setText(tr("Dark mode (needs restart)"));
#else
ui->dark_mode_combobox->addItem(tr("Always On"), QVariant::fromValue(DarkModeState::On));
#endif
ui->dark_mode_combobox->addItem(tr("Always Off"), QVariant::fromValue(DarkModeState::Off));
InitializeIconSizeComboBox(); InitializeIconSizeComboBox();
InitializeRowComboBoxes(); InitializeRowComboBoxes();
@ -185,6 +199,8 @@ ConfigureUi::~ConfigureUi() = default;
void ConfigureUi::ApplyConfiguration() { void ConfigureUi::ApplyConfiguration() {
UISettings::values.theme = UISettings::values.theme =
ui->theme_combobox->itemData(ui->theme_combobox->currentIndex()).toString(); ui->theme_combobox->itemData(ui->theme_combobox->currentIndex()).toString();
UISettings::values.dark_mode_state =
static_cast<DarkModeState>(ui->dark_mode_combobox->currentData().toUInt());
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();
@ -212,6 +228,8 @@ void ConfigureUi::RequestGameListUpdate() {
void ConfigureUi::SetConfiguration() { void ConfigureUi::SetConfiguration() {
ui->theme_combobox->setCurrentIndex(ui->theme_combobox->findData(UISettings::values.theme)); ui->theme_combobox->setCurrentIndex(ui->theme_combobox->findData(UISettings::values.theme));
ui->dark_mode_combobox->setCurrentIndex(
ui->dark_mode_combobox->findData(QVariant::fromValue(UISettings::values.dark_mode_state)));
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

@ -63,6 +63,20 @@
</item> </item>
</layout> </layout>
</item> </item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_6">
<item>
<widget class="QLabel" name="dark_mode_label">
<property name="text">
<string>Dark Mode:</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="dark_mode_combobox"/>
</item>
</layout>
</item>
</layout> </layout>
</item> </item>
</layout> </layout>

View File

@ -259,8 +259,10 @@ void QtConfig::ReadShortcutValues() {
void QtConfig::ReadUIValues() { void QtConfig::ReadUIValues() {
BeginGroup(Settings::TranslateCategory(Settings::Category::Ui)); BeginGroup(Settings::TranslateCategory(Settings::Category::Ui));
UISettings::values.theme = QString::fromStdString( UISettings::values.theme =
ReadStringSetting(std::string("theme"), std::string(UISettings::default_theme))); QString::fromStdString(ReadStringSetting("theme", std::string(UISettings::default_theme)));
UISettings::values.dark_mode_state = static_cast<DarkModeState>(
ReadIntegerSetting("dark_mode_state", static_cast<int>(DarkModeState::Auto)));
ReadUIGamelistValues(); ReadUIGamelistValues();
ReadUILayoutValues(); ReadUILayoutValues();
@ -466,8 +468,10 @@ void QtConfig::SaveUIValues() {
WriteCategory(Settings::Category::Ui); WriteCategory(Settings::Category::Ui);
WriteCategory(Settings::Category::UiGeneral); WriteCategory(Settings::Category::UiGeneral);
WriteStringSetting(std::string("theme"), UISettings::values.theme.toStdString(), WriteStringSetting("theme", UISettings::values.theme.toStdString(),
std::make_optional(std::string(UISettings::default_theme))); std::make_optional(std::string(UISettings::default_theme)));
WriteIntegerSetting("dark_mode_state", static_cast<int>(UISettings::values.dark_mode_state),
std::make_optional(static_cast<int>(DarkModeState::Auto)));
SaveUIGamelistValues(); SaveUIGamelistValues();
SaveUILayoutValues(); SaveUILayoutValues();

View File

@ -3721,6 +3721,7 @@ void GMainWindow::ResetWindowSize1080() {
void GMainWindow::OnConfigure() { void GMainWindow::OnConfigure() {
const QString old_theme = UISettings::values.theme; const QString old_theme = UISettings::values.theme;
DarkModeState old_dark_mode_state = UISettings::values.dark_mode_state;
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__
@ -3781,7 +3782,8 @@ void GMainWindow::OnConfigure() {
} }
InitializeHotkeys(); InitializeHotkeys();
if (UISettings::values.theme != old_theme) { if (UISettings::values.theme != old_theme ||
UISettings::values.dark_mode_state != old_dark_mode_state) {
UpdateUITheme(); UpdateUITheme();
} }
if (UISettings::values.enable_discord_presence.GetValue() != old_discord_presence) { if (UISettings::values.enable_discord_presence.GetValue() != old_discord_presence) {
@ -4795,13 +4797,11 @@ void GMainWindow::filterBarSetChecked(bool state) {
} }
void GMainWindow::UpdateUITheme() { void GMainWindow::UpdateUITheme() {
LOG_DEBUG(Frontend, "Updating UI"); const QString default_theme = QString::fromStdString(UISettings::default_theme.data());
QString default_theme = QString::fromStdString(UISettings::default_theme.data());
QString current_theme = UISettings::values.theme; QString current_theme = UISettings::values.theme;
if (current_theme.isEmpty()) { if (current_theme.isEmpty()) {
current_theme = default_theme; current_theme = default_theme;
} }
const bool current_dark_mode = CheckDarkMode();
UpdateIcons(current_theme); UpdateIcons(current_theme);
@ -4873,7 +4873,7 @@ bool GMainWindow::TryLoadStylesheet(const QString& theme_uri) {
style_path = theme_uri + QStringLiteral("light.qss"); style_path = theme_uri + QStringLiteral("light.qss");
} }
if (!QFile::exists(style_path)) { if (!QFile::exists(style_path)) {
LOG_INFO(Frontend, "Themed (light/dark) stylesheet could not be found, using default one"); LOG_DEBUG(Frontend, "No themed (light/dark) stylesheet, using default one");
// Use common stylesheet if themed one does not exist // Use common stylesheet if themed one does not exist
style_path = theme_uri + QStringLiteral("style.qss"); style_path = theme_uri + QStringLiteral("style.qss");
} }
@ -4884,7 +4884,7 @@ bool GMainWindow::TryLoadStylesheet(const QString& theme_uri) {
// Update the color palette before applying the stylesheet // Update the color palette before applying the stylesheet
UpdateThemePalette(); UpdateThemePalette();
LOG_INFO(Frontend, "Loading stylesheet in: {}", theme_uri.toStdString()); LOG_DEBUG(Frontend, "Loading stylesheet in: {}", theme_uri.toStdString());
QTextStream ts_theme(&style_file); QTextStream ts_theme(&style_file);
qApp->setStyleSheet(ts_theme.readAll()); qApp->setStyleSheet(ts_theme.readAll());
setStyleSheet(ts_theme.readAll()); setStyleSheet(ts_theme.readAll());
@ -4947,7 +4947,6 @@ void GMainWindow::UpdateThemePalette() {
#else #else
if (CheckDarkMode()) { if (CheckDarkMode()) {
// Set Dark palette on non Windows platforms (that may not have a dark palette) // Set Dark palette on non Windows platforms (that may not have a dark palette)
LOG_INFO(Frontend, "Using custom dark palette");
themePalette.setColor(QPalette::Window, QColor(53, 53, 53)); themePalette.setColor(QPalette::Window, QColor(53, 53, 53));
themePalette.setColor(QPalette::WindowText, Qt::white); themePalette.setColor(QPalette::WindowText, Qt::white);
themePalette.setColor(QPalette::Disabled, QPalette::WindowText, QColor(127, 127, 127)); themePalette.setColor(QPalette::Disabled, QPalette::WindowText, QColor(127, 127, 127));
@ -4969,7 +4968,6 @@ void GMainWindow::UpdateThemePalette() {
themePalette.setColor(QPalette::HighlightedText, Qt::white); themePalette.setColor(QPalette::HighlightedText, Qt::white);
themePalette.setColor(QPalette::Disabled, QPalette::HighlightedText, QColor(127, 127, 127)); themePalette.setColor(QPalette::Disabled, QPalette::HighlightedText, QColor(127, 127, 127));
} else { } else {
LOG_INFO(Frontend, "Using standard palette");
// Reset light palette on non Windows platforms // Reset light palette on non Windows platforms
themePalette = this->style()->standardPalette(); themePalette = this->style()->standardPalette();
} }
@ -5031,13 +5029,23 @@ bool GMainWindow::ListenColorSchemeChange() {
#endif #endif
bool GMainWindow::CheckDarkMode() { bool GMainWindow::CheckDarkMode() {
bool is_dark_mode_auto;
#ifdef _WIN32
// Dark mode cannot be changed after the app started on Windows
is_dark_mode_auto = qgetenv("QT_QPA_PLATFORM").contains("darkmode=2");
#else
is_dark_mode_auto = UISettings::values.dark_mode_state == DarkModeState::Auto;
#endif
if (!is_dark_mode_auto) {
return UISettings::values.dark_mode_state == DarkModeState::On;
} else {
const QPalette current_palette(qApp->palette()); const QPalette current_palette(qApp->palette());
#ifdef __unix__ #ifdef __unix__
QProcess process; QProcess process;
QStringList gdbus_arguments;
// Using the freedesktop specifications for checking dark mode // Using the freedesktop specifications for checking dark mode
LOG_INFO(Frontend, "Retrieving theme from freedesktop color-scheme..."); LOG_DEBUG(Frontend, "Retrieving theme from freedesktop color-scheme...");
QStringList gdbus_arguments;
gdbus_arguments << QStringLiteral("--dest=org.freedesktop.portal.Desktop") gdbus_arguments << QStringLiteral("--dest=org.freedesktop.portal.Desktop")
<< QStringLiteral("--object-path /org/freedesktop/portal/desktop") << QStringLiteral("--object-path /org/freedesktop/portal/desktop")
<< QStringLiteral("--method org.freedesktop.portal.Settings.Read") << QStringLiteral("--method org.freedesktop.portal.Settings.Read")
@ -5064,7 +5072,7 @@ bool GMainWindow::CheckDarkMode() {
// Try older gtk-theme method if the previous one failed // Try older gtk-theme method if the previous one failed
if (gsettings_output.isEmpty()) { if (gsettings_output.isEmpty()) {
LOG_INFO(Frontend, "failed, retrieving theme from gtk-theme..."); LOG_DEBUG(Frontend, "failed, retrieving theme from gtk-theme...");
gsettings_arguments.takeLast(); gsettings_arguments.takeLast();
gsettings_arguments << QStringLiteral("gtk-theme"); gsettings_arguments << QStringLiteral("gtk-theme");
@ -5081,19 +5089,20 @@ bool GMainWindow::CheckDarkMode() {
} }
LOG_DEBUG(Frontend, "failed, retrieving theme from palette"); LOG_DEBUG(Frontend, "failed, retrieving theme from palette");
#endif #endif
// Use default method based on palette swap by OS. // Use default method based on palette swap by OS. It is the only method on Windows with
// It is the only method on Windows with Qt 5. // Qt 5. Windows needs QT_QPA_PLATFORM env variable set to windows:darkmode=2 to force
// Windows needs QT_QPA_PLATFORM env variable set to windows:darkmode=2 to force palette change // palette change
return (current_palette.color(QPalette::WindowText).lightness() > return (current_palette.color(QPalette::WindowText).lightness() >
current_palette.color(QPalette::Window).lightness()); current_palette.color(QPalette::Window).lightness());
} }
}
void GMainWindow::changeEvent(QEvent* event) { void GMainWindow::changeEvent(QEvent* event) {
// PaletteChange event appears to only reach so far into the GUI, explicitly asking to // PaletteChange event appears to only reach so far into the GUI, explicitly asking to
// UpdateUITheme is a decent work around // UpdateUITheme is a decent work around
if (event->type() == QEvent::PaletteChange || if (event->type() == QEvent::PaletteChange ||
event->type() == QEvent::ApplicationPaletteChange) { event->type() == QEvent::ApplicationPaletteChange) {
LOG_INFO(Frontend, LOG_DEBUG(Frontend,
"Window color palette changed by event: {} (QEvent::PaletteChange is: {})", "Window color palette changed by event: {} (QEvent::PaletteChange is: {})",
event->type(), QEvent::PaletteChange); event->type(), QEvent::PaletteChange);
const QPalette test_palette(qApp->palette()); const QPalette test_palette(qApp->palette());
@ -5285,6 +5294,13 @@ int main(int argc, char* argv[]) {
QCoreApplication::setApplicationName(QStringLiteral("yuzu")); QCoreApplication::setApplicationName(QStringLiteral("yuzu"));
#ifdef _WIN32 #ifdef _WIN32
QByteArray current_qt_qpa = qgetenv("QT_QPA_PLATFORM");
// Follow dark mode setting, if the "-platform" launch option is not set
if (UISettings::values.dark_mode_state == DarkModeState::Auto && current_qt_qpa.isEmpty()) {
// When setting is Auto, force adapting window decoration and stylesheet palette to use
// Windows theme. Default is darkmode:0, which always uses light palette
qputenv("QT_QPA_PLATFORM", QByteArray("windows:darkmode=2"));
}
// Increases the maximum open file limit to 8192 // Increases the maximum open file limit to 8192
_setmaxstdio(8192); _setmaxstdio(8192);
#endif #endif

View File

@ -6,7 +6,6 @@
#ifdef _WIN32 #ifdef _WIN32
#include <cstring> #include <cstring>
#include <QByteArray> #include <QByteArray>
#include <QtGlobal>
#include <processthreadsapi.h> #include <processthreadsapi.h>
#include <windows.h> #include <windows.h>
#elif defined(YUZU_UNIX) #elif defined(YUZU_UNIX)
@ -38,9 +37,6 @@ void CheckVulkan() {
bool CheckEnvVars(bool* is_child) { bool CheckEnvVars(bool* is_child) {
#ifdef _WIN32 #ifdef _WIN32
// Force adapting theme to follow Windows dark mode
qputenv("QT_QPA_PLATFORM", QByteArray("windows:darkmode=2"));
// Check environment variable to see if we are the child // Check environment variable to see if we are the child
char variable_contents[8]; char variable_contents[8];
const DWORD startup_check_var = const DWORD startup_check_var =

View File

@ -18,6 +18,7 @@
using Settings::Category; using Settings::Category;
using Settings::ConfirmStop; using Settings::ConfirmStop;
using Settings::DarkModeState;
using Settings::Setting; using Settings::Setting;
using Settings::SwitchableSetting; using Settings::SwitchableSetting;
@ -144,6 +145,7 @@ struct Values {
Setting<std::string> language{linkage, {}, "language", Category::Paths}; Setting<std::string> language{linkage, {}, "language", Category::Paths};
QString theme; QString theme;
DarkModeState dark_mode_state;
// Shortcut name <Shortcut, context> // Shortcut name <Shortcut, context>
std::vector<Shortcut> shortcuts; std::vector<Shortcut> shortcuts;
@ -260,3 +262,4 @@ Q_DECLARE_METATYPE(Settings::RendererBackend);
Q_DECLARE_METATYPE(Settings::ShaderBackend); Q_DECLARE_METATYPE(Settings::ShaderBackend);
Q_DECLARE_METATYPE(Settings::AstcRecompression); Q_DECLARE_METATYPE(Settings::AstcRecompression);
Q_DECLARE_METATYPE(Settings::AstcDecodeMode); Q_DECLARE_METATYPE(Settings::AstcDecodeMode);
Q_DECLARE_METATYPE(Settings::DarkModeState);