Files
Jungfraujoch/viewer/ViewerTheme.cpp
T
Filip LeonarskiandClaude Opus 5.5 9f369567f1 Viewer: font size and theme buttons on the display toolbar, matching title bar
The right end of the display toolbar now carries three "A" buttons for the
100/125/150 % font sizes and a light/dark switch (a sun with a half-filled
disc), so both settings are visible without opening the View menu. The size
buttons and View > Font size stay in sync through JFJochViewerMenu's new
SetFontZoom / fontZoomChanged; the View > Theme tick follows ThemeNotifier,
so it moves when the toolbar switches the theme.

With Qt 6.8+ an explicit Light or Dark choice is also requested from the OS
(QStyleHints::setColorScheme), so the title bar macOS and Windows draw
matches the window, as apps with their own theme switch do. Follow system
withdraws the request first, since Qt otherwise reports the requested scheme
instead of the desktop's.

The image counter reads "1 / 1800" instead of "1/1800".

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

165 lines
7.2 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();
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
// Once the application has asked for a scheme, Qt reports that scheme instead of the desktop's,
// so the request is withdrawn before System looks at what the desktop is using.
if (choice == ThemeChoice::System)
QGuiApplication::styleHints()->setColorScheme(Qt::ColorScheme::Unknown);
#endif
dark_theme = choice == ThemeChoice::Dark || (choice == ThemeChoice::System && SystemIsDark());
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
// The window frame is drawn by the OS (macOS, Windows), not by Qt, and follows the desktop's
// scheme unless the application asks for one - as apps with their own theme switch do, so the
// title bar matches the window under it.
if (choice != ThemeChoice::System)
QGuiApplication::styleHints()->setColorScheme(dark_theme ? Qt::ColorScheme::Dark
: Qt::ColorScheme::Light);
#endif
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));
}