diff --git a/files.cmake b/files.cmake index 65ec27a..4b26f63 100644 --- a/files.cmake +++ b/files.cmake @@ -48,6 +48,8 @@ set(sources src/SpeechBubble.h src/TalkStateManager.cpp src/TalkStateManager.h + src/Theme.cpp + src/Theme.h src/ts3log.cpp src/ts3log.h src/UpdateChecker.cpp diff --git a/src/ConfigModel.cpp b/src/ConfigModel.cpp index f84dac8..226d8bb 100644 --- a/src/ConfigModel.cpp +++ b/src/ConfigModel.cpp @@ -12,6 +12,7 @@ #include #include "ConfigModel.h" +#include "Theme.h" #include "main.h" #include "buildinfo.h" #include "plugin.h" @@ -37,6 +38,7 @@ ConfigModel::ConfigModel() m_activeConfig = 0; m_nextUpdateCheck = 0; + m_themeMode = ThemeMode::System; } @@ -75,6 +77,7 @@ void ConfigModel::readConfig(const QString& file) m_showHotkeysOnButtons = settings.value("show_hotkeys_on_buttons", false).toBool(); m_hotkeysEnabled = settings.value("hotkeys_enabled", true).toBool(); m_nextUpdateCheck = settings.value("next_update_check", 0).toUInt(); + m_themeMode = themeModeFromString(settings.value("theme_mode", "system").toString().toUtf8().constData()); notifyAllEvents(); } @@ -102,6 +105,7 @@ void ConfigModel::writeConfig(const QString& file) settings.setValue("show_hotkeys_on_buttons", m_showHotkeysOnButtons); settings.setValue("hotkeys_enabled", m_hotkeysEnabled); settings.setValue("next_update_check", m_nextUpdateCheck); + settings.setValue("theme_mode", themeModeToString(m_themeMode)); for (int i = 0; i < NUM_CONFIGS; i++) writeConfiguration(settings, i == 0 ? QString("files") : QString("files%1").arg(i + 1), m_sounds[i]); @@ -378,6 +382,7 @@ void ConfigModel::notifyAllEvents() notify(NOTIFY_SET_BUBBLE_COLS_BUILD, m_bubbleColsBuild); notify(NOTIFY_SET_SHOW_HOTKEYS_ON_BUTTONS, m_showHotkeysOnButtons); notify(NOTIFY_SET_HOTKEYS_ENABLED, m_hotkeysEnabled); + notify(NOTIFY_SET_THEME_MODE, static_cast(m_themeMode)); } @@ -387,3 +392,11 @@ void ConfigModel::setShowHotkeysOnButtons(bool show) writeConfig(); notify(NOTIFY_SET_SHOW_HOTKEYS_ON_BUTTONS, show ? 1 : 0); } + + +void ConfigModel::setThemeMode(ThemeMode mode) +{ + m_themeMode = mode; + writeConfig(); + notify(NOTIFY_SET_THEME_MODE, static_cast(mode)); +} diff --git a/src/ConfigModel.h b/src/ConfigModel.h index 13c990f..1dc9409 100644 --- a/src/ConfigModel.h +++ b/src/ConfigModel.h @@ -12,6 +12,7 @@ #include #include #include "SoundInfo.h" +#include "Theme.h" #include #include #include @@ -36,6 +37,7 @@ class ConfigModel NOTIFY_SET_SHOW_HOTKEYS_ON_BUTTONS, NOTIFY_SET_HOTKEYS_ENABLED, NOTIFY_SET_NEXT_UPDATE_CHECK, + NOTIFY_SET_THEME_MODE, }; class Observer @@ -152,6 +154,12 @@ class ConfigModel } void setNextUpdateCheck(uint time); + ThemeMode getThemeMode() const + { + return m_themeMode; + } + void setThemeMode(ThemeMode mode); + private: std::vector& sounds() { @@ -183,4 +191,5 @@ class ConfigModel bool m_hotkeysEnabled; uint m_nextUpdateCheck; + ThemeMode m_themeMode; }; diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 42b0c53..447161b 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -17,6 +17,7 @@ #include "MainWindow.h" #include "ConfigModel.h" +#include "Theme.h" #include "main.h" #include "SoundSettings.h" #include "ts3log.h" @@ -59,6 +60,14 @@ MainWindow::MainWindow(ConfigModel* model, QWidget* parent /*= 0*/) : createConfigButtons(); + m_themeButton = new QPushButton(this); + m_themeButton->setFixedSize(28, 28); + m_themeButton->setFlat(true); + m_themeButton->setToolTip("Toggle dark/light mode"); + connect(m_themeButton, &QPushButton::clicked, this, &MainWindow::onThemeButtonClicked); + if (auto* settingsLayout = qobject_cast(ui->settingsWidget->layout())) + settingsLayout->addWidget(m_themeButton); + settingsSection = new ExpandableSection("Settings", 200, this); settingsSection->setContentLayout(*ui->settingsWidget->layout()); layout()->addWidget(settingsSection); @@ -160,27 +169,7 @@ MainWindow::MainWindow(ConfigModel* model, QWidget* parent /*= 0*/) : /* Force configuration 0 */ setConfiguration(0); - ui->gridWidget->setStyleSheet( - "QPushButton {" - " padding: 4px;" - " border: 1px solid rgb(173, 173, 173);" - " color: black;" - " background-color: rgb(225, 225, 225);" - "}" - "QPushButton:disabled {" - " background-color: rgb(204, 204, 204);" - " border-color: rgb(191, 191, 191);" - " color: rgb(120, 120, 120);" - "}" - "QPushButton:hover {" - " background-color: rgb(228, 239, 249);" - " border-color: rgb(11, 123, 212);" - "}" - "QPushButton:pressed {" - " background-color: rgb(204, 228, 247);" - " border-color: rgb(0, 85, 155);" - "}" - ); + applyTheme(m_model->getThemeMode()); } void MainWindow::setConfiguration(int cfg) @@ -888,6 +877,23 @@ void MainWindow::onVolumeSliderContextMenuRemote(const QPoint& point) ts3Functions.requestHotkeyInputDialog(getPluginID(), HOTKEY_VOLUME_DECREASE, 0, this); } +void MainWindow::applyTheme(ThemeMode mode) +{ + bool dark = themeIsDark(mode); + setStyleSheet(dark ? darkThemeStylesheet() : lightThemeStylesheet()); + ui->gridWidget->setStyleSheet(dark ? darkGridStylesheet() : lightGridStylesheet()); + // Sun = click to switch to light; Moon = click to switch to dark + m_themeButton->setText(dark ? "\u2600" : "\U0001F319"); +} + + +void MainWindow::onThemeButtonClicked() +{ + bool currentlyDark = themeIsDark(m_model->getThemeMode()); + m_model->setThemeMode(currentlyDark ? ThemeMode::Light : ThemeMode::Dark); +} + + void MainWindow::ModelObserver::notify(ConfigModel& model, ConfigModel::notifications_e what, int data) { switch (what) @@ -938,6 +944,9 @@ void MainWindow::ModelObserver::notify(ConfigModel& model, ConfigModel::notifica if (p.ui->cb_disable_hotkeys->isChecked() == model.getHotkeysEnabled()) p.ui->cb_disable_hotkeys->setChecked(!model.getHotkeysEnabled()); break; + case ConfigModel::NOTIFY_SET_THEME_MODE: + p.applyTheme(static_cast(data)); + break; default: break; } diff --git a/src/MainWindow.h b/src/MainWindow.h index a6666bb..9e2356e 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -23,6 +23,7 @@ #include "ui_MainWindow.h" #include "ConfigModel.h" +#include "Theme.h" class SpeechBubble; class ExpandableSection; @@ -89,9 +90,9 @@ class MainWindow : public QWidget void onSetConfig(); void onConfigHotkey(); - void onSaveModel(); void onLoadModel(); + void onThemeButtonClicked(); signals: void hotkeyRecordedEvent(QString keyword, QString key); @@ -112,6 +113,7 @@ class MainWindow : public QWidget void openHotkeySetDialog(size_t buttonId); void openButtonColorDialog(size_t buttonId); QString unescapeCustomText(const QString& text); + void applyTheme(ThemeMode mode); class ModelObserver : public ConfigModel::Observer { @@ -142,4 +144,5 @@ class MainWindow : public QWidget QIcon m_playIcon; std::array m_configRadioButtons; std::array m_configHotkeyButtons; + QPushButton* m_themeButton; }; diff --git a/src/Theme.cpp b/src/Theme.cpp new file mode 100644 index 0000000..4573aca --- /dev/null +++ b/src/Theme.cpp @@ -0,0 +1,202 @@ +// src/Theme.cpp +//---------------------------------- +// RP Soundboard Source Code +// Copyright (c) 2015 Marius Graefe +// All rights reserved +// Contact: rp_soundboard@mgraefe.de +//---------------------------------- + +#include "Theme.h" + +#include + + +const char* themeModeToString(ThemeMode mode) +{ + switch (mode) + { + case ThemeMode::Light: return "light"; + case ThemeMode::Dark: return "dark"; + case ThemeMode::System: return "system"; + } + return "system"; +} + + +ThemeMode themeModeFromString(const char* str) +{ + if (strcmp(str, "light") == 0) return ThemeMode::Light; + if (strcmp(str, "dark") == 0) return ThemeMode::Dark; + if (strcmp(str, "system") == 0) return ThemeMode::System; + return ThemeMode::System; +} + +#ifdef _WIN32 +#include + +static bool windowsIsDarkMode() +{ + HKEY key; + if (RegOpenKeyExW( + HKEY_CURRENT_USER, + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize", + 0, KEY_READ, &key) != ERROR_SUCCESS) + return false; + + DWORD value = 1; // default: light + DWORD size = sizeof(value); + RegQueryValueExW(key, L"AppsUseLightTheme", nullptr, nullptr, reinterpret_cast(&value), &size); + RegCloseKey(key); + + return value == 0; // 0 = dark mode +} +#endif + + +bool themeIsDark(ThemeMode mode) +{ + if (mode == ThemeMode::Dark) + return true; + if (mode == ThemeMode::Light) + return false; + + // ThemeMode::System +#ifdef _WIN32 + return windowsIsDarkMode(); +#else + return false; +#endif +} + + +QString lightThemeStylesheet() +{ + return QString(); // Qt default light theme +} + + +QString darkThemeStylesheet() +{ + return + "QWidget {" + " background-color: #2b2b2b;" + " color: #bbbbbb;" + "}" + "QPushButton {" + " background-color: #4c5052;" + " border: 1px solid #555555;" + " color: #bbbbbb;" + " padding: 4px;" + "}" + "QPushButton:hover {" + " background-color: #5c7a9e;" + " border-color: #7aaddc;" + "}" + "QPushButton:pressed {" + " background-color: #4a6fa5;" + " border-color: #6699cc;" + "}" + "QPushButton:disabled {" + " background-color: #3a3d3e;" + " border-color: #444444;" + " color: #666666;" + "}" + "QPushButton:flat {" + " background-color: transparent;" + " border: none;" + "}" + "QPushButton:flat:hover {" + " background-color: #444444;" + " border: none;" + "}" + "QCheckBox {" + " color: #bbbbbb;" + "}" + "QLabel {" + " color: #bbbbbb;" + "}" + "QSpinBox {" + " background-color: #3c3f41;" + " border: 1px solid #555555;" + " color: #bbbbbb;" + "}" + "QSlider::groove:horizontal {" + " background: #555555;" + " height: 4px;" + " border-radius: 2px;" + "}" + "QSlider::handle:horizontal {" + " background: #7aaddc;" + " width: 12px;" + " height: 12px;" + " margin: -4px 0;" + " border-radius: 6px;" + "}" + "QLineEdit {" + " background-color: #3c3f41;" + " border: 1px solid #555555;" + " color: #bbbbbb;" + "}" + "QFrame {" + " color: #555555;" + "}" + "QToolButton {" + " background-color: transparent;" + " border: none;" + " color: #bbbbbb;" + "}" + "QScrollArea {" + " border: none;" + " background: transparent;" + "}"; +} + + +QString lightGridStylesheet() +{ + return + "QPushButton {" + " padding: 4px;" + " border: 1px solid rgb(173, 173, 173);" + " color: black;" + " background-color: rgb(225, 225, 225);" + "}" + "QPushButton:disabled {" + " background-color: rgb(204, 204, 204);" + " border-color: rgb(191, 191, 191);" + " color: rgb(120, 120, 120);" + "}" + "QPushButton:hover {" + " background-color: rgb(228, 239, 249);" + " border-color: rgb(11, 123, 212);" + "}" + "QPushButton:pressed {" + " background-color: rgb(204, 228, 247);" + " border-color: rgb(0, 85, 155);" + "}"; +} + + +QString darkGridStylesheet() +{ + return + "QPushButton {" + " padding: 4px;" + " border: 1px solid rgb(85, 85, 85);" + " color: rgb(187, 187, 187);" + " background-color: rgb(60, 63, 65);" + "}" + "QPushButton:disabled {" + " background-color: rgb(50, 52, 54);" + " border-color: rgb(65, 65, 65);" + " color: rgb(100, 100, 100);" + "}" + "QPushButton:hover {" + " background-color: rgb(70, 100, 140);" + " border-color: rgb(100, 155, 220);" + "}" + "QPushButton:pressed {" + " background-color: rgb(50, 80, 120);" + " border-color: rgb(80, 130, 200);" + "}"; +} diff --git a/src/Theme.h b/src/Theme.h new file mode 100644 index 0000000..8af2055 --- /dev/null +++ b/src/Theme.h @@ -0,0 +1,32 @@ +// src/Theme.h +//---------------------------------- +// RP Soundboard Source Code +// Copyright (c) 2015 Marius Graefe +// All rights reserved +// Contact: rp_soundboard@mgraefe.de +//---------------------------------- + +#pragma once + +#include + +enum class ThemeMode +{ + System = 0, // Follow OS (Windows only; falls back to Light on other platforms) + Light = 1, + Dark = 2, +}; + +const char* themeModeToString(ThemeMode mode); +ThemeMode themeModeFromString(const char* str); // unknown strings fall back to System + +// Returns true if the given mode resolves to dark (queries the OS for System mode). +bool themeIsDark(ThemeMode mode); + +// Returns the stylesheet to apply to the top-level window widget. +QString lightThemeStylesheet(); +QString darkThemeStylesheet(); + +// Returns the stylesheet for the sound button grid (gridWidget). +QString lightGridStylesheet(); +QString darkGridStylesheet();