Automatic dark theme switching for Windows and Linux

- Windows dark theme uses "fusion" style, which is better suited, but has minor differences
- Improve OS theme detection
  - Linux:
    - Listen for OS color schemes changes on D-Bus
    - Read OS scheme for D-Bus. Fallback with gsettings, reading org.gnome.desktop.interface.
      First "color-scheme" key, then "gtk-theme". Finally, fallback to checking window palette
  - Windows (dark mode detection was not implemented before):
    - Force dark palette when OS uses dark mode by setting QT_QPA_PLATFORM to "windows:darkmode=2"
    - This enables to detect dark mode by checking the window palette
- Improve theming capabilites:
  - Linux uses custom palette when dark mode is detected.
    By using palette(xxx) in .qss files, there is no need to create a dark stylesheet
  - Allow themes to have stylesheet variants, dark.qss and light.qss
  - If current mode is dark, use dark icons for controller and keyboard applets
  - Add "dark" property to RendererStatusBarButton and GPUStatusBarButton, set to true when dark mode is used.
    Allows to have distinct colors for GPU API and accuracy buttons depending on dark mode or not
  - Enable all themes to have dark icon alternatives, not just "default" and "colorful"
    - If dark mode, icons are loaded from the directory "THEME-NAME_dark/icons"
  - If current mode is dark, use dark icons for controller and keyboard applets
  - Only qdarkstyle, qdarkstyle_midnight_blue, colorful_dark and
    colorful_midnight_blue used elements specific to dark themes
This commit is contained in:
flodavid 2024-02-04 04:04:47 +01:00 committed by Crimson Hawk
parent 97814d3e59
commit f01d7305c0
Signed by: Crimson-Hawk
GPG key ID: 0804DD39BB9BF5AC
8 changed files with 414 additions and 158 deletions

View file

