diff --git a/viewer/CMakeLists.txt b/viewer/CMakeLists.txt index eaf9d1ddb..0531d5062 100644 --- a/viewer/CMakeLists.txt +++ b/viewer/CMakeLists.txt @@ -25,6 +25,8 @@ ADD_EXECUTABLE(jfjoch_viewer jfjoch_viewer.cpp JFJochViewerWindow.cpp JFJochView JFJochViewerMenu.h RemoteDisplayMode.cpp RemoteDisplayMode.h + ViewerTheme.cpp + ViewerTheme.h image_viewer/JFJochDiffractionImage.cpp image_viewer/JFJochDiffractionImage.h image_viewer/JFJochSimpleImage.cpp diff --git a/viewer/JFJochViewerMenu.cpp b/viewer/JFJochViewerMenu.cpp index 60f5cf129..1c5d06305 100644 --- a/viewer/JFJochViewerMenu.cpp +++ b/viewer/JFJochViewerMenu.cpp @@ -3,6 +3,7 @@ #include "JFJochViewerMenu.h" #include "RemoteDisplayMode.h" +#include "ViewerTheme.h" #include #include @@ -90,6 +91,22 @@ JFJochViewerMenu::JFJochViewerMenu(QWidget *parent) : QMenuBar(parent) { remoteMode->setChecked(RemoteDisplayMode()); connect(remoteMode, &QAction::toggled, this, [](bool on) { SetRemoteDisplayMode(on); }); + viewMenu->addSeparator(); + QMenu *themeMenu = viewMenu->addMenu("Theme"); + auto *themeGroup = new QActionGroup(this); + const ThemeChoice currentTheme = LoadThemeChoice(); + const std::pair themes[] = {{ThemeChoice::System, "Follow system"}, + {ThemeChoice::Light, "Light"}, + {ThemeChoice::Dark, "Dark"}}; + for (const auto &theme: themes) { + const ThemeChoice choice = theme.first; + QAction *action = themeMenu->addAction(theme.second); + action->setCheckable(true); + action->setChecked(choice == currentTheme); + themeGroup->addAction(action); + connect(action, &QAction::triggered, this, [choice] { SetThemeChoice(choice); }); + } + viewMenu->addSeparator(); // The desktop's own text scaling stays the primary mechanism, but few users know where it lives, // so the viewer carries a zoom of its own. It multiplies the font the desktop handed us, so it diff --git a/viewer/JFJochViewerStatusBar.cpp b/viewer/JFJochViewerStatusBar.cpp index 64ba1ed4b..f8c350ee2 100644 --- a/viewer/JFJochViewerStatusBar.cpp +++ b/viewer/JFJochViewerStatusBar.cpp @@ -18,6 +18,8 @@ JFJochViewerStatusBar::JFJochViewerStatusBar(QWidget *parent) : QStatusBar(paren state_box->setTextVisible(true); state_box->setAlignment(Qt::AlignCenter); state_box->setFormat(""); + // No detector session yet: the badge keeps its place but draws nothing (see setHttpConnection). + state_box->setStyleSheet("QProgressBar { border: none; background: transparent; }"); addPermanentWidget(state_box); } @@ -58,7 +60,7 @@ void JFJochViewerStatusBar::setHttpConnection(bool connected, QString addr) { // No detector session at all -> blank the whole cluster (but keep the reserved space). state_box->setFormat(""); state_box->setValue(0); - state_box->setStyleSheet(""); + state_box->setStyleSheet("QProgressBar { border: none; background: transparent; }"); state_box->setToolTip(""); rate_label->setText(""); return; diff --git a/viewer/ViewerTheme.cpp b/viewer/ViewerTheme.cpp new file mode 100644 index 000000000..02361d54c --- /dev/null +++ b/viewer/ViewerTheme.cpp @@ -0,0 +1,150 @@ +// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute +// SPDX-License-Identifier: GPL-3.0-only + +#include "ViewerTheme.h" + +#include +#include +#include +#include +#include +#include +#include + +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 ¬ifier; +} + +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 &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)); +} diff --git a/viewer/ViewerTheme.h b/viewer/ViewerTheme.h new file mode 100644 index 000000000..022c218d4 --- /dev/null +++ b/viewer/ViewerTheme.h @@ -0,0 +1,45 @@ +// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute +// SPDX-License-Identifier: GPL-3.0-only + +#pragma once + +#include +#include +#include +#include + +class QChart; +class QWidget; + +// The viewer's colour scheme: the light salmon one, or a dark indigo one. The View menu offers +// System, Light and Dark; System follows the desktop where Qt can read it (Qt 6.5+) and is light +// otherwise. A change applies at once: widgets that follow the palette repaint by themselves, and +// the ones that put colours into stylesheets, icons or charts listen to ThemeNotifier. +enum class ThemeChoice { System, Light, Dark }; +ThemeChoice LoadThemeChoice(); +void SetThemeChoice(ThemeChoice choice); // stores the choice and applies it + +// Sets the palette and starts following the desktop's scheme. Call once, after the Fusion style is +// set and before any window is made. +void InitTheme(); +bool DarkTheme(); + +class ThemeNotifier : public QObject { + Q_OBJECT +public: + static ThemeNotifier *Instance(); +signals: + void changed(); +}; + +// Sets the widget's stylesheet from `style`, now and again after every theme change. +void SetThemedStyleSheet(QWidget *widget, const std::function &style); + +// Colours that widgets put into their own stylesheets and drawings. +QColor AccentTextColor(); // headings and headline values: navy on light, light blue on dark +QColor IconInkColor(); // toolbar glyphs and flat toolbar-button text +QString SliderStyle(); // the coral/navy sliders of the toolbars + +// Qt Charts do not follow the widget palette; this gives a chart the current scheme's colours. It +// recolours the series too, so call it before adding series that carry colours of their own. +void StyleChart(QChart *chart); diff --git a/viewer/charts/JFJochDatasetInfoChartView.cpp b/viewer/charts/JFJochDatasetInfoChartView.cpp index c22b83c44..9093c04c4 100644 --- a/viewer/charts/JFJochDatasetInfoChartView.cpp +++ b/viewer/charts/JFJochDatasetInfoChartView.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-only #include +#include "../ViewerTheme.h" #include #include #include @@ -26,6 +27,12 @@ namespace { JFJochDatasetInfoChartView::JFJochDatasetInfoChartView(QWidget *parent) : QChartView(new QChart(), parent) { + StyleChart(chart()); + // The theme recolours every series, the run colours included, so the chart is rebuilt after it. + connect(ThemeNotifier::Instance(), &ThemeNotifier::changed, this, [this] { + StyleChart(chart()); + updateChart(); + }); chart()->legend()->hide(); // Reclaim Qt Charts' outer layout padding so the axis labels keep their room even when the dock // is short (otherwise they are the first thing Qt drops); the inner margins are set per rebuild. @@ -400,7 +407,7 @@ void JFJochDatasetInfoChartView::buildTimeDomainChart() { series->setColor(primary_color_); setSeriesPenWidth(series, fontMetrics().height()); currentSeries = new QScatterSeries(this); - currentSeries->setColor(Qt::black); // "current image" marker: fixed, not a run colour + currentSeries->setColor(palette().color(QPalette::WindowText)); // "current image" marker: not a run colour currentSeries->setMarkerSize(9.0); double dispMin = std::numeric_limits::infinity(); diff --git a/viewer/charts/JFJochSimpleChartView.cpp b/viewer/charts/JFJochSimpleChartView.cpp index 6a8ce3e7d..36be54608 100644 --- a/viewer/charts/JFJochSimpleChartView.cpp +++ b/viewer/charts/JFJochSimpleChartView.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-only #include "JFJochSimpleChartView.h" +#include "../ViewerTheme.h" #include #include @@ -12,6 +13,8 @@ JFJochSimpleChartView::JFJochSimpleChartView(QWidget *parent) : QChartView(new QChart(), parent) { + StyleChart(chart()); + connect(ThemeNotifier::Instance(), &ThemeNotifier::changed, this, [this] { StyleChart(chart()); }); chart()->legend()->hide(); chart()->layout()->setContentsMargins(0, 0, 0, 0); // reclaim padding for the plot + labels chart()->setMargins(QMargins(2, 10, 8, 2)); // top room so the highest Y label is not clipped diff --git a/viewer/jfjoch_viewer.cpp b/viewer/jfjoch_viewer.cpp index 72133418d..4587560f9 100644 --- a/viewer/jfjoch_viewer.cpp +++ b/viewer/jfjoch_viewer.cpp @@ -10,6 +10,7 @@ #include "JFJochViewerWindow.h" #include "RemoteDisplayMode.h" +#include "ViewerTheme.h" #include "../writer/HDF5Objects.h" #include "../common/GitInfo.h" @@ -65,28 +66,8 @@ int main(int argc, char *argv[]) { RegisterHDF5Filter(); QApplication app(argc, argv); - // Theme via the palette (robust across widgets): salmon panels, white entry fields that fall back - // to salmon when disabled, and a navy accent for selections and progress-bar chunks. - // Start from Fusion's own (light) palette, not the system one, which is dark under a dark OS theme. app.setStyle(new SeparatorHandleStyle("Fusion")); - QPalette pal = app.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); - // Black text as on Linux and Windows: on macOS the palette's text colours come from the platform - // theme and are grey (or white under a dark OS theme). Disabled text keeps Fusion's grey. - for (auto group: {QPalette::Active, QPalette::Inactive}) - for (auto role: {QPalette::WindowText, QPalette::Text, QPalette::ButtonText, QPalette::ToolTipText}) - pal.setColor(group, role, Qt::black); - app.setPalette(pal); - // Fusion fills QGroupBox interiors with a flat light colour; make them transparent so they show - // the salmon window background like the rest of the UI (entry widgets stay white via the palette). - // The dock resize handles are drawn by SeparatorHandleStyle above, not the stylesheet. - app.setStyleSheet("QGroupBox { background-color: transparent; }"); + InitTheme(); // light or dark palette and the application stylesheet, per the View menu's choice QIcon appIcon(":/jfjoch.png"); app.setWindowIcon(appIcon); diff --git a/viewer/toolbar/JFJochViewerToolbarDisplay.cpp b/viewer/toolbar/JFJochViewerToolbarDisplay.cpp index d9fac3972..a0f3e91c8 100644 --- a/viewer/toolbar/JFJochViewerToolbarDisplay.cpp +++ b/viewer/toolbar/JFJochViewerToolbarDisplay.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-only #include "JFJochViewerToolbarDisplay.h" +#include "../ViewerTheme.h" #include #include "../../common/ColorScale.h" #include "../RemoteDisplayMode.h" @@ -23,11 +24,6 @@ JFJochViewerToolbarDisplay::JFJochViewerToolbarDisplay(QWidget *parent) connect(background_tail_timer_, &QTimer::timeout, this, &JFJochViewerToolbarDisplay::ApplyPendingBackground); - const QString sliderStyle = - "QSlider::groove:horizontal { height:6px; background:#F3D9D4; border-radius:3px; }" - "QSlider::sub-page:horizontal { background:#FA7268; border-radius:3px; }" - "QSlider::handle:horizontal { background:#1F3A5F; width:14px; margin:-5px 0; border-radius:7px; }" - "QSlider::handle:horizontal:hover { background:#16314F; }"; // Two sliders share the strip, so each is capped rather than expanding across the window. const int slider_cap = fontMetrics().averageCharWidth() * 36; @@ -38,7 +34,7 @@ JFJochViewerToolbarDisplay::JFJochViewerToolbarDisplay(QWidget *parent) foreground_slider->setValue(10); foreground_slider->setToolTip("White point of the color map (image contrast); hold F and use" " the mouse wheel, Ctrl+wheel for coarse steps"); - foreground_slider->setStyleSheet(sliderStyle); + SetThemedStyleSheet(foreground_slider, SliderStyle); foreground_slider->setMaximumWidth(slider_cap); addWidget(foreground_slider); @@ -49,7 +45,7 @@ JFJochViewerToolbarDisplay::JFJochViewerToolbarDisplay(QWidget *parent) background_slider->setValue(0); background_slider->setToolTip("Black point of the color map (0 = off); hold B and use" " the mouse wheel"); - background_slider->setStyleSheet(sliderStyle); + SetThemedStyleSheet(background_slider, SliderStyle); background_slider->setMaximumWidth(slider_cap); addWidget(background_slider); @@ -60,7 +56,7 @@ JFJochViewerToolbarDisplay::JFJochViewerToolbarDisplay(QWidget *parent) b->setCheckable(true); b->setToolTip(tip); b->setCursor(Qt::PointingHandCursor); - b->setStyleSheet(ToolbarIcons::buttonStyle()); + SetThemedStyleSheet(b, ToolbarIcons::buttonStyle); return b; }; auto_foreground_button = makeToggle("Auto", "Auto-contrast to the current image"); diff --git a/viewer/toolbar/JFJochViewerToolbarImage.cpp b/viewer/toolbar/JFJochViewerToolbarImage.cpp index 3362a8905..c9b12068f 100644 --- a/viewer/toolbar/JFJochViewerToolbarImage.cpp +++ b/viewer/toolbar/JFJochViewerToolbarImage.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-only #include "JFJochViewerToolbarImage.h" +#include "../ViewerTheme.h" #include #include #include "../RemoteDisplayMode.h" @@ -18,7 +19,7 @@ JFJochViewerToolbarImage::JFJochViewerToolbarImage(QWidget *parent) : QToolBar(p b->setToolTip(tip); b->setCheckable(checkable); b->setCursor(Qt::PointingHandCursor); - b->setStyleSheet(ToolbarIcons::buttonStyle()); + SetThemedStyleSheet(b, ToolbarIcons::buttonStyle); return b; }; @@ -38,11 +39,7 @@ JFJochViewerToolbarImage::JFJochViewerToolbarImage(QWidget *parent) : QToolBar(p image_number_slider->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); image_number_slider->setMinimumWidth(140); image_number_slider->setToolTip("Scrub through the dataset"); - image_number_slider->setStyleSheet( - "QSlider::groove:horizontal { height:6px; background:#F3D9D4; border-radius:3px; }" - "QSlider::sub-page:horizontal { background:#FA7268; border-radius:3px; }" - "QSlider::handle:horizontal { background:#1F3A5F; width:14px; margin:-5px 0; border-radius:7px; }" - "QSlider::handle:horizontal:hover { background:#16314F; }"); + SetThemedStyleSheet(image_number_slider, SliderStyle); image_number_slider_timer = new QTimer(this); image_number_slider_timer->setSingleShot(true); diff --git a/viewer/widgets/CollapsibleSection.cpp b/viewer/widgets/CollapsibleSection.cpp index f60602089..e7eb2a130 100644 --- a/viewer/widgets/CollapsibleSection.cpp +++ b/viewer/widgets/CollapsibleSection.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-only #include "CollapsibleSection.h" +#include "../ViewerTheme.h" #include #include @@ -18,10 +19,12 @@ CollapsibleSection::CollapsibleSection(const QString &title, QWidget *parent) : header_->setArrowType(Qt::RightArrow); header_->setCursor(Qt::PointingHandCursor); header_->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); - header_->setStyleSheet( - "QToolButton { color:#1F3A5F; font-weight:bold; text-align:left;" - " padding:3px 6px 3px 6px; border:none;" - " border-left:3px solid #FA7268; border-bottom:1px solid #FA7268; background:transparent; }"); + SetThemedStyleSheet(header_, [] { + return QString("QToolButton { color:%1; font-weight:bold; text-align:left;" + " padding:3px 6px 3px 6px; border:none;" + " border-left:3px solid #FA7268; border-bottom:1px solid #FA7268; background:transparent; }") + .arg(AccentTextColor().name()); + }); content_ = new QWidget(this); content_->setVisible(false); diff --git a/viewer/widgets/JFJochViewerImageStatistics.cpp b/viewer/widgets/JFJochViewerImageStatistics.cpp index 51ae89045..a1b816586 100644 --- a/viewer/widgets/JFJochViewerImageStatistics.cpp +++ b/viewer/widgets/JFJochViewerImageStatistics.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-only #include "JFJochViewerImageStatistics.h" +#include "../ViewerTheme.h" #include "../../common/time_utc.h" #include #include @@ -403,7 +404,7 @@ void JFJochViewerImageStatistics::loadImage(std::shared_ptrImageData().indexing_lattice; if (latt) { - text = mkUnitCell(latt->GetUnitCell(), "purple"); + text = mkUnitCell(latt->GetUnitCell(), DarkTheme() ? "violet" : "purple"); auto vec0 = latt->Vec0(); auto vec1 = latt->Vec1(); diff --git a/viewer/widgets/JFJochViewerROIList.cpp b/viewer/widgets/JFJochViewerROIList.cpp index 2fbdc395e..78800a731 100644 --- a/viewer/widgets/JFJochViewerROIList.cpp +++ b/viewer/widgets/JFJochViewerROIList.cpp @@ -54,7 +54,6 @@ JFJochViewerROIList::JFJochViewerROIList(QWidget *parent) : QWidget(parent) { fedit_[i] = new QLineEdit(this); fedit_[i]->setAlignment(Qt::AlignRight); fedit_[i]->setMaximumWidth(90); - fedit_[i]->setStyleSheet("background-color: white; color: black;"); grid->addWidget(flabel_[i], i, 0); grid->addWidget(fedit_[i], i, 1, Qt::AlignRight); connect(fedit_[i], &QLineEdit::editingFinished, this, &JFJochViewerROIList::ApplyEditor); diff --git a/viewer/widgets/NumberLineEdit.cpp b/viewer/widgets/NumberLineEdit.cpp index de6939e5a..8727ac0e2 100644 --- a/viewer/widgets/NumberLineEdit.cpp +++ b/viewer/widgets/NumberLineEdit.cpp @@ -13,7 +13,6 @@ NumberLineEdit::NumberLineEdit(float minVal, , decimalCount(decimals) , savedValue(def_value) { - setStyleSheet("background-color: rgb(255, 255, 255);"); setRange(minVal, maxVal); setDecimals(decimalCount); setButtonSymbols(QAbstractSpinBox::ButtonSymbols::NoButtons); diff --git a/viewer/widgets/NumericComboBox.cpp b/viewer/widgets/NumericComboBox.cpp index 688dfb155..caf7159bd 100644 --- a/viewer/widgets/NumericComboBox.cpp +++ b/viewer/widgets/NumericComboBox.cpp @@ -16,8 +16,6 @@ NumericComboBox::NumericComboBox(std::vector options, int min, int max, QWi setCurrentText(QString::number(min)); - setStyleSheet("background-color: rgb(255, 255, 255);"); - connect(this, QOverload::of(&QComboBox::currentIndexChanged), this, &NumericComboBox::onCurrentIndexChanged); connect(this, &QComboBox::editTextChanged, @@ -35,7 +33,7 @@ void NumericComboBox::setValue(int value) { void NumericComboBox::onCurrentIndexChanged(int index) { if (index >= 0) { - setStyleSheet("background-color: rgb(255, 255, 255);"); + setStyleSheet(""); emit valueChanged(getValue()); } } @@ -44,7 +42,7 @@ void NumericComboBox::onEditTextChanged(const QString &text) { bool ok; int value = text.toInt(&ok); if (ok) { - setStyleSheet("background-color: rgb(255, 255, 255);"); + setStyleSheet(""); emit valueChanged(value); } else { setStyleSheet("background-color: rgb(125, 0, 0);"); diff --git a/viewer/widgets/SliderPlusBox.cpp b/viewer/widgets/SliderPlusBox.cpp index 98e06cd09..b81776346 100644 --- a/viewer/widgets/SliderPlusBox.cpp +++ b/viewer/widgets/SliderPlusBox.cpp @@ -33,7 +33,6 @@ SliderPlusBox::SliderPlusBox(double min, double max, double step, int decimals, m_doubleSpinBox->setDecimals(decimals); m_doubleSpinBox->setSingleStep(step); m_doubleSpinBox->setValue(v); - m_doubleSpinBox->setStyleSheet("background-color: rgb(255, 255, 255);"); m_updateTimer = new QTimer(this); m_updateTimer->setInterval(500); // 500ms throttle diff --git a/viewer/widgets/TitleLabel.cpp b/viewer/widgets/TitleLabel.cpp index 2f4ded107..fa9ffc986 100644 --- a/viewer/widgets/TitleLabel.cpp +++ b/viewer/widgets/TitleLabel.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-only #include "TitleLabel.h" +#include "../ViewerTheme.h" TitleLabel::TitleLabel(QString text, QWidget *parent) : QLabel(parent) { // Slim section header: navy bold text with a coral accent rule, in place of a heavy 50px @@ -10,12 +11,14 @@ TitleLabel::TitleLabel(QString text, QWidget *parent) : QLabel(parent) { QFont f = font(); f.setBold(true); setFont(f); - setStyleSheet("QLabel {" - " color: #1F3A5F;" - " background-color: transparent;" - " border-left: 3px solid #FA7268;" - " border-bottom: 1px solid #FA7268;" - " padding: 3px 6px 3px 8px; }"); + SetThemedStyleSheet(this, [] { + return QString("QLabel {" + " color: %1;" + " background-color: transparent;" + " border-left: 3px solid #FA7268;" + " border-bottom: 1px solid #FA7268;" + " padding: 3px 6px 3px 8px; }").arg(AccentTextColor().name()); + }); setAlignment(Qt::AlignLeft | Qt::AlignVCenter); setFixedHeight(26); } diff --git a/viewer/widgets/ToolbarIcons.cpp b/viewer/widgets/ToolbarIcons.cpp index 3a5241ee7..b5d8b38db 100644 --- a/viewer/widgets/ToolbarIcons.cpp +++ b/viewer/widgets/ToolbarIcons.cpp @@ -2,7 +2,9 @@ // SPDX-License-Identifier: GPL-3.0-only #include "ToolbarIcons.h" +#include "../ViewerTheme.h" +#include #include #include #include @@ -10,28 +12,49 @@ namespace { constexpr int S = 32; - const QColor INK("#2B2B2B"); const QColor DISABLED("#9AA6B3"); using DrawFn = std::function; - QPixmap render(const DrawFn &fn, const QColor &color) { - QPixmap pm(S, S); + // The glyphs are drawn on an S x S canvas; the painter is scaled so they come out sharp at any size. + QPixmap render(const DrawFn &fn, const QColor &color, const QSize &size) { + QPixmap pm(size); pm.fill(Qt::transparent); QPainter p(&pm); p.setRenderHint(QPainter::Antialiasing); + p.scale(size.width() / double(S), size.height() / double(S)); fn(p, color); return pm; } - // Toggle-capable icon: ink when off, white when on (checked, navy background), grey when disabled. + // Draws the glyph when the icon is painted rather than when it is made, so the ink follows the + // theme: ink when off, white when on (a toggle on its navy checked background), grey when disabled. + class InkIconEngine final : public QIconEngine { + public: + InkIconEngine(DrawFn fn, bool toggle) : fn_(std::move(fn)), toggle_(toggle) {} + + void paint(QPainter *painter, const QRect &rect, QIcon::Mode mode, QIcon::State state) override { + painter->drawPixmap(rect, pixmap(rect.size(), mode, state)); + } + + QPixmap pixmap(const QSize &size, QIcon::Mode mode, QIcon::State state) override { + QColor color = IconInkColor(); + if (mode == QIcon::Disabled) + color = DISABLED; + else if (toggle_ && state == QIcon::On) + color = Qt::white; + return render(fn_, color, size); + } + + QIconEngine *clone() const override { return new InkIconEngine(fn_, toggle_); } + + private: + DrawFn fn_; + bool toggle_; + }; + QIcon toggleIcon(const DrawFn &fn) { - QIcon ic; - ic.addPixmap(render(fn, INK), QIcon::Normal, QIcon::Off); - ic.addPixmap(render(fn, Qt::white), QIcon::Normal, QIcon::On); - ic.addPixmap(render(fn, Qt::white), QIcon::Active, QIcon::On); - ic.addPixmap(render(fn, DISABLED), QIcon::Disabled, QIcon::Off); - return ic; + return QIcon(new InkIconEngine(fn, true)); } void fillTriangle(QPainter &p, const QColor &c, QPointF a, QPointF b, QPointF tip) { @@ -180,19 +203,16 @@ QIcon httpSync(HttpState state) { p.setBrush(dot); p.drawEllipse(QPointF(24, 24), 4.2, 4.2); // status dot }; - QIcon ic; - ic.addPixmap(render(draw, INK), QIcon::Normal, QIcon::Off); - ic.addPixmap(render(draw, DISABLED), QIcon::Disabled, QIcon::Off); - return ic; + return QIcon(new InkIconEngine(draw, false)); } QString buttonStyle() { - return QStringLiteral( - "QToolButton { border:none; border-radius:4px; padding:4px 6px; color:#2B2B2B;" + return QString( + "QToolButton { border:none; border-radius:4px; padding:4px 6px; color:%1;" " background:transparent; }" " QToolButton:hover { background:rgba(250,114,104,0.20); }" " QToolButton:checked { background:#1F3A5F; color:white; }" - " QToolButton:disabled { color:#9AA6B3; }"); + " QToolButton:disabled { color:#9AA6B3; }").arg(IconInkColor().name()); } } // namespace ToolbarIcons diff --git a/viewer/windows/JFJochCalibrationResultWindow.cpp b/viewer/windows/JFJochCalibrationResultWindow.cpp index 33f042077..afaf1e56d 100644 --- a/viewer/windows/JFJochCalibrationResultWindow.cpp +++ b/viewer/windows/JFJochCalibrationResultWindow.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-only #include "JFJochCalibrationResultWindow.h" +#include "../ViewerTheme.h" #include #include @@ -27,7 +28,7 @@ namespace { f.setPointSizeF(f.pointSizeF() * 2.2); f.setBold(true); v->setFont(f); - v->setStyleSheet("color: #1F3A5F;"); + SetThemedStyleSheet(v, [] { return "color: " + AccentTextColor().name() + ";"; }); v->setAlignment(Qt::AlignCenter); auto *c = new QLabel(caption, card); c->setStyleSheet("color: gray;"); diff --git a/viewer/windows/JFJochMergeStatsWindow.cpp b/viewer/windows/JFJochMergeStatsWindow.cpp index d04de09a0..db7c50401 100644 --- a/viewer/windows/JFJochMergeStatsWindow.cpp +++ b/viewer/windows/JFJochMergeStatsWindow.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-only #include "JFJochMergeStatsWindow.h" +#include "../ViewerTheme.h" #include #include @@ -57,7 +58,7 @@ namespace { f.setPointSizeF(f.pointSizeF() * 2.2); f.setBold(true); v->setFont(f); - v->setStyleSheet("color: #1F3A5F;"); + SetThemedStyleSheet(v, [] { return "color: " + AccentTextColor().name() + ";"; }); v->setAlignment(Qt::AlignCenter); auto *c = new QLabel(caption, card); c->setStyleSheet("color: gray;"); @@ -170,7 +171,10 @@ JFJochMergeStatsWindow::JFJochMergeStatsWindow(const QString &title, const Merge auto *banner = new QLabel(text, this); banner->setContentsMargins(10, 6, 10, 6); if (twinning.twinning_suspected) - banner->setStyleSheet("color: rgb(160,0,0); border: 1px solid red; border-radius: 4px;"); + SetThemedStyleSheet(banner, [] { + return QString("color: %1; border: 1px solid red; border-radius: 4px;") + .arg(DarkTheme() ? "#FF7B72" : "rgb(160,0,0)"); + }); else banner->setStyleSheet("color: gray;"); layout->addWidget(banner); @@ -183,22 +187,22 @@ JFJochMergeStatsWindow::JFJochMergeStatsWindow(const QString &title, const Merge plotBtn->setCheckable(true); plotBtn->setChecked(true); plotBtn->setToolTip("Per-resolution plot"); - plotBtn->setStyleSheet(ToolbarIcons::buttonStyle()); + SetThemedStyleSheet(plotBtn, ToolbarIcons::buttonStyle); auto *tableBtn = new QToolButton(this); tableBtn->setIcon(ToolbarIcons::table()); tableBtn->setCheckable(true); tableBtn->setToolTip("Per-shell table"); - tableBtn->setStyleSheet(ToolbarIcons::buttonStyle()); + SetThemedStyleSheet(tableBtn, ToolbarIcons::buttonStyle); auto *isaBtn = new QToolButton(this); isaBtn->setIcon(ToolbarIcons::linePlot()); isaBtn->setCheckable(true); isaBtn->setToolTip("ISa diagnostic: merged I/σ against I"); - isaBtn->setStyleSheet(ToolbarIcons::buttonStyle()); + SetThemedStyleSheet(isaBtn, ToolbarIcons::buttonStyle); auto *reportBtn = new QToolButton(this); reportBtn->setText("Report"); reportBtn->setCheckable(true); reportBtn->setToolTip("The rugnux result report, as written to _report.txt"); - reportBtn->setStyleSheet(ToolbarIcons::buttonStyle()); + SetThemedStyleSheet(reportBtn, ToolbarIcons::buttonStyle); auto *viewGroup = new QButtonGroup(this); viewGroup->setExclusive(true); viewGroup->addButton(plotBtn, 0); @@ -277,7 +281,9 @@ QWidget *JFJochMergeStatsWindow::buildIsaDiagnostic(const std::vectorsetName("merged reflections"); points->setMarkerSize(4.0); points->setBorderColor(Qt::transparent); - points->setColor(QColor(0x1F, 0x3A, 0x5F, 0x60)); + QColor point_color = AccentTextColor(); + point_color.setAlpha(0x60); + points->setColor(point_color); double i_lo = std::numeric_limits::max(), i_hi = 0.0, r_hi = 0.0; for (const auto &[I, sigma] : i_sigma) { @@ -295,6 +301,7 @@ QWidget *JFJochMergeStatsWindow::buildIsaDiagnostic(const std::vectorlegend()->setVisible(false); chart->addSeries(points); diff --git a/viewer/windows/JFJochViewerImageListWindow.cpp b/viewer/windows/JFJochViewerImageListWindow.cpp index 10b13c4fd..909117cde 100644 --- a/viewer/windows/JFJochViewerImageListWindow.cpp +++ b/viewer/windows/JFJochViewerImageListWindow.cpp @@ -223,7 +223,7 @@ void JFJochViewerImageListWindow::imageLoaded(std::shared_ptrverticalHeader()->setVisible(false); tableView->horizontalHeader()->setSectionsClickable(true); tableView->horizontalHeader()->setSortIndicatorShown(true); - tableView->setStyleSheet("background-color: white;"); tableView->setSelectionBehavior(QAbstractItemView::SelectRows); tableView->setSelectionMode(QAbstractItemView::SingleSelection); diff --git a/viewer/windows/JFJochViewerSpotListWindow.cpp b/viewer/windows/JFJochViewerSpotListWindow.cpp index 300a85984..2ab0aaedf 100644 --- a/viewer/windows/JFJochViewerSpotListWindow.cpp +++ b/viewer/windows/JFJochViewerSpotListWindow.cpp @@ -29,7 +29,6 @@ JFJochViewerSpotListWindow::JFJochViewerSpotListWindow(QWidget* parent) tableView->verticalHeader()->setVisible(false); tableView->horizontalHeader()->setSectionsClickable(true); tableView->horizontalHeader()->setSortIndicatorShown(true); - tableView->setStyleSheet("background-color: white;"); tableView->setSelectionBehavior(QAbstractItemView::SelectRows); tableView->setSelectionMode(QAbstractItemView::SingleSelection);