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