@ -1,3 +1,14 @@
/*
* SPDX-FileCopyrightText: 2018 yuzu Emulator Project
* SPDX-FileCopyrightText: 2024 suyu Emulator Project
* SPDX-License-Identifier: GPL-2.0-or-later
*/
QWidget:item:hover {
background-color: #28668d;
color: #eff0f1;
}
QAbstractSpinBox { QAbstractSpinBox {
min-height: 19px; min-height: 19px;
} }
@ -94,21 +105,21 @@ QGroupBox#groupPlayer5Connected:checked,
QGroupBox#groupPlayer6Connected:checked, QGroupBox#groupPlayer6Connected:checked,
QGroupBox#groupPlayer7Connected:checked, QGroupBox#groupPlayer7Connected:checked,
QGroupBox#groupPlayer8Connected:checked { QGroupBox#groupPlayer8Connected:checked {
background-color: #f5f5f5; background-color: palette(window);
} }
QWidget#topControllerApplet { QWidget#topControllerApplet {
border-bottom: 1px solid #828790 border-bottom: 1px solid palette(dark)
} }
QWidget#bottomPerGameInput, QWidget#bottomPerGameInput,
QWidget#bottomControllerApplet { QWidget#bottomControllerApplet {
border-top: 1px solid #828790 border-top: 1px solid palette(dark)
} }
QWidget#topPerGameInput, QWidget#topPerGameInput,
QWidget#middleControllerApplet { QWidget#middleControllerApplet {
background-color: #fff; background-color: palette(base)
} }
QWidget#topPerGameInput QComboBox, QWidget#topPerGameInput QComboBox,
@ -345,7 +356,7 @@ QWidget#lineDialog {
QStackedWidget#bottomOSK, QStackedWidget#bottomOSK,
QWidget#contentDialog, QWidget#contentDialog,
QWidget#contentRichDialog { QWidget#contentRichDialog {
background: rgba(240, 240, 240, 1); background: palette(base);
} }
QWidget#contentDialog, QWidget#contentDialog,
@ -402,6 +413,7 @@ QWidget#inputOSK QLineEdit {
background: transparent; background: transparent;
border: none; border: none;
color: #ccc; color: #ccc;
padding: 0px;
} }
QWidget#inputBoxOSK { QWidget#inputBoxOSK {
@ -431,6 +443,27 @@ QWidget#boxOSK QLabel#label_characters_box {
color: #ccc; color: #ccc;
} }
QWidget#buttonsDialog,
QWidget#buttonsRichDialog,
QWidget#mainOSK,
QWidget#headerOSK,
QWidget#normalOSK,
QWidget#shiftOSK,
QWidget#numOSK,
QWidget#subOSK,
QWidget#inputOSK,
QWidget#inputBoxOSK,
QWidget#charactersOSK,
QWidget#charactersBoxOSK,
QWidget#legendOSK,
QWidget#legendOSK QWidget,
QWidget#legendOSKshift,
QWidget#legendOSKshift QWidget,
QWidget#legendOSKnum,
QWidget#legendOSKnum QWidget {
background: transparent;
}
QWidget#contentDialog QLabel#label_title, QWidget#contentDialog QLabel#label_title,
QWidget#contentRichDialog QLabel#label_title_rich { QWidget#contentRichDialog QLabel#label_title_rich {
color: #888; color: #888;
@ -471,8 +504,8 @@ QDialog#OverlayDialog QPushButton:pressed {
} }
QDialog#QtSoftwareKeyboardDialog QPushButton { QDialog#QtSoftwareKeyboardDialog QPushButton {
background: rgba(232, 232, 232, 1); background: palette(window);
border: 2px solid rgba(240, 240, 240, 1); border: 2px solid palette(base);
} }
QDialog#QtSoftwareKeyboardDialog QPushButton#button_shift, QDialog#QtSoftwareKeyboardDialog QPushButton#button_shift,
@ -481,27 +514,35 @@ QDialog#QtSoftwareKeyboardDialog QPushButton#button_space,
QDialog#QtSoftwareKeyboardDialog QPushButton#button_shift_shift, QDialog#QtSoftwareKeyboardDialog QPushButton#button_shift_shift,
QDialog#QtSoftwareKeyboardDialog QPushButton#button_return_shift, QDialog#QtSoftwareKeyboardDialog QPushButton#button_return_shift,
QDialog#QtSoftwareKeyboardDialog QPushButton#button_space_shift { QDialog#QtSoftwareKeyboardDialog QPushButton#button_space_shift {
background: rgba(218, 218, 218, 1); background: palette(alternate-base);
border: 2px solid rgba(240, 240, 240, 1); border: 2px solid palette(base);
} }
QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace, QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace,
QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace_shift, QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace_shift,
QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace_num { QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace_num {
color: rgba(240, 240, 240, 1); color: palette(base);
background: rgba(44, 44, 44, 1); background: palette(mid);
border: 2px solid rgba(240, 240, 240, 1); border: 2px solid palette(base);
} }
QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok, QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok,
QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok_shift, QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok_shift,
QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok_num { QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok_num {
color: rgba(240, 240, 240, 1); color: palette(base);
background: rgba(49, 79, 239, 1); background: palette(highlight);
border: 2px solid rgba(240, 240, 240, 1); border: 2px solid palette(base);
} }
QDialog#QtSoftwareKeyboardDialog QPushButton:focus, QDialog#QtSoftwareKeyboardDialog QPushButton:focus,
QDialog#QtSoftwareKeyboardDialog QPushButton:hover
{
background: palette(base);
border: 5px solid rgba(148, 250, 202, 1);
border-radius: 6px;
outline: none;
}
QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok:focus, QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok:focus,
QDialog#QtSoftwareKeyboardDialog QPushButton#button_shift:focus, QDialog#QtSoftwareKeyboardDialog QPushButton#button_shift:focus,
QDialog#QtSoftwareKeyboardDialog QPushButton#button_space:focus, QDialog#QtSoftwareKeyboardDialog QPushButton#button_space:focus,
@ -514,8 +555,6 @@ QDialog#QtSoftwareKeyboardDialog QPushButton#button_return_shift:focus,
QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace_shift:focus, QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace_shift:focus,
QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok_num:focus, QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok_num:focus,
QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace_num:focus, QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace_num:focus,
QDialog#QtSoftwareKeyboardDialog QPushButton:hover,
QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok:hover, QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok:hover,
QDialog#QtSoftwareKeyboardDialog QPushButton#button_shift:hover, QDialog#QtSoftwareKeyboardDialog QPushButton#button_shift:hover,
QDialog#QtSoftwareKeyboardDialog QPushButton#button_space:hover, QDialog#QtSoftwareKeyboardDialog QPushButton#button_space:hover,
@ -524,12 +563,11 @@ QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace:hover,
QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok_shift:hover, QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok_shift:hover,
QDialog#QtSoftwareKeyboardDialog QPushButton#button_shift_shift:hover, QDialog#QtSoftwareKeyboardDialog QPushButton#button_shift_shift:hover,
QDialog#QtSoftwareKeyboardDialog QPushButton#button_space_shift:hover, QDialog#QtSoftwareKeyboardDialog QPushButton#button_space_shift:hover,
QDialog#QtSoftwareKeyboardDialog QPushButton#button_return_shift:hover,
QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace_shift:hover, QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace_shift:hover,
QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok_num:hover, QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok_num:hover,
QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace_num:hover { QDialog#QtSoftwareKeyboardDialog QPushButton#button_return_shift:hover,
color: rgba(0, 0, 0, 1); QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace_num:hover
background: rgba(255, 255, 255, 1); {
border: 5px solid rgba(148, 250, 202, 1); border: 5px solid rgba(148, 250, 202, 1);
border-radius: 6px; border-radius: 6px;
outline: none; outline: none;
@ -548,7 +586,7 @@ QDialog#QtSoftwareKeyboardDialog QPushButton#button_return_shift:pressed,
QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace_shift:pressed, QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace_shift:pressed,
QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok_num:pressed, QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok_num:pressed,
QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace_num:pressed { QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace_num:pressed {
color: rgba(240, 240, 240, 1); color: palette(base);
background: rgba(150, 150, 150, 1); background: rgba(150, 150, 150, 1);
border: 5px solid rgba(148, 250, 202, 1); border: 5px solid rgba(148, 250, 202, 1);
border-radius: 6px; border-radius: 6px;
@ -653,8 +691,8 @@ QDialog#QtSoftwareKeyboardDialog QPushButton#button_return_shift:disabled,
QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace_shift:disabled, QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace_shift:disabled,
QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok_num:disabled, QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok_num:disabled,
QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace_num:disabled { QDialog#QtSoftwareKeyboardDialog QPushButton#button_backspace_num:disabled {
color: rgba(164, 164, 164, 1); color: palette(midlight);
background-color: rgba(218, 218, 218, 1); background-color: palette(alternate-base);
} }
QDialog#QtSoftwareKeyboardDialog QPushButton#button_at:disabled, QDialog#QtSoftwareKeyboardDialog QPushButton#button_at:disabled,
@ -671,7 +709,7 @@ QDialog#QtSoftwareKeyboardDialog QPushButton#button_8:disabled,
QDialog#QtSoftwareKeyboardDialog QPushButton#button_9:disabled, QDialog#QtSoftwareKeyboardDialog QPushButton#button_9:disabled,
QDialog#QtSoftwareKeyboardDialog QPushButton#button_0:disabled, QDialog#QtSoftwareKeyboardDialog QPushButton#button_0:disabled,
QDialog#QtSoftwareKeyboardDialog QPushButton#button_return:disabled { QDialog#QtSoftwareKeyboardDialog QPushButton#button_return:disabled {
color: rgba(164, 164, 164, 1); color: palette(midlight);
} }
QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok:disabled, QDialog#QtSoftwareKeyboardDialog QPushButton#button_ok:disabled,

View file

@ -384,10 +384,12 @@ bool QtControllerSelectorDialog::CheckIfParametersMet() {
void QtControllerSelectorDialog::SetSupportedControllers() { void QtControllerSelectorDialog::SetSupportedControllers() {
const QString theme = [] { const QString theme = [] {
if (QIcon::themeName().contains(QStringLiteral("dark"))) { if (QIcon::themeName().contains(QStringLiteral("midnight"))) {
return QStringLiteral("_dark");
} else if (QIcon::themeName().contains(QStringLiteral("midnight"))) {
return QStringLiteral("_midnight"); return QStringLiteral("_midnight");
} else if (GMainWindow::CheckDarkMode() ||
QIcon::themeName().contains(QStringLiteral("dark"))) {
// Use dark icons if current OS mode is dark, or the theme contains "dark" in its name
return QStringLiteral("_dark");
} else { } else {
return QString{}; return QString{};
} }
@ -572,10 +574,12 @@ void QtControllerSelectorDialog::UpdateControllerIcon(std::size_t player_index)
} }
const QString theme = [] { const QString theme = [] {
if (QIcon::themeName().contains(QStringLiteral("dark"))) { if (QIcon::themeName().contains(QStringLiteral("midnight"))) {
return QStringLiteral("_dark");
} else if (QIcon::themeName().contains(QStringLiteral("midnight"))) {
return QStringLiteral("_midnight"); return QStringLiteral("_midnight");
} else if (GMainWindow::CheckDarkMode() ||
QIcon::themeName().contains(QStringLiteral("dark"))) {
// Use dark icons if current OS mode is dark, or the theme contains "dark" in its name
return QStringLiteral("_dark");
} else { } else {
return QString{}; return QString{};
} }

View file

@ -823,7 +823,9 @@ void QtSoftwareKeyboardDialog::SetControllerImage() {
handheld->IsConnected() ? handheld->GetNpadStyleIndex() : player_1->GetNpadStyleIndex(); handheld->IsConnected() ? handheld->GetNpadStyleIndex() : player_1->GetNpadStyleIndex();
const QString theme = [] { const QString theme = [] {
if (QIcon::themeName().contains(QStringLiteral("dark")) || // Use dark icons if current OS mode is dark, or the theme contains "dark", or "midnight" in
// its name
if (GMainWindow::CheckDarkMode() || QIcon::themeName().contains(QStringLiteral("dark")) ||
QIcon::themeName().contains(QStringLiteral("midnight"))) { QIcon::themeName().contains(QStringLiteral("midnight"))) {
return QStringLiteral("_dark"); return QStringLiteral("_dark");
} else { } else {

View file

@ -9,7 +9,9 @@
#include <fstream> #include <fstream>
#include <iostream> #include <iostream>
#include <memory> #include <memory>
#include <QStyleFactory>
#include <thread> #include <thread>
#include "core/hle/service/am/applet_manager.h" #include "core/hle/service/am/applet_manager.h"
#include "core/loader/nca.h" #include "core/loader/nca.h"
#include "core/loader/nro.h" #include "core/loader/nro.h"
@ -20,6 +22,9 @@
#endif #endif
#ifdef __unix__ #ifdef __unix__
#include <csignal> #include <csignal>
#include <QtDBus/QDBusInterface>
#include <QtDBus/QDBusMessage>
#include <QtDBus/QtDBus>
#include <sys/socket.h> #include <sys/socket.h>
#include "common/linux/gamemode.h" #include "common/linux/gamemode.h"
#endif #endif
@ -271,18 +276,6 @@ static void OverrideWindowsFont() {
} }
#endif #endif
bool GMainWindow::CheckDarkMode() {
#ifdef __unix__
const QPalette test_palette(qApp->palette());
const QColor text_color = test_palette.color(QPalette::Active, QPalette::Text);
const QColor window_color = test_palette.color(QPalette::Active, QPalette::Window);
return (text_color.value() > window_color.value());
#else
// TODO: Windows
return false;
#endif // __unix__
}
GMainWindow::GMainWindow(std::unique_ptr<QtConfig> config_, bool has_broken_vulkan) GMainWindow::GMainWindow(std::unique_ptr<QtConfig> config_, bool has_broken_vulkan)
: ui{std::make_unique<Ui::MainWindow>()}, system{std::make_unique<Core::System>()}, : ui{std::make_unique<Ui::MainWindow>()}, system{std::make_unique<Core::System>()},
input_subsystem{std::make_shared<InputCommon::InputSubsystem>()}, config{std::move(config_)}, input_subsystem{std::make_shared<InputCommon::InputSubsystem>()}, config{std::move(config_)},
@ -303,8 +296,6 @@ GMainWindow::GMainWindow(std::unique_ptr<QtConfig> config_, bool has_broken_vulk
ui->setupUi(this); ui->setupUi(this);
statusBar()->hide(); statusBar()->hide();
// Check dark mode before a theme is loaded
os_dark_mode = CheckDarkMode();
startup_icon_theme = QIcon::themeName(); startup_icon_theme = QIcon::themeName();
// fallback can only be set once, colorful theme icons are okay on both light/dark // fallback can only be set once, colorful theme icons are okay on both light/dark
QIcon::setFallbackThemeName(QStringLiteral("colorful")); QIcon::setFallbackThemeName(QStringLiteral("colorful"));
@ -329,6 +320,7 @@ GMainWindow::GMainWindow(std::unique_ptr<QtConfig> config_, bool has_broken_vulk
SetDefaultUIGeometry(); SetDefaultUIGeometry();
RestoreUIState(); RestoreUIState();
UpdateUITheme();
ConnectMenuEvents(); ConnectMenuEvents();
ConnectWidgetEvents(); ConnectWidgetEvents();
@ -449,7 +441,10 @@ GMainWindow::GMainWindow(std::unique_ptr<QtConfig> config_, bool has_broken_vulk
SDL_EnableScreenSaver(); SDL_EnableScreenSaver();
#endif #endif
#ifdef __unix__
SetupPrepareForSleep(); SetupPrepareForSleep();
ListenColorSchemeChange();
#endif
QStringList args = QApplication::arguments(); QStringList args = QApplication::arguments();
@ -1647,8 +1642,8 @@ void GMainWindow::OnDisplayTitleBars(bool show) {
} }
} }
void GMainWindow::SetupPrepareForSleep() {
#ifdef __unix__ #ifdef __unix__
void GMainWindow::SetupPrepareForSleep() {
auto bus = QDBusConnection::systemBus(); auto bus = QDBusConnection::systemBus();
if (bus.isConnected()) { if (bus.isConnected()) {
const bool success = bus.connect( const bool success = bus.connect(
@ -1662,8 +1657,8 @@ void GMainWindow::SetupPrepareForSleep() {
} else { } else {
LOG_WARNING(Frontend, "QDBusConnection system bus is not connected"); LOG_WARNING(Frontend, "QDBusConnection system bus is not connected");
} }
#endif // __unix__
} }
#endif // __unix__
void GMainWindow::OnPrepareForSleep(bool prepare_sleep) { void GMainWindow::OnPrepareForSleep(bool prepare_sleep) {
if (emu_thread == nullptr) { if (emu_thread == nullptr) {
@ -4799,9 +4794,106 @@ void GMainWindow::filterBarSetChecked(bool state) {
emit(OnToggleFilterBar()); emit(OnToggleFilterBar());
} }
void GMainWindow::UpdateUITheme() {
LOG_DEBUG(Frontend, "Updating UI");
QString default_theme = QString::fromStdString(UISettings::default_theme.data());
QString current_theme = UISettings::values.theme;
if (current_theme.isEmpty()) {
current_theme = default_theme;
}
const bool current_dark_mode = CheckDarkMode();
UpdateIcons(current_theme);
/* Find the stylesheet to load */
if (TryLoadStylesheet(current_theme)) {
return;
}
// Reading new theme failed, loading default stylesheet
LOG_ERROR(Frontend, "Unable to open style \"{}\", fallback to the default theme",
current_theme.toStdString());
if (TryLoadStylesheet(QStringLiteral(":/%1").arg(default_theme))) {
return;
}
// Reading default failed, loading empty stylesheet
LOG_ERROR(Frontend, "Unable to set default style, stylesheet file not found");
qApp->setStyleSheet({});
setStyleSheet({});
}
void GMainWindow::UpdateIcons(const QString& theme_path) {
// Get the theme directory from its path
std::size_t last_slash = theme_path.toStdString().find_last_of("/");
QString theme_dir = QString::fromStdString(theme_path.toStdString().substr(last_slash + 1));
// Append _dark to the theme name to use dark variant icons
if (CheckDarkMode()) {
LOG_DEBUG(Frontend, "Using icons from: {}", theme_dir.toStdString() + "_dark");
QIcon::setThemeName(theme_dir + QStringLiteral("_dark"));
} else {
LOG_DEBUG(Frontend, "Using icons from: {}", theme_dir.toStdString());
QIcon::setThemeName(theme_dir);
}
const QString theme_directory{
QString::fromStdString(Common::FS::GetSuyuPathString(Common::FS::SuyuPath::ThemesDir))};
// Set path for default icons
// Use icon resources from application binary and current theme local subdirectory, if it exists
QStringList theme_paths;
theme_paths << QString::fromStdString(":/icons") << QStringLiteral("%1").arg(theme_directory);
QIcon::setThemeSearchPaths(theme_paths);
// Change current directory, to allow user themes to add their own icons
QDir::setCurrent(QStringLiteral("%1/%2").arg(theme_directory, UISettings::values.theme));
emit UpdateThemedIcons();
}
bool GMainWindow::TryLoadStylesheet(const QString& theme_uri) {
QString style_path;
// Use themed stylesheet if it exists
if (CheckDarkMode()) {
style_path = theme_uri + QStringLiteral("/dark.qss");
} else {
style_path = theme_uri + QStringLiteral("/light.qss");
}
if (!QFile::exists(style_path)) {
LOG_INFO(Frontend, "Themed (light/dark) stylesheet could not be found, using default one");
// Use common stylesheet if themed one does not exist
style_path = theme_uri + QStringLiteral("/style.qss");
}
// Loading stylesheet
QFile style_file(style_path);
if (style_file.open(QFile::ReadOnly | QFile::Text)) {
// Update the color palette before applying the stylesheet
UpdateThemePalette();
LOG_INFO(Frontend, "Loading stylesheet in: {}", theme_uri.toStdString());
QTextStream ts_theme(&style_file);
qApp->setStyleSheet(ts_theme.readAll());
setStyleSheet(ts_theme.readAll());
SetCustomStylesheet();
return true;
}
// Opening the file failed
return false;
}
bool GMainWindow::TryLoadStylesheet(const std::filesystem::path& theme_path) {
return TryLoadStylesheet(QString::fromStdString(theme_path.string() + "/"));
}
static void AdjustLinkColor() { static void AdjustLinkColor() {
QPalette new_pal(qApp->palette()); QPalette new_pal(qApp->palette());
if (UISettings::IsDarkTheme()) { if (GMainWindow::CheckDarkMode()) {
new_pal.setColor(QPalette::Link, QColor(0, 190, 255, 255)); new_pal.setColor(QPalette::Link, QColor(0, 190, 255, 255));
} else { } else {
new_pal.setColor(QPalette::Link, QColor(0, 140, 200, 255)); new_pal.setColor(QPalette::Link, QColor(0, 140, 200, 255));
@ -4811,77 +4903,201 @@ static void AdjustLinkColor() {
} }
} }
void GMainWindow::UpdateUITheme() { void GMainWindow::UpdateThemePalette() {
QString default_theme = QString::fromStdString(UISettings::default_theme.data()); QPalette themePalette(qApp->palette());
QString current_theme = UISettings::values.theme;
if (current_theme.isEmpty()) {
current_theme = default_theme;
}
#ifdef _WIN32 #ifdef _WIN32
QIcon::setThemeName(current_theme); QColor dark(25, 25, 25);
AdjustLinkColor(); QColor darkGray(100, 100, 100);
#else QColor gray(150, 150, 150);
if (current_theme == QStringLiteral("default") || current_theme == QStringLiteral("colorful")) { QColor light(230, 230, 230);
LOG_INFO(Frontend, "Theme is default or colorful: {}", current_theme.toStdString()); // By default, revert fusion style set for Windows dark theme
QIcon::setThemeName(current_theme == QStringLiteral("colorful") ? current_theme QString style;
: startup_icon_theme);
QIcon::setThemeSearchPaths(QStringList(default_theme_paths));
if (CheckDarkMode()) { if (CheckDarkMode()) {
current_theme = QStringLiteral("default_dark"); // AlternateBase is kept at rgb(233, 231, 227) or rgb(245, 245, 245) on Windows dark
// palette, fix this. Sometimes, it even is rgb(0, 0, 0), but uses a very light gray for
// alternate rows, do not know why
if (themePalette.alternateBase().color() == QColor(233, 231, 227) ||
themePalette.alternateBase().color() == QColor(245, 245, 245) ||
themePalette.alternateBase().color() == QColor(0, 0, 0)) {
themePalette.setColor(QPalette::AlternateBase, dark);
alternate_base_modified = true;
} }
// Use fusion theme, since its close to windowsvista, but works well with a dark palette
style = QStringLiteral("fusion");
} else { } else {
LOG_INFO(Frontend, "Theme is NOT default or colorful: {}", current_theme.toStdString()); // Reset AlternateBase if it has been modified
QIcon::setThemeName(current_theme); if (alternate_base_modified) {
// Use icon resources from application binary and current theme subdirectory if it exists themePalette.setColor(QPalette::AlternateBase, QColor(245, 245, 245));
QStringList theme_paths; alternate_base_modified = false;
theme_paths << QString::fromStdString(":/icons") }
<< QStringLiteral("%1/%2/icons") // Reset Windows theme to the default
.arg(QString::fromStdString( style = QStringLiteral("windowsvista");
Common::FS::GetYuzuPathString(Common::FS::YuzuPath::ThemesDir)), }
current_theme); LOG_DEBUG(Frontend, "Using style: {}", style.toStdString());
QIcon::setThemeSearchPaths(theme_paths); qApp->setStyle(style);
AdjustLinkColor(); #else
if (CheckDarkMode()) {
// 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::WindowText, Qt::white);
themePalette.setColor(QPalette::Disabled, QPalette::WindowText, QColor(127, 127, 127));
themePalette.setColor(QPalette::Base, QColor(42, 42, 42));
themePalette.setColor(QPalette::AlternateBase, QColor(66, 66, 66));
themePalette.setColor(QPalette::ToolTipBase, Qt::white);
themePalette.setColor(QPalette::ToolTipText, QColor(53, 53, 53));
themePalette.setColor(QPalette::Text, Qt::white);
themePalette.setColor(QPalette::Disabled, QPalette::Text, QColor(127, 127, 127));
themePalette.setColor(QPalette::Dark, QColor(35, 35, 35));
themePalette.setColor(QPalette::Shadow, QColor(20, 20, 20));
themePalette.setColor(QPalette::Button, QColor(53, 53, 53));
themePalette.setColor(QPalette::ButtonText, Qt::white);
themePalette.setColor(QPalette::Disabled, QPalette::ButtonText, QColor(127, 127, 127));
themePalette.setColor(QPalette::BrightText, Qt::red);
themePalette.setColor(QPalette::Link, QColor(42, 130, 218));
themePalette.setColor(QPalette::Highlight, QColor(42, 130, 218));
themePalette.setColor(QPalette::Disabled, QPalette::Highlight, QColor(80, 80, 80));
themePalette.setColor(QPalette::HighlightedText, Qt::white);
themePalette.setColor(QPalette::Disabled, QPalette::HighlightedText, QColor(127, 127, 127));
} else {
LOG_INFO(Frontend, "Using standard palette");
// Reset light palette on non Windows platforms
themePalette = this->style()->standardPalette();
} }
#endif #endif
if (current_theme != default_theme) { qApp->setPalette(themePalette);
QString theme_uri{current_theme + QStringLiteral("style.qss")}; AdjustLinkColor();
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",
current_theme.toStdString());
qApp->setStyleSheet({});
setStyleSheet({});
}
} }
bool GMainWindow::tryLoadStylesheet(const QString& theme_path) { void GMainWindow::SetCustomStylesheet() {
QFile theme_file(theme_path); setStyleSheet(QStringLiteral("QStatusBar::item { border: none; }"));
if (theme_file.open(QFile::ReadOnly | QFile::Text)) {
LOG_INFO(Frontend, "Loading style in: {}", theme_path.toStdString()); // Set "dark" qss property value, that may be used in stylesheets
QTextStream ts(&theme_file); bool is_dark_mode = CheckDarkMode();
qApp->setStyleSheet(ts.readAll()); if (renderer_status_button) {
setStyleSheet(ts.readAll()); renderer_status_button->setProperty("dark", is_dark_mode);
return true;
} }
// Opening the file failed if (gpu_accuracy_button) {
gpu_accuracy_button->setProperty("dark", is_dark_mode);
}
#ifdef _WIN32
// Windows dark mode uses "fusion" style. Make it look like more "windowsvista" light style
if (is_dark_mode) {
/* the groove expands to the size of the slider by default. by giving it a height, it has a
fixed size */
/* handle is placed by default on the contents rect of the groove. Negative margin expands
it outside the groove */
setStyleSheet(QStringLiteral("QSlider:horizontal{ height:30px; }\
QSlider::sub-page:horizontal { background-color: palette(highlight); }\
QSlider::add-page:horizontal { background-color: palette(midlight);}\
QSlider::groove:horizontal { border-width: 1px; margin: 1px 0; height: 2px;}\
QSlider::handle:horizontal { border-width: 1px; border-style: solid; border-color: palette(dark);\
width: 10px; margin: -10px 0px; }\
QSlider::handle { background-color: palette(button); }\
QSlider::handle:hover { background-color: palette(highlight); }"));
}
#endif
}
#ifdef __unix__
bool GMainWindow::ListenColorSchemeChange() {
auto bus = QDBusConnection::sessionBus();
if (bus.isConnected()) {
const QString dbus_service = QStringLiteral("org.freedesktop.portal.Desktop");
const QString dbus_path = QStringLiteral("/org/freedesktop/portal/desktop");
const QString dbus_interface = QStringLiteral("org.freedesktop.portal.Settings");
const QString dbus_method = QStringLiteral("SettingChanged");
QStringList dbus_arguments;
dbus_arguments << QStringLiteral("org.freedesktop.appearance")
<< QStringLiteral("color-scheme");
const QString dbus_signature = QStringLiteral("ssv");
LOG_INFO(Frontend, "Connected to DBus, listening for OS theme changes");
return bus.connect(dbus_service, dbus_path, dbus_interface, dbus_method, dbus_arguments,
dbus_signature, this, SLOT(UpdateUITheme()));
}
LOG_WARNING(Frontend, "Unable to connect to DBus to listen for OS theme changes");
return false; return false;
} }
#endif
bool GMainWindow::CheckDarkMode() {
const QPalette current_palette(qApp->palette());
#ifdef __unix__
QProcess process;
QStringList gdbus_arguments;
// Using the freedesktop specifications for checking dark mode
LOG_INFO(Frontend, "Retrieving theme from freedesktop color-scheme...");
gdbus_arguments << QStringLiteral("--dest=org.freedesktop.portal.Desktop")
<< QStringLiteral("--object-path /org/freedesktop/portal/desktop")
<< QStringLiteral("--method org.freedesktop.portal.Settings.Read "
"org.freedesktop.appearance color-scheme");
process.start(QStringLiteral("gdbus call --session"), gdbus_arguments);
process.waitForFinished(1000);
QByteArray dbus_output = process.readAllStandardOutput();
if (!dbus_output.isEmpty()) {
const int systemColorSchema = QString::fromUtf8(dbus_output).trimmed().right(1).toInt();
return systemColorSchema == 1;
}
// Try alternative for Gnome if the previous one failed
QStringList gsettings_arguments;
gsettings_arguments << QStringLiteral("get")
<< QStringLiteral("org.gnome.desktop.interface")
<< QStringLiteral("color-scheme");
LOG_DEBUG(Frontend, "failed, retrieving theme from gsettings color-scheme...");
process.start(QStringLiteral("gsettings"), gsettings_arguments);
process.waitForFinished(1000);
QByteArray gsettings_output = process.readAllStandardOutput();
// Try older gtk-theme method if the previous one failed
if (gsettings_output.isEmpty()) {
LOG_INFO(Frontend, "failed, retrieving theme from gtk-theme...");
gsettings_arguments.takeLast();
gsettings_arguments << QStringLiteral("gtk-theme");
process.start(QStringLiteral("gsettings"), gsettings_arguments);
process.waitForFinished(1000);
gsettings_output = process.readAllStandardOutput();
}
// Interpret gsettings value if it succeeded
if (!gsettings_output.isEmpty()) {
QString systeme_theme = QString::fromUtf8(gsettings_output);
LOG_DEBUG(Frontend, "Gsettings output: {}", systeme_theme.toStdString());
return systeme_theme.contains(QStringLiteral("dark"), Qt::CaseInsensitive);
}
LOG_DEBUG(Frontend, "failed, retrieving theme from palette");
#endif
// Use default method based on palette swap by OS.
// It is the only method on Windows with Qt 5.
// Windows needs QT_QPA_PLATFORM env variable set to windows:darkmode=2 to force palette change
return (current_palette.color(QPalette::WindowText).lightness() >
current_palette.color(QPalette::Window).lightness());
}
void GMainWindow::changeEvent(QEvent* event) {
// PaletteChange event appears to only reach so far into the GUI, explicitly asking to
// UpdateUITheme is a decent work around
if (event->type() == QEvent::PaletteChange ||
event->type() == QEvent::ApplicationPaletteChange) {
LOG_INFO(Frontend,
"Window color palette changed by event: {} (QEvent::PaletteChange is: {})",
event->type(), QEvent::PaletteChange);
const QPalette test_palette(qApp->palette());
// Keeping eye on QPalette::Window to avoid looping. QPalette::Text might be useful too
const QColor window_color = test_palette.color(QPalette::Active, QPalette::Window);
if (last_window_color != window_color) {
last_window_color = window_color;
UpdateUITheme();
}
} else QWidget::changeEvent(event);
}
void GMainWindow::LoadTranslation() { void GMainWindow::LoadTranslation() {
bool loaded; bool loaded;
@ -4935,26 +5151,6 @@ void GMainWindow::SetGamemodeEnabled(bool state) {
} }
#endif #endif
void GMainWindow::changeEvent(QEvent* event) {
#ifdef __unix__
// PaletteChange event appears to only reach so far into the GUI, explicitly asking to
// UpdateUITheme is a decent work around
if (event->type() == QEvent::PaletteChange) {
const QPalette test_palette(qApp->palette());
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);
if (last_window_color != window_color && (current_theme == QStringLiteral("default") ||
current_theme == QStringLiteral("colorful"))) {
UpdateUITheme();
}
last_window_color = window_color;
}
#endif // __unix__
QWidget::changeEvent(event);
}
Service::AM::FrontendAppletParameters GMainWindow::ApplicationAppletParameters() { Service::AM::FrontendAppletParameters GMainWindow::ApplicationAppletParameters() {
return Service::AM::FrontendAppletParameters{ return Service::AM::FrontendAppletParameters{
.applet_id = Service::AM::AppletId::Application, .applet_id = Service::AM::AppletId::Application,

View file

@ -25,9 +25,8 @@
#include "suyu/util/controller_navigation.h" #include "suyu/util/controller_navigation.h"
#ifdef __unix__ #ifdef __unix__
#include <QSocketNotifier>
#include <QVariant> #include <QVariant>
#include <QtDBus/QDBusInterface>
#include <QtDBus/QtDBus>
#endif #endif
class QtConfig; class QtConfig;
@ -165,14 +164,9 @@ 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);
static bool CheckDarkMode();
void UpdateUITheme(); void UpdateUITheme();
explicit GMainWindow(std::unique_ptr<QtConfig> config_, bool has_broken_vulkan); explicit GMainWindow(std::unique_ptr<QtConfig> config_, bool has_broken_vulkan);
~GMainWindow() override; ~GMainWindow() override;
@ -265,12 +259,44 @@ private:
void SetDefaultUIGeometry(); void SetDefaultUIGeometry();
void RestoreUIState(); void RestoreUIState();
/**
* Load the icons used by the current theme. Use dark icons if the current mode is dark
*/
void UpdateIcons(const QString& theme_used);
/**
* Set the palette used by the stylsheet for the dark/light mode selected, according to the OS
*/
void UpdateThemePalette();
/**
* Try to load a stylesheet from its URI.
* If the path starts with ":/", its embedded in the app, otherwise its in a local directory
* @returns true if the text file could be opened as read-only
*/
bool TryLoadStylesheet(const QString& theme_uri);
/**
* Try to load a stylesheet from filesystem path
* @returns true if the text file could be opened as read-only
*/
bool TryLoadStylesheet(const std::filesystem::path& theme_path);
/**
* Default customizations to the stylesheets
*/
void SetCustomStylesheet();
#ifdef __unix__
/**
* Create a signal to update the UI theme when the OS color scheme is changed
* @returns true if we could connect to dbus
*/
bool ListenColorSchemeChange();
#endif
void ConnectWidgetEvents(); void ConnectWidgetEvents();
void ConnectMenuEvents(); void ConnectMenuEvents();
void UpdateMenuState(); void UpdateMenuState();
#ifdef __unix__
void SetupPrepareForSleep(); void SetupPrepareForSleep();
#endif
void PreventOSSleep(); void PreventOSSleep();
void AllowOSSleep(); void AllowOSSleep();
@ -401,6 +427,7 @@ private slots:
void ResetWindowSize720(); void ResetWindowSize720();
void ResetWindowSize900(); void ResetWindowSize900();
void ResetWindowSize1080(); void ResetWindowSize1080();
void UpdateUITheme();
void OnAlbum(); void OnAlbum();
void OnCabinet(Service::NFP::CabinetMode mode); void OnCabinet(Service::NFP::CabinetMode mode);
void OnMiiEdit(); void OnMiiEdit();
@ -447,7 +474,7 @@ private:
void OpenURL(const QUrl& url); void OpenURL(const QUrl& url);
void LoadTranslation(); void LoadTranslation();
void OpenPerGameConfiguration(u64 title_id, const std::string& file_name); void OpenPerGameConfiguration(u64 title_id, const std::string& file_name);
bool CheckDarkMode(); bool CheckSystemArchiveDecryption();
bool CheckFirmwarePresence(); bool CheckFirmwarePresence();
void SetFirmwareVersion(); void SetFirmwareVersion();
void ConfigureFilesystemProvider(const std::string& filepath); void ConfigureFilesystemProvider(const std::string& filepath);
@ -531,7 +558,8 @@ private:
QTimer update_input_timer; QTimer update_input_timer;
QString startup_icon_theme; QString startup_icon_theme;
bool os_dark_mode = false; bool alternate_base_modified = false;
QColor last_window_color;
// FS // FS
std::shared_ptr<FileSys::VfsFilesystem> vfs; std::shared_ptr<FileSys::VfsFilesystem> vfs;

View file

@ -5,6 +5,8 @@
#ifdef _WIN32 #ifdef _WIN32
#include <cstring> #include <cstring>
#include <QByteArray>
#include <QtGlobal>
#include <processthreadsapi.h> #include <processthreadsapi.h>
#include <windows.h> #include <windows.h>
#elif defined(SUYU_UNIX) #elif defined(SUYU_UNIX)
@ -36,6 +38,9 @@ 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

@ -31,11 +31,6 @@ const Themes included_themes{{
{"Midnight Blue Colorful", ":/colorful_midnight_blue/"}, {"Midnight Blue Colorful", ":/colorful_midnight_blue/"},
}}; }};
bool IsDarkTheme() {
return UISettings::values.theme.contains(QStringLiteral("dark")) ||
UISettings::values.theme.contains(QStringLiteral("midnight"));
}
Values values = {}; Values values = {};
u32 CalculateWidth(u32 height, Settings::AspectRatio ratio) { u32 CalculateWidth(u32 height, Settings::AspectRatio ratio) {

View file

@ -35,12 +35,6 @@ 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();
struct ContextualShortcut { struct ContextualShortcut {
std::string keyseq; std::string keyseq;
std::string controller_keyseq; std::string controller_keyseq;
@ -54,13 +48,7 @@ struct Shortcut {
ContextualShortcut shortcut; ContextualShortcut shortcut;
}; };
static constexpr std::string_view default_theme{ static constexpr std::string_view default_theme{"colorful"};
#ifdef _WIN32
"colorful_dark"
#else
"colorful"
#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 included_themes; extern const Themes included_themes;