Files
Jungfraujoch/viewer/ViewerTheme.cpp
T
Filip LeonarskiandClaude Opus 5.5 0be8eb2bca Viewer: dark theme, switchable live from the View menu
View > Theme offers Follow system / Light / Dark, stored in the settings.
Follow system uses the desktop's colour scheme where Qt reports it (6.5+)
and is light otherwise. The dark theme is a deep indigo (#1A1D3A panels,
#12142B entry fields and charts).

The palettes live in the new ViewerTheme module. A switch applies at once:
the application stylesheet is set again after the palette so every widget
is re-polished (with a stylesheet active, polished widgets otherwise keep
their old palette); widgets that bake colours into stylesheets use
SetThemedStyleSheet, which re-applies them on ThemeNotifier::changed;
toolbar icons draw their glyph at paint time through an icon engine, so the
ink follows the theme (and stays sharp at any size); charts re-theme and
rebuild. Hard-coded white backgrounds on entry fields and tables are
removed in favour of the palette's base colour.

The idle detector-status badge in the status bar is now transparent instead
of an empty default-styled progress bar.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 17:51:50 +02:00

151 lines
6.3 KiB
C++

// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#include "ViewerTheme.h"
#include <QApplication>
#include <QChart>
#include <QPalette>
#include <QSettings>
#include <QStyle>
#include <QStyleHints>
#include <QWidget>
namespace {
bool dark_theme = false;
const QColor DARK_WINDOW(0x1A, 0x1D, 0x3A); // a deep indigo
const QColor DARK_TEXT(0xE3, 0xE7, 0xEC);
bool SystemIsDark() {
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
return QGuiApplication::styleHints()->colorScheme() == Qt::ColorScheme::Dark;
#else
return false;
#endif
}
// Text roles are set for the active and inactive groups only, so disabled text keeps the greyed
// colour the palette derived for it.
void SetTextColor(QPalette &pal, const QColor &color) {
for (auto group: {QPalette::Active, QPalette::Inactive})
for (auto role: {QPalette::WindowText, QPalette::Text, QPalette::ButtonText, QPalette::ToolTipText})
pal.setColor(group, role, color);
}
// Salmon panels, white entry fields that fall back to salmon when disabled, and a navy accent for
// selections and progress-bar chunks. It starts from Fusion's own (light) palette, not the system
// one, which is dark under a dark OS theme; the text is set to black because on macOS the palette's
// text colours come from the platform theme and are grey.
QPalette LightPalette() {
QPalette pal = QApplication::style()->standardPalette();
pal.setColor(QPalette::Window, QColor(255, 235, 230));
pal.setColor(QPalette::Base, Qt::white);
pal.setColor(QPalette::AlternateBase, QColor(255, 245, 242));
pal.setColor(QPalette::Button, QColor(255, 235, 230));
pal.setColor(QPalette::Disabled, QPalette::Base, QColor(255, 235, 230));
pal.setColor(QPalette::Highlight, QColor(0x1f, 0x3a, 0x5f));
pal.setColor(QPalette::HighlightedText, Qt::white);
SetTextColor(pal, Qt::black);
return pal;
}
// The same layout in the dark: indigo panels, entry fields darker still, and a lighter navy for
// selections so they stand out from the panels. QPalette(button) derives the bevel shades and the
// disabled text from the one colour.
QPalette DarkPalette() {
QPalette pal(DARK_WINDOW);
pal.setColor(QPalette::Window, DARK_WINDOW);
pal.setColor(QPalette::Base, QColor(0x12, 0x14, 0x2B));
pal.setColor(QPalette::AlternateBase, QColor(0x24, 0x28, 0x4A));
pal.setColor(QPalette::Button, DARK_WINDOW);
pal.setColor(QPalette::Disabled, QPalette::Base, DARK_WINDOW);
pal.setColor(QPalette::ToolTipBase, QColor(0x2A, 0x2E, 0x55));
pal.setColor(QPalette::Highlight, QColor(0x2F, 0x5A, 0x8F));
pal.setColor(QPalette::HighlightedText, Qt::white);
pal.setColor(QPalette::Link, QColor(0x8A, 0xB4, 0xF8));
pal.setColor(QPalette::PlaceholderText, QColor(0x80, 0x84, 0xA8));
SetTextColor(pal, DARK_TEXT);
return pal;
}
void ApplyTheme() {
const ThemeChoice choice = LoadThemeChoice();
dark_theme = choice == ThemeChoice::Dark || (choice == ThemeChoice::System && SystemIsDark());
QApplication::setPalette(dark_theme ? DarkPalette() : LightPalette());
// With an application stylesheet in place, widgets take their palette when they are polished
// and do not see a later palette change; setting the stylesheet again re-polishes them all.
// Fusion fills QGroupBox interiors with a flat colour of its own; transparent makes them show
// the window background like the rest of the UI (the dock resize handles are drawn by
// SeparatorHandleStyle, not the stylesheet).
qApp->setStyleSheet("QGroupBox { background-color: transparent; }");
emit ThemeNotifier::Instance()->changed();
}
}
ThemeNotifier *ThemeNotifier::Instance() {
static ThemeNotifier notifier;
return &notifier;
}
ThemeChoice LoadThemeChoice() {
const QString value = QSettings("PSI", "jfjoch_viewer").value("theme", "system").toString();
if (value == "light") return ThemeChoice::Light;
if (value == "dark") return ThemeChoice::Dark;
return ThemeChoice::System;
}
void SetThemeChoice(ThemeChoice choice) {
const char *value = "system";
if (choice == ThemeChoice::Light) value = "light";
if (choice == ThemeChoice::Dark) value = "dark";
QSettings("PSI", "jfjoch_viewer").setValue("theme", value);
ApplyTheme();
}
void InitTheme() {
ApplyTheme();
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
// Re-applied on any desktop scheme change, not only under System: the palette set here must also
// survive the platform reacting to the change with a palette of its own.
QObject::connect(QGuiApplication::styleHints(), &QStyleHints::colorSchemeChanged,
ThemeNotifier::Instance(), [] { ApplyTheme(); });
#endif
}
bool DarkTheme() {
return dark_theme;
}
void SetThemedStyleSheet(QWidget *widget, const std::function<QString()> &style) {
widget->setStyleSheet(style());
QObject::connect(ThemeNotifier::Instance(), &ThemeNotifier::changed, widget,
[widget, style] { widget->setStyleSheet(style()); });
}
QColor AccentTextColor() {
return dark_theme ? QColor(0x9D, 0xBB, 0xE3) : QColor(0x1F, 0x3A, 0x5F);
}
QColor IconInkColor() {
return dark_theme ? DARK_TEXT : QColor(0x2B, 0x2B, 0x2B);
}
QString SliderStyle() {
// On the dark panels the navy handle would vanish, so it takes the light blue of the headings.
const QString groove = dark_theme ? "#363A66" : "#F3D9D4";
const QString handle = dark_theme ? "#9DBBE3" : "#1F3A5F";
const QString handle_hover = dark_theme ? "#BCD2EE" : "#16314F";
return QString(
"QSlider::groove:horizontal { height:6px; background:%1; border-radius:3px; }"
"QSlider::sub-page:horizontal { background:#FA7268; border-radius:3px; }"
"QSlider::handle:horizontal { background:%2; width:14px; margin:-5px 0; border-radius:7px; }"
"QSlider::handle:horizontal:hover { background:%3; }").arg(groove, handle, handle_hover);
}
void StyleChart(QChart *chart) {
chart->setTheme(dark_theme ? QChart::ChartThemeDark : QChart::ChartThemeLight);
if (dark_theme)
chart->setBackgroundBrush(QApplication::palette().color(QPalette::Base));
}