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>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
f1a8c1d868
commit
0be8eb2bca
@@ -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
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
#include "JFJochViewerMenu.h"
|
||||
#include "RemoteDisplayMode.h"
|
||||
#include "ViewerTheme.h"
|
||||
|
||||
#include <QMessageBox>
|
||||
#include <QApplication>
|
||||
@@ -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<ThemeChoice, QString> 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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
// 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 ¬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<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));
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <QColor>
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
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<QString()> &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);
|
||||
@@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
#include <QMenu>
|
||||
#include "../ViewerTheme.h"
|
||||
#include <QApplication>
|
||||
#include <QClipboard>
|
||||
#include <QCategoryAxis>
|
||||
@@ -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<double>::infinity();
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
#include "JFJochSimpleChartView.h"
|
||||
#include "../ViewerTheme.h"
|
||||
|
||||
#include <QMenu>
|
||||
#include <QClipboard>
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
#include "JFJochViewerToolbarDisplay.h"
|
||||
#include "../ViewerTheme.h"
|
||||
#include <QLabel>
|
||||
#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");
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
#include "JFJochViewerToolbarImage.h"
|
||||
#include "../ViewerTheme.h"
|
||||
#include <algorithm>
|
||||
#include <QTimer>
|
||||
#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);
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
#include "CollapsibleSection.h"
|
||||
#include "../ViewerTheme.h"
|
||||
|
||||
#include <QToolButton>
|
||||
#include <QVBoxLayout>
|
||||
@@ -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);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
#include "JFJochViewerImageStatistics.h"
|
||||
#include "../ViewerTheme.h"
|
||||
#include "../../common/time_utc.h"
|
||||
#include <QFormLayout>
|
||||
#include <QRegularExpression>
|
||||
@@ -403,7 +404,7 @@ void JFJochViewerImageStatistics::loadImage(std::shared_ptr<const JFJochReaderIm
|
||||
|
||||
auto latt = image->ImageData().indexing_lattice;
|
||||
if (latt) {
|
||||
text = mkUnitCell(latt->GetUnitCell(), "purple");
|
||||
text = mkUnitCell(latt->GetUnitCell(), DarkTheme() ? "violet" : "purple");
|
||||
|
||||
auto vec0 = latt->Vec0();
|
||||
auto vec1 = latt->Vec1();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -16,8 +16,6 @@ NumericComboBox::NumericComboBox(std::vector<int> options, int min, int max, QWi
|
||||
|
||||
setCurrentText(QString::number(min));
|
||||
|
||||
setStyleSheet("background-color: rgb(255, 255, 255);");
|
||||
|
||||
connect(this, QOverload<int>::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);");
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
#include "ToolbarIcons.h"
|
||||
#include "../ViewerTheme.h"
|
||||
|
||||
#include <QIconEngine>
|
||||
#include <QPainter>
|
||||
#include <QPainterPath>
|
||||
#include <QPolygonF>
|
||||
@@ -10,28 +12,49 @@
|
||||
|
||||
namespace {
|
||||
constexpr int S = 32;
|
||||
const QColor INK("#2B2B2B");
|
||||
const QColor DISABLED("#9AA6B3");
|
||||
|
||||
using DrawFn = std::function<void(QPainter &, const QColor &)>;
|
||||
|
||||
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
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
#include "JFJochCalibrationResultWindow.h"
|
||||
#include "../ViewerTheme.h"
|
||||
|
||||
#include <QFrame>
|
||||
#include <QHBoxLayout>
|
||||
@@ -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;");
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
#include "JFJochMergeStatsWindow.h"
|
||||
#include "../ViewerTheme.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
@@ -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 <prefix>_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::vector<std::pair<
|
||||
points->setName("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<double>::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::vector<std::pair<
|
||||
}
|
||||
|
||||
auto *chart = new QChart();
|
||||
StyleChart(chart);
|
||||
chart->legend()->setVisible(false);
|
||||
chart->addSeries(points);
|
||||
|
||||
|
||||
@@ -223,7 +223,7 @@ void JFJochViewerImageListWindow::imageLoaded(std::shared_ptr<const JFJochReader
|
||||
|
||||
// Only the row losing the highlight and the row gaining it change. Repainting every cell
|
||||
// emitted dataChanged for the whole model - and a proxy re-sort - on each frame.
|
||||
setRowBackground(highlighted_row, Qt::white);
|
||||
setRowBackground(highlighted_row, QBrush()); // back to the palette's own
|
||||
setRowBackground(curr_row, Qt::blue);
|
||||
highlighted_row = curr_row;
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@ JFJochViewerReflectionListWindow::JFJochViewerReflectionListWindow(QWidget* pare
|
||||
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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user