Viewer: font size and theme buttons on the display toolbar, matching title bar

The right end of the display toolbar now carries three "A" buttons for the
100/125/150 % font sizes and a light/dark switch (a sun with a half-filled
disc), so both settings are visible without opening the View menu. The size
buttons and View > Font size stay in sync through JFJochViewerMenu's new
SetFontZoom / fontZoomChanged; the View > Theme tick follows ThemeNotifier,
so it moves when the toolbar switches the theme.

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

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Filip LeonarskiandClaude Opus 5.5 committed 2026-09-26 17:51:50 +02:00
1 parent 0be8eb2bca
commit 9f369567f1
9 files changed
+120 -11

No files matched your search

+10 -6
View File
@@ -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) {
+4 -1
View File
@@ -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);
};
+4
View File
@@ -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);
+14
View File
@@ -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.
@@ -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) {
+8 -2
View File
@@ -7,13 +7,14 @@
#include <QElapsedTimer>
#include <QTimer>
#include "../widgets/SliderPlusBox.h"
#include <QButtonGroup>
#include <QToolButton>
#include <QComboBox>
#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<const JFJochReaderImage> image);
private slots:
void foregroundSet(double val);
+2 -2
View File
@@ -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() {
+39
View File
@@ -8,7 +8,9 @@
#include <QPainter>
#include <QPainterPath>
#include <QPolygonF>
#include <cmath>
#include <functional>
#include <numbers>
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) {
+2
View File
@@ -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)