From 9f369567f13dfd677b63a9e8e5f15aad98d262ad Mon Sep 17 00:00:00 2001 From: Filip Leonarski Date: Sat, 26 Sep 2026 17:43:22 +0200 Subject: [PATCH] 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 --- viewer/JFJochViewerMenu.cpp | 16 +++++--- viewer/JFJochViewerMenu.h | 5 ++- viewer/JFJochViewerWindow.cpp | 4 ++ viewer/ViewerTheme.cpp | 14 +++++++ viewer/toolbar/JFJochViewerToolbarDisplay.cpp | 37 ++++++++++++++++++ viewer/toolbar/JFJochViewerToolbarDisplay.h | 10 ++++- viewer/toolbar/JFJochViewerToolbarImage.cpp | 4 +- viewer/widgets/ToolbarIcons.cpp | 39 +++++++++++++++++++ viewer/widgets/ToolbarIcons.h | 2 + 9 files changed, 120 insertions(+), 11 deletions(-) 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)