wiRedPanda
Logic Circuit Simulator
Loading...
Searching...
No Matches
ThemeManager.cpp
Go to the documentation of this file.
1// Copyright 2015 - 2026, GIBIS-UNIFESP and the wiRedPanda contributors
2// SPDX-License-Identifier: GPL-3.0-or-later
3
5
6#include <QCoreApplication>
7#include <QFont>
8#include <QStyleHints>
9#include <QThread>
10
11#include "App/Core/Settings.h"
12
13ThemeManager::ThemeManager(QObject *parent)
14 : QObject(parent)
15{
16 Q_ASSERT(QCoreApplication::instance()->thread() == QThread::currentThread());
17
18 // Load the persisted theme preference; if not set, m_theme keeps its
19 // default-initialised value (Theme::Light, as defined in the header)
20 m_theme = Settings::theme();
21
22 // Apply the theme immediately so colours are correct before any widgets are shown.
23 // Resolve System here directly — effectiveTheme() would re-enter instance() and deadlock
24 // because the static local is still being constructed.
25 const Theme effective = (m_theme == Theme::System) ? resolveSystemTheme() : m_theme;
26
27#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
28 // Set color scheme BEFORE applying the palette: setColorScheme() controls what
29 // QApplication::style()->standardPalette() returns on Windows/Qt 6.8+.
30 // Calling it after would cause the Light theme to read a dark standardPalette().
31 // System theme: leave m_requestedColorScheme as Unknown so the platform reads the
32 // OS color scheme naturally. Setting it to Dark/Light explicitly would block runtime
33 // OS theme-change events from propagating through QGtk3Theme::colorScheme().
34 if (auto *app = qApp) {
35 switch (m_theme) {
36 case Theme::Light: app->styleHints()->setColorScheme(Qt::ColorScheme::Light); break;
37 case Theme::Dark: app->styleHints()->setColorScheme(Qt::ColorScheme::Dark); break;
38 case Theme::System: break; // leave Unknown — platform reads OS setting directly
39 }
40 }
41#endif
42
43 m_attributes.setTheme(effective);
44
45#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
46 // Qt 6.5+ provides a built-in color-scheme change signal; use it so System
47 // theme reacts at runtime when the user toggles OS dark/light mode.
48 if (auto *app = qApp) {
49 connect(app->styleHints(), &QStyleHints::colorSchemeChanged,
50 this, [this](Qt::ColorScheme) { onSystemColorSchemeChanged(); });
51 }
52#endif
53}
54
56{
57 return (effectiveTheme() == Theme::Light) ? "Light" : "Dark";
58}
59
61{
62 return instance().m_theme;
63}
64
66{
67 const Theme t = instance().m_theme;
68 return (t == Theme::System) ? resolveSystemTheme() : t;
69}
70
71Theme ThemeManager::resolveSystemTheme()
72{
73#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
74 if (auto *app = qApp) {
75 return (app->styleHints()->colorScheme() == Qt::ColorScheme::Dark) ? Theme::Dark : Theme::Light;
76 }
77 return Theme::Light;
78#else
79 if (auto *app = qApp) {
80 return (app->palette().color(QPalette::Window).lightness() < 128)
82 }
83 return Theme::Light;
84#endif
85}
86
87void ThemeManager::onSystemColorSchemeChanged()
88{
89 if (m_theme == Theme::System) {
90 m_attributes.setTheme(resolveSystemTheme());
91 emit themeChanged();
92 }
93}
94
96{
97 Q_ASSERT(QCoreApplication::instance()->thread() == QThread::currentThread());
98
99#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
100 // For System theme, clear the explicit color scheme override first so the platform
101 // re-reads the OS setting. Qt 6.9.3's requestColorScheme() posts a QWindowSystemInterface
102 // event but doesn't flush it — QStyleHints::colorScheme() would return a stale cached
103 // value until the event loop runs. Flushing here replicates the fix that Qt 6.10.0
104 // added directly inside requestColorScheme().
105 if (theme == Theme::System) {
106 if (auto *app = Application::instance()) {
107 app->styleHints()->setColorScheme(Qt::ColorScheme::Unknown);
108 // Flush the pending QWindowSystemInterface::handleThemeChange() event so
109 // QStyleHintsPrivate::m_colorScheme is updated before we call resolveSystemTheme().
110 // Qt 6.10.0 fixed this inside requestColorScheme() itself (commit 2fe9eed3fdd5,
111 // "QGnomeTheme, QGtk3Theme: Refactor and Simplify DBus Interactions", 2025-05-30)
112 // by adding QWindowSystemInterface::sendWindowSystemEvents(QEventLoop::AllEvents).
113 // On Qt 6.9.x the flush is absent, leaving the cache stale until the event loop runs.
114 QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
115 }
116 }
117#endif
118
119 const Theme effective = (theme == Theme::System) ? resolveSystemTheme() : theme;
120
121#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
122 // Set color scheme BEFORE applying the palette: on Windows/Qt 6.8+,
123 // setColorScheme() controls what QApplication::style()->standardPalette() returns.
124 // Calling it after would cause the Light theme to read a dark standardPalette().
125 // System theme: m_requestedColorScheme is already Unknown from the block above —
126 // leave it there. Re-setting it to Dark/Light would break runtime OS theme-change
127 // detection (QGtk3Theme::colorScheme() would return the explicit value, ignoring
128 // any subsequent OS toggle).
129 if (auto *app = Application::instance()) {
130 switch (theme) {
131 case Theme::Light: app->styleHints()->setColorScheme(Qt::ColorScheme::Light); break;
132 case Theme::Dark: app->styleHints()->setColorScheme(Qt::ColorScheme::Dark); break;
133 case Theme::System: break; // already Unknown — platform reads OS setting directly
134 }
135 }
136#endif
137
138 // Always refresh ThemeAttributes so palette and color constants are current,
139 // even if the theme value itself hasn't changed (e.g. initial load).
140 instance().m_attributes.setTheme(effective);
141
142 // Early-exit after refreshing attributes: don't re-emit themeChanged() or
143 // re-write to settings when the theme is the same (avoids unnecessary repaints)
144 if (instance().m_theme == theme) {
145 return;
146 }
147
148 instance().m_theme = theme;
149 // Persist so the selected theme is restored on next application launch
151 // Notify all connected widgets (scene, view, etc.) to repaint with the new palette
152 emit instance().themeChanged();
153}
154
156{
157 return instance().m_attributes;
158}
159
161{
162 switch (theme) {
163 case Theme::Light: {
164 m_sceneBgBrush = QColor(255, 255, 230); // warm off-white (slight yellow tint); less eye-strain than pure white
165 m_sceneBgDots = QColor(Qt::darkGray);
166
167 m_selectionBrush = QColor(175, 0, 0, 80); // semi-transparent red fill for rubber-band selection
168 m_selectionPen = QColor(175, 0, 0, 255); // fully-opaque red border for the selection rectangle
169
170 m_graphicElementLabelColor = QColor(Qt::black);
171
172 m_connectionUnknown = QColor(140, 140, 140); // neutral gray — "nothing is driving this"
173 m_connectionInactive = QColor(Qt::darkGreen);
174 m_connectionActive = QColor(Qt::green);
176
177#ifndef Q_OS_MAC
178 // Build light palette from scratch, independent of the current system
179 // theme state. This ensures Light theme works correctly even when the
180 // system is in dark mode.
181 QPalette lightPalette;
182 lightPalette.setColor(QPalette::Window, Qt::white);
183 lightPalette.setColor(QPalette::WindowText, Qt::black);
184 lightPalette.setColor(QPalette::Base, Qt::white);
185 lightPalette.setColor(QPalette::AlternateBase, QColor(233, 231, 227)); // light grey for alternating rows
186 lightPalette.setColor(QPalette::ToolTipBase, Qt::white);
187 lightPalette.setColor(QPalette::ToolTipText, Qt::black);
188 lightPalette.setColor(QPalette::Text, Qt::black);
189 lightPalette.setColor(QPalette::Button, Qt::white);
190 lightPalette.setColor(QPalette::ButtonText, Qt::black);
191 lightPalette.setColor(QPalette::BrightText, Qt::red);
192 lightPalette.setColor(QPalette::Link, QColor(0, 0, 255));
193 lightPalette.setColor(QPalette::Highlight, QColor(0, 120, 215)); // light blue selection
194 lightPalette.setColor(QPalette::Disabled, QPalette::ButtonText, QColor(128, 128, 128));
195 lightPalette.setColor(QPalette::Disabled, QPalette::Base, QColor(240, 240, 240));
196 lightPalette.setColor(QPalette::Disabled, QPalette::WindowText, QColor(128, 128, 128));
197
198 if (Application::instance()) {
199 Application::instance()->setPalette(lightPalette);
200 }
201#endif
202
203 break;
204 }
205
206 case Theme::Dark: {
207 m_sceneBgBrush = QColor(64, 69, 82); // dark blue-grey slate; chosen to provide sufficient luminance
208 // contrast against the light-grey dot grid overlay
209 m_sceneBgDots = QColor(Qt::lightGray);
210
211 m_selectionBrush = QColor(230, 255, 85, 150); // semi-transparent yellow-green fill; contrasts with dark background
212 m_selectionPen = QColor(230, 255, 85, 255); // fully-opaque for a crisp selection border
213
214 m_graphicElementLabelColor = QColor(Qt::white);
215
216 m_connectionUnknown = QColor(160, 160, 160, 255); // light gray — "nothing is driving this"
217 m_connectionInactive = QColor(65, 150, 130, 255); // muted teal; visible on dark background without competing with active wires
218 m_connectionActive = QColor(115, 255, 220, 255); // bright cyan-green; clearly distinguishes an asserted wire
220
221#ifndef Q_OS_MAC
222 // macOS handles dark mode at the OS level; manually overriding the
223 // palette there causes visual glitches, so the block is skipped.
224 QPalette darkPalette;
225 // A standard Qt dark palette recipe: 53/53/53 for surfaces, 25/25/25 for
226 // input fields (darker so they stand out from the surrounding surface).
227 // 120/120/120 is used for disabled roles — roughly mid-grey, readable but
228 // clearly de-emphasised compared to the full-white active text.
229 darkPalette.setColor(QPalette::Window, QColor(53, 53, 53));
230 darkPalette.setColor(QPalette::WindowText, Qt::white);
231 darkPalette.setColor(QPalette::Base, QColor(25, 25, 25)); // input field / list background
232 darkPalette.setColor(QPalette::AlternateBase, QColor(53, 53, 53));
233 darkPalette.setColor(QPalette::ToolTipBase, Qt::white);
234 darkPalette.setColor(QPalette::ToolTipText, Qt::white);
235 darkPalette.setColor(QPalette::Text, Qt::white);
236 darkPalette.setColor(QPalette::Button, QColor(53, 53, 53));
237 darkPalette.setColor(QPalette::ButtonText, Qt::white);
238 darkPalette.setColor(QPalette::BrightText, Qt::red); // used by some styles for error text
239 darkPalette.setColor(QPalette::Link, QColor(42, 130, 218)); // blue link colour matching Highlight
240
241 darkPalette.setColor(QPalette::Highlight, QColor(42, 130, 218)); // selection highlight (mid-blue)
242
243 // Disabled widgets use a uniform mid-grey to signal unavailability without
244 // disappearing entirely (white on dark would be too low-contrast)
245 darkPalette.setColor(QPalette::Disabled, QPalette::ButtonText, QColor(120, 120, 120));
246 darkPalette.setColor(QPalette::Disabled, QPalette::Base, QColor(120, 120, 120));
247 darkPalette.setColor(QPalette::Disabled, QPalette::WindowText, QColor(120, 120, 120));
248
249 if (Application::instance()) {
250 Application::instance()->setPalette(darkPalette);
251 }
252#endif
253
254 break;
255 }
256
257 default:
258 // Handle unexpected theme values gracefully - fallback to Light theme
260 break;
261 }
262
263#ifndef Q_OS_MAC
264 // Force a consistent tooltip style on both themes; without this, Windows/Linux
265 // style sheets would show platform-default tooltips that are illegible on dark
266 // backgrounds. The #2a82da background matches QPalette::Highlight above.
267 //
268 // Toolbar button labels are shrunk here too, relative to the app's default font size.
269 // This has to live in the same setStyleSheet() call rather than a one-off setFont() on
270 // the toolbar/buttons: applying (or re-applying) an app-wide style sheet re-polishes
271 // every widget, which silently resets any font set directly via QWidget::setFont() --
272 // and setTheme() re-runs this on every theme switch, including the async system
273 // colour-scheme change QStyleHints::colorSchemeChanged delivers a moment after startup.
274 if (Application::instance()) {
275 const qreal toolBarFontPt = Application::instance()->font().pointSizeF() * 0.75;
276 Application::instance()->setStyleSheet(
277 QStringLiteral("QToolTip { color: #ffffff; background-color: #2a82da; border: 1px solid white; }"
278 "QToolBar QToolButton { font-size: %1pt; }").arg(toolBarFontPt));
279 }
280#endif
281
282 // Port brushes mirror the wire colours for consistency so users can visually
283 // correlate a wire's colour with the port it connects to.
288
289 // Port pen colours are theme-invariant because they are drawn as outlines
290 // on top of the brush fill and need consistent contrast
291 m_portUnknownPen = QColor(120, 120, 120); // dark gray — distinct from error red
292 m_portInactivePen = QColor(Qt::black);
293 m_portActivePen = QColor(Qt::black);
294 m_portErrorPen = QColor(Qt::red);
295
296 m_portHoverPort = QColor(Qt::yellow); // bright yellow on hover for high visibility regardless of theme
297
298 // Hover-label chips reuse the forced tooltip palette (see the QToolTip stylesheet above):
299 // #2a82da background with white text, theme-invariant so the labels read clearly on both themes.
300 m_portHoverLabelBg = QColor(0x2a, 0x82, 0xda);
301 m_portHoverLabelText = QColor(Qt::white);
302}
Typed wrappers around QSettings for all application preferences.
Theme management types and singleton ThemeManager.
Theme
Enumeration of available application themes.
static Application * instance()
Returns the application instance cast to Application*.
static Theme theme()
Definition Settings.cpp:154
static void setTheme(Theme theme)
Definition Settings.cpp:167
Contains all color attributes for a theme.
QColor m_connectionUnknown
QColor m_connectionInactive
QColor m_portHoverLabelText
QColor m_portInactivePen
QColor m_portHoverPort
QColor m_portHoverLabelBg
QColor m_connectionSelected
QColor m_portInactiveBrush
QColor m_portUnknownPen
QColor m_portUnknownBrush
QColor m_sceneBgBrush
QColor m_connectionActive
QColor m_connectionError
QColor m_portActivePen
QColor m_portErrorPen
QColor m_portActiveBrush
void setTheme(const Theme theme)
Applies theme colors to all color attributes in this object.
QColor m_selectionBrush
QColor m_selectionPen
QColor m_portErrorBrush
QColor m_graphicElementLabelColor
static Theme theme()
Returns the currently active theme.
static ThemeManager & instance()
Returns the singleton ThemeManager instance.
void themeChanged()
Emitted whenever the active theme changes.
static void setTheme(const Theme theme)
Switches the application to theme and emits themeChanged().
static Theme effectiveTheme()
Returns the effective theme (Light or Dark), resolving System to the OS preference.
static QString themePath()
Returns the resource path prefix for the current theme (e.g. "dark" or "light").
static const ThemeAttributes & attributes()
Returns the current ThemeAttributes color set.