diff --git a/viewer/JFJochViewerMenu.cpp b/viewer/JFJochViewerMenu.cpp index 1c5d06305..c45523c14 100644 --- a/viewer/JFJochViewerMenu.cpp +++ b/viewer/JFJochViewerMenu.cpp @@ -105,6 +105,9 @@ JFJochViewerMenu::JFJochViewerMenu(QWidget *parent) : QMenuBar(parent) { action->setChecked(choice == currentTheme); themeGroup->addAction(action); connect(action, &QAction::triggered, this, [choice] { SetThemeChoice(choice); }); + // The display toolbar switches the theme too; keep the tick on the choice in force. + connect(ThemeNotifier::Instance(), &ThemeNotifier::changed, action, + [action, choice] { action->setChecked(LoadThemeChoice() == choice); }); } viewMenu->addSeparator(); @@ -117,11 +120,11 @@ JFJochViewerMenu::JFJochViewerMenu(QWidget *parent) : QMenuBar(parent) { fontLarger->setShortcuts({QKeySequence::ZoomIn, QKeySequence(Qt::ControlModifier | Qt::Key_Equal), QKeySequence(Qt::ControlModifier | Qt::KeypadModifier | Qt::Key_Plus)}); - connect(fontLarger, &QAction::triggered, this, [this] { ApplyFontZoom(fontZoom + 25); }); + connect(fontLarger, &QAction::triggered, this, [this] { SetFontZoom(fontZoom + 25); }); QAction *fontSmaller = fontMenu->addAction("Smaller"); fontSmaller->setShortcuts({QKeySequence::ZoomOut, QKeySequence(Qt::ControlModifier | Qt::KeypadModifier | Qt::Key_Minus)}); - connect(fontSmaller, &QAction::triggered, this, [this] { ApplyFontZoom(fontZoom - 25); }); + connect(fontSmaller, &QAction::triggered, this, [this] { SetFontZoom(fontZoom - 25); }); fontMenu->addSeparator(); fontZoomGroup = new QActionGroup(this); for (int percent: {100, 125, 150}) { @@ -129,9 +132,9 @@ JFJochViewerMenu::JFJochViewerMenu(QWidget *parent) : QMenuBar(parent) { level->setCheckable(true); level->setData(percent); fontZoomGroup->addAction(level); - connect(level, &QAction::triggered, this, [this, percent] { ApplyFontZoom(percent); }); + connect(level, &QAction::triggered, this, [this, percent] { SetFontZoom(percent); }); } - ApplyFontZoom(QSettings("PSI", "jfjoch_viewer").value("fontZoom", 100).toInt()); + SetFontZoom(QSettings("PSI", "jfjoch_viewer").value("fontZoom", 100).toInt()); QMenu *helpMenu = addMenu("Help"); // Add "About" action @@ -149,7 +152,7 @@ JFJochViewerMenu::JFJochViewerMenu(QWidget *parent) : QMenuBar(parent) { } // Three levels, 25 % apart: clamping here is what stops the two stepping actions at the ends. -void JFJochViewerMenu::ApplyFontZoom(int percent) { +void JFJochViewerMenu::SetFontZoom(int percent) { fontZoom = std::clamp(percent, 100, 150); QFont font = baseFont; font.setPointSizeF(baseFont.pointSizeF() * fontZoom / 100.0); @@ -162,10 +165,11 @@ void JFJochViewerMenu::ApplyFontZoom(int percent) { QSettings("PSI", "jfjoch_viewer").setValue("fontZoom", fontZoom); for (QAction *level: fontZoomGroup->actions()) level->setChecked(level->data().toInt() == fontZoom); + emit fontZoomChanged(fontZoom); } void JFJochViewerMenu::ResetFontZoom() { - ApplyFontZoom(100); + SetFontZoom(100); } void JFJochViewerMenu::AddWindowEntry(JFJochHelperWindow *window, const QString &name) { diff --git a/viewer/JFJochViewerMenu.h b/viewer/JFJochViewerMenu.h index b55140d8c..5825c1c18 100644 --- a/viewer/JFJochViewerMenu.h +++ b/viewer/JFJochViewerMenu.h @@ -39,6 +39,9 @@ public: void AddDockEntry(QDockWidget *dock, const QString &name); // Back to 100 %, for "Reset all settings to defaults". void ResetFontZoom(); + // 100, 125 or 150 per cent; also reachable from the display toolbar's size buttons. + void SetFontZoom(int percent); + [[nodiscard]] int FontZoom() const { return fontZoom; } public slots: void openSelected(); // also reachable from the toolbar's open-dataset button @@ -58,6 +61,7 @@ signals: void processingLayoutSelected(); void resetLayoutSelected(); void resetSettingsSelected(); + void fontZoomChanged(int percent); private slots: void aboutSelected(); @@ -72,5 +76,4 @@ private slots: private: void openLoadUserMaskTiff(bool replace); - void ApplyFontZoom(int percent); }; diff --git a/viewer/JFJochViewerWindow.cpp b/viewer/JFJochViewerWindow.cpp index 0087d63f7..4dc9eaeb7 100644 --- a/viewer/JFJochViewerWindow.cpp +++ b/viewer/JFJochViewerWindow.cpp @@ -89,6 +89,10 @@ JFJochViewerWindow::JFJochViewerWindow(QWidget *parent, bool dbus, const QString toolBarDisplay = new JFJochViewerToolbarDisplay(this); toolBarDisplay->setObjectName("toolBarDisplay"); addToolBar(Qt::TopToolBarArea, toolBarDisplay); + // The font size lives in the menu; the toolbar's "A" buttons are a second way to it. + connect(toolBarDisplay, &JFJochViewerToolbarDisplay::fontZoomSelected, menuBar, &JFJochViewerMenu::SetFontZoom); + connect(menuBar, &JFJochViewerMenu::fontZoomChanged, toolBarDisplay, &JFJochViewerToolbarDisplay::updateFontZoom); + toolBarDisplay->updateFontZoom(menuBar->FontZoom()); statusbar = new JFJochViewerStatusBar(this); setStatusBar(statusbar); diff --git a/viewer/ViewerTheme.cpp b/viewer/ViewerTheme.cpp index 02361d54c..a715ed20d 100644 --- a/viewer/ViewerTheme.cpp +++ b/viewer/ViewerTheme.cpp @@ -71,7 +71,21 @@ namespace { 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. diff --git a/viewer/toolbar/JFJochViewerToolbarDisplay.cpp b/viewer/toolbar/JFJochViewerToolbarDisplay.cpp index a0f3e91c8..33893b9db 100644 --- a/viewer/toolbar/JFJochViewerToolbarDisplay.cpp +++ b/viewer/toolbar/JFJochViewerToolbarDisplay.cpp @@ -89,6 +89,43 @@ JFJochViewerToolbarDisplay::JFJochViewerToolbarDisplay(QWidget *parent) auto *stretch = new QWidget(this); stretch->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); addWidget(stretch); + + auto makeIconButton = [this](const QIcon &icon, const QString &tip, bool checkable) { + auto *b = new QToolButton(this); + b->setIcon(icon); + b->setIconSize(QSize(20, 20)); + b->setToolTip(tip); + b->setCheckable(checkable); + b->setCursor(Qt::PointingHandCursor); + SetThemedStyleSheet(b, ToolbarIcons::buttonStyle); + return b; + }; + font_zoom_group = new QButtonGroup(this); + font_zoom_group->setExclusive(true); + for (int step = 0; step < 3; step++) { + const int percent = 100 + 25 * step; + auto *b = makeIconButton(ToolbarIcons::letterA(step), QString("Font size %1 %").arg(percent), true); + font_zoom_group->addButton(b, percent); + addWidget(b); + } + connect(font_zoom_group, &QButtonGroup::idClicked, this, &JFJochViewerToolbarDisplay::fontZoomSelected); + addSeparator(); + + theme_button = makeIconButton(ToolbarIcons::halfSun(), QString(), false); + addWidget(theme_button); + connect(theme_button, &QToolButton::clicked, this, + [] { SetThemeChoice(DarkTheme() ? ThemeChoice::Light : ThemeChoice::Dark); }); + connect(ThemeNotifier::Instance(), &ThemeNotifier::changed, this, &JFJochViewerToolbarDisplay::UpdateThemeButton); + UpdateThemeButton(); +} + +void JFJochViewerToolbarDisplay::UpdateThemeButton() { + theme_button->setToolTip(DarkTheme() ? "Switch to the light theme" : "Switch to the dark theme"); +} + +void JFJochViewerToolbarDisplay::updateFontZoom(int percent) { + if (auto *b = font_zoom_group->button(percent)) + b->setChecked(true); } void JFJochViewerToolbarDisplay::foregroundSet(double val) { diff --git a/viewer/toolbar/JFJochViewerToolbarDisplay.h b/viewer/toolbar/JFJochViewerToolbarDisplay.h index 0f41efe33..0606f0ff7 100644 --- a/viewer/toolbar/JFJochViewerToolbarDisplay.h +++ b/viewer/toolbar/JFJochViewerToolbarDisplay.h @@ -7,13 +7,14 @@ #include #include #include "../widgets/SliderPlusBox.h" +#include #include #include #include "../../reader/JFJochReaderImage.h" // Display toolbar: how the image is rendered — foreground (white-point) and background -// (black-point) sliders, auto-contrast, HDR (full dynamic range), and the colour map. The two -// "Reanalyze" hero buttons are appended to this bar by the main window. +// (black-point) sliders, auto-contrast, HDR (full dynamic range), and the colour map; at its right +// end the font size and the light/dark switch, which are also in the View menu but easy to miss there. class JFJochViewerToolbarDisplay : public QToolBar { Q_OBJECT @@ -24,6 +25,9 @@ class JFJochViewerToolbarDisplay : public QToolBar { QToolButton *auto_foreground_button; QToolButton *hdr_mode_button; QComboBox *color_map_select; + QButtonGroup *font_zoom_group; // three "A" buttons; the id is the zoom in per cent + QToolButton *theme_button; // light/dark switch; its tooltip names the theme it switches to + void UpdateThemeButton(); // A ctrl-wheel or auto-foreground burst reports a new foreground per event, and on a remote // display each slider update is a repaint of its own. Hold the readback to the remote @@ -51,6 +55,7 @@ signals: void colorMapChanged(int val); void setAutoForeground(bool val); void setHDRMode(bool val); + void fontZoomSelected(int percent); public slots: void updateForeground(float val); @@ -58,6 +63,7 @@ public slots: void updateAutoForeground(bool val); void updateColorMap(int val); void updateHDRMode(bool val); + void updateFontZoom(int percent); void imageLoaded(std::shared_ptr image); private slots: void foregroundSet(double val); diff --git a/viewer/toolbar/JFJochViewerToolbarImage.cpp b/viewer/toolbar/JFJochViewerToolbarImage.cpp index c9b12068f..17403d372 100644 --- a/viewer/toolbar/JFJochViewerToolbarImage.cpp +++ b/viewer/toolbar/JFJochViewerToolbarImage.cpp @@ -60,7 +60,7 @@ JFJochViewerToolbarImage::JFJochViewerToolbarImage(QWidget *parent) : QToolBar(p current_image_edit->setValidator(current_image_edit_validator); current_image_edit->setToolTip("Current image — type a number to jump"); - total_number_label = new QLabel("/0", this); + total_number_label = new QLabel("/ 0", this); right_button = makeButton(ToolbarIcons::next(), "Next (by jump)", false); rightmost_button = makeButton(ToolbarIcons::last(), "Last image", false); @@ -133,7 +133,7 @@ void JFJochViewerToolbarImage::updateButtons() { if (image_count_in_dataset == 0) total_number_label->setText("/ - "); else - total_number_label->setText("/" + QString::number(image_count_in_dataset)); + total_number_label->setText("/ " + QString::number(image_count_in_dataset)); } void JFJochViewerToolbarImage::updateHttpButton() { diff --git a/viewer/widgets/ToolbarIcons.cpp b/viewer/widgets/ToolbarIcons.cpp index b5d8b38db..fe41df07d 100644 --- a/viewer/widgets/ToolbarIcons.cpp +++ b/viewer/widgets/ToolbarIcons.cpp @@ -8,7 +8,9 @@ #include #include #include +#include #include +#include namespace { constexpr int S = 32; @@ -190,6 +192,43 @@ QIcon table() { }); } +QIcon halfSun() { + return toggleIcon([](QPainter &p, const QColor &c) { + // A sun whose disc is half filled, split along the diagonal: light and dark in one glyph. + p.setPen(QPen(c, 2.0)); + p.setBrush(Qt::NoBrush); + p.drawEllipse(QPointF(16, 16), 7, 7); + QPainterPath disc, half; + disc.addEllipse(QPointF(16, 16), 7, 7); + half.addPolygon(QPolygonF({QPointF(32, 0), QPointF(32, 32), QPointF(0, 32)})); + p.setPen(Qt::NoPen); + p.setBrush(c); + p.drawPath(disc.intersected(half)); + p.setPen(QPen(c, 2.2, Qt::SolidLine, Qt::RoundCap)); + for (int i = 0; i < 8; i++) { + const double a = i * std::numbers::pi / 4.0; + p.drawLine(QPointF(16 + 10.5 * std::cos(a), 16 + 10.5 * std::sin(a)), + QPointF(16 + 13.5 * std::cos(a), 16 + 13.5 * std::sin(a))); + } + }); +} + +QIcon letterA(int size_step) { + return toggleIcon([size_step](QPainter &p, const QColor &c) { + // A bold "A" at 16, 21 or 26 units; all three stand on the same baseline, so side by side + // they read as a ladder of sizes. + QFont font; + font.setBold(true); + font.setPixelSize(16 + 5 * size_step); + QPainterPath path; + path.addText(0, 0, font, "A"); + const QRectF r = path.boundingRect(); + p.setPen(Qt::NoPen); + p.setBrush(c); + p.drawPath(path.translated(16 - r.center().x(), 28 - r.bottom())); + }); +} + QIcon httpSync(HttpState state) { QColor dot; switch (state) { diff --git a/viewer/widgets/ToolbarIcons.h b/viewer/widgets/ToolbarIcons.h index 74b49391a..04514a2d7 100644 --- a/viewer/widgets/ToolbarIcons.h +++ b/viewer/widgets/ToolbarIcons.h @@ -19,6 +19,8 @@ namespace ToolbarIcons { QIcon reanalyzeDataset(); // a stack of frames QIcon linePlot(); // line chart on axes (per-resolution plot view) QIcon table(); // grid of rows/columns (table view) + QIcon halfSun(); // light/dark theme switch: a sun with a half-filled disc + QIcon letterA(int size_step); // font size 0 (small), 1, 2 (large) enum class HttpState { Disconnected, Live, Frozen }; QIcon httpSync(HttpState state); // sync arrows + a status dot (grey / green / amber)