viewer: toolbar rework — icons, prominent scrubber, HTTP states (g–k)

Complete pass over both toolbars (review items g–k), closing the toolbar topic.

New ToolbarIcons: crisp QPainter vector glyphs (Qt SVG is not a build dep), with
white "on" variants for toggles and a shared flat button style (coral hover,
navy checked).

Navigation toolbar, laid out source-first:
- Open (3.5" diskette) · HTTP sync · Movie (reel-on-top camera, distinct from
  the next/play triangle) ║ the scrub slider, which expands to fill the bar
  (coral track, navy handle) so it is the obvious way to move across the data ║
  precise navigation: first/prev/[number]/next/last, Jump, Sum.
- Open and HTTP-sync reach the file / connect dialogs (JFJochViewerMenu
  openSelected and openHttpSelected, now public).
- HTTP-sync has three states via a status dot (grey disconnected / green live /
  amber frozen); clicking with no live source attached opens the connect dialog.
  Removed the separate "Reanalyze" toggle.

Display toolbar:
- Styled foreground slider (matching), Auto/HDR as styled text toggles.

Hero buttons:
- "Reanalyze image" is now a toggle (worker ReanalyzeImages: run now + keep
  re-analysing on image/settings/processing changes; coral = active).

Processing dock:
- Drop the "New job…" toolbar action (the hero button drives it) and parent the
  job dialog to the main window instead of the dock.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-23 16:26:15 +02:00
co-authored by Claude Opus 4.8
parent 03ac43da69
commit a7c1560dcb
10 changed files with 377 additions and 162 deletions
+2
View File
@@ -64,6 +64,8 @@ ADD_EXECUTABLE(jfjoch_viewer jfjoch_viewer.cpp JFJochViewerWindow.cpp JFJochView
widgets/JFJochViewerSettingsDock.h
widgets/CollapsibleSection.cpp
widgets/CollapsibleSection.h
widgets/ToolbarIcons.cpp
widgets/ToolbarIcons.h
windows/JFJochViewerSpotListWindow.cpp
windows/JFJochViewerSpotListWindow.h
image_viewer/JFJochAzIntImage.cpp
+4 -2
View File
@@ -29,6 +29,10 @@ public:
// Adds a dock's show/hide toggle to the Window menu (for docked panels).
void AddDockEntry(QDockWidget *dock, const QString &name);
public slots:
void openSelected(); // also reachable from the toolbar's open-dataset button
void openHttpSelected(); // also reachable from the toolbar's HTTP-sync button
signals:
void fileOpenSelected(const QString &filename, qint64 image_number, qint64 summation, bool retry);
void fileCloseSelected();
@@ -47,8 +51,6 @@ private slots:
void aboutSelected();
void licensesSelected();
void quitSelected();
void openSelected();
void openHttpSelected();
void closeSelected();
void saveUserMaskAsTiffSelected();
+16 -5
View File
@@ -168,17 +168,22 @@ JFJochViewerWindow::JFJochViewerWindow(QWidget *parent, bool dbus, const QString
"QPushButton { background-color:#1F3A5F; color:white; font-weight:bold;"
" padding:3px 10px; border:none; border-radius:3px; }"
" QPushButton:hover { background-color:#16314f; }"
" QPushButton:checked { background-color:#FA7268; }"
" QPushButton:disabled { background-color:#9aa6b3; }";
// Reanalyze image is a toggle: re-analyse now and keep re-analysing on every image /
// settings / processing change while armed (coral = active). Reanalyze dataset is one-shot.
auto *reanalyzeImageBtn = new QPushButton(FramesIcon(1), " Reanalyze image", this);
reanalyzeImageBtn->setStyleSheet(heroStyle);
reanalyzeImageBtn->setToolTip("Re-run the analysis pipeline on the current image");
reanalyzeImageBtn->setCheckable(true);
reanalyzeImageBtn->setToolTip("Re-analyse the current image now, and keep re-analysing on"
" every image / settings / processing change while active");
auto *reanalyzeDatasetBtn = new QPushButton(FramesIcon(3), " Reanalyze dataset", this);
reanalyzeDatasetBtn->setStyleSheet(heroStyle);
reanalyzeDatasetBtn->setToolTip("Re-process the whole dataset (opens a new processing job)");
toolBarDisplay->addWidget(reanalyzeImageBtn);
toolBarDisplay->addWidget(reanalyzeDatasetBtn);
connect(reanalyzeImageBtn, &QPushButton::clicked,
reading_worker, &JFJochImageReadingWorker::Analyze);
connect(reanalyzeImageBtn, &QPushButton::toggled,
reading_worker, &JFJochImageReadingWorker::ReanalyzeImages);
connect(reanalyzeDatasetBtn, &QPushButton::clicked,
processingJobsWindow, &JFJochProcessingJobsWindow::newJob);
}
@@ -313,8 +318,14 @@ JFJochViewerWindow::JFJochViewerWindow(QWidget *parent, bool dbus, const QString
reading_worker, &JFJochImageReadingWorker::setAutoLoadMode);
connect(toolBarImage, &JFJochViewerToolbarImage::imageJumpChanged,
reading_worker, &JFJochImageReadingWorker::setAutoLoadJump);
connect(toolBarImage, &JFJochViewerToolbarImage::reanalyzeImages,
reading_worker, &JFJochImageReadingWorker::ReanalyzeImages);
// HTTP-sync button: reflect the live connection, and open the connect dialog when clicked
// while no live source is attached (i.e. a plain file is open).
connect(reading_worker, &JFJochImageReadingWorker::httpConnectionChanged,
toolBarImage, &JFJochViewerToolbarImage::setHttpConnection);
connect(toolBarImage, &JFJochViewerToolbarImage::requestHttpConnect,
menuBar, &JFJochViewerMenu::openHttpSelected);
connect(toolBarImage, &JFJochViewerToolbarImage::requestOpenFile,
menuBar, &JFJochViewerMenu::openSelected);
connect(side_panel, &JFJochViewerSidePanel::analyze,
reading_worker, &JFJochImageReadingWorker::Analyze);
+25 -12
View File
@@ -4,29 +4,42 @@
#include "JFJochViewerToolbarDisplay.h"
#include <QLabel>
#include "../../common/ColorScale.h"
#include "../widgets/ToolbarIcons.h"
constexpr double MAX_SLIDER_NO_IMAGE = 65000;
JFJochViewerToolbarDisplay::JFJochViewerToolbarDisplay(QWidget *parent)
: QToolBar(parent) {
addWidget(new QLabel("<b>Foreground </b>&nbsp;&nbsp;"));
setWindowTitle("Display"); // shown in the toolbar/dock context menu
addWidget(new QLabel(" Foreground ", this));
foreground_slider = new SliderPlusBox(1, MAX_SLIDER_NO_IMAGE, 1.0, 1, this,
SliderPlusBox::ScaleType::Logarithmic);
foreground_slider->setValue(10);
foreground_slider->setToolTip("White point of the colour map (image contrast)");
foreground_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; }");
addWidget(foreground_slider);
auto_foreground_button = new QPushButton("Auto");
auto_foreground_button->setCheckable(true);
auto_foreground_button->setChecked(false);
addWidget(auto_foreground_button);
hdr_mode_button = new QPushButton("HDR");
hdr_mode_button->setCheckable(true);
hdr_mode_button->setChecked(false);
auto makeToggle = [this](const QString &text, const QString &tip) {
auto *b = new QToolButton(this);
b->setText(text);
b->setToolButtonStyle(Qt::ToolButtonTextOnly);
b->setCheckable(true);
b->setToolTip(tip);
b->setCursor(Qt::PointingHandCursor);
b->setStyleSheet(ToolbarIcons::buttonStyle());
return b;
};
auto_foreground_button = makeToggle("Auto", "Auto-contrast to the current image");
addWidget(auto_foreground_button);
hdr_mode_button = makeToggle("HDR", "High dynamic range: show the full intensity range unclipped");
addWidget(hdr_mode_button);
addWidget(new QLabel("&nbsp;&nbsp;<b>Color map</b>&nbsp;&nbsp;"));
addWidget(new QLabel(" Colour map ", this));
// Initialize QComboBox with the options
color_map_select = new QComboBox(this);
@@ -45,8 +58,8 @@ JFJochViewerToolbarDisplay::JFJochViewerToolbarDisplay(QWidget *parent)
connect(foreground_slider, &SliderPlusBox::valueChanged, this, &JFJochViewerToolbarDisplay::foregroundSet);
connect(color_map_select, QOverload<int>::of(&QComboBox::currentIndexChanged),
this, &JFJochViewerToolbarDisplay::colorComboBoxSet);
connect(auto_foreground_button, &QPushButton::clicked, this, &JFJochViewerToolbarDisplay::autoForegroundButtonPressed);
connect(hdr_mode_button, &QPushButton::clicked, this, &JFJochViewerToolbarDisplay::HDRModeButtonPressed);
connect(auto_foreground_button, &QToolButton::clicked, this, &JFJochViewerToolbarDisplay::autoForegroundButtonPressed);
connect(hdr_mode_button, &QToolButton::clicked, this, &JFJochViewerToolbarDisplay::HDRModeButtonPressed);
auto *stretch = new QWidget(this);
stretch->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
addWidget(stretch);
+6 -3
View File
@@ -5,18 +5,21 @@
#include <QToolBar>
#include "../widgets/SliderPlusBox.h"
#include <QPushButton>
#include <QToolButton>
#include <QComboBox>
#include "../../reader/JFJochReaderImage.h"
// Display toolbar: how the image is rendered — foreground (white-point) slider, auto-contrast,
// HDR (full dynamic range), and the colour map. The two "Reanalyze" hero buttons are appended
// to this bar by the main window.
class JFJochViewerToolbarDisplay : public QToolBar {
Q_OBJECT
bool auto_foreground = false;
SliderPlusBox *foreground_slider;
QPushButton *auto_foreground_button;
QPushButton *hdr_mode_button;
QToolButton *auto_foreground_button;
QToolButton *hdr_mode_button;
QComboBox *color_map_select;
public:
JFJochViewerToolbarDisplay(QWidget *parent = nullptr);
+108 -127
View File
@@ -3,100 +3,95 @@
#include "JFJochViewerToolbarImage.h"
#include <QTimer>
#include <QStyle>
#include "../widgets/ToolbarIcons.h"
JFJochViewerToolbarImage::JFJochViewerToolbarImage(QWidget *parent) : QToolBar(parent) {
setWindowTitle("Navigation"); // shown in the toolbar/dock context menu
image_count_in_dataset = 0;
leftmost_button = new QPushButton(this);
leftmost_button->setIcon(style()->standardIcon(QStyle::SP_MediaSkipBackward));
leftmost_button->setToolTip("First image");
leftmost_button->setFixedWidth(32);
addWidget(leftmost_button);
auto makeButton = [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);
b->setStyleSheet(ToolbarIcons::buttonStyle());
return b;
};
left_button = new QPushButton(this);
left_button->setIcon(style()->standardIcon(QStyle::SP_MediaSeekBackward));
left_button->setToolTip("Previous (by jump)");
left_button->setFixedWidth(32);
addWidget(left_button);
// --- create widgets ---
open_button = makeButton(ToolbarIcons::openFile(), "Open dataset…", false);
open_button->setShortcut(QKeySequence::Open);
current_image_edit_validator = new QIntValidator(0, 1000, this);
autoload_button = makeButton(ToolbarIcons::httpSync(ToolbarIcons::HttpState::Disconnected),
"Connect to a live source…", false);
autoload_button->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_S));
current_image_edit = new QLineEdit("0", this);
current_image_edit->setFixedWidth(50);
current_image_edit->setAlignment(Qt::AlignmentFlag::AlignRight);
current_image_edit->setValidator(current_image_edit_validator);
addWidget(current_image_edit);
total_number_label = new QLabel("/0", this);
addWidget(total_number_label);
right_button = new QPushButton(this);
right_button->setIcon(style()->standardIcon(QStyle::SP_MediaSeekForward));
right_button->setToolTip("Next (by jump)");
right_button->setFixedWidth(32);
addWidget(right_button);
rightmost_button = new QPushButton(this);
rightmost_button->setIcon(style()->standardIcon(QStyle::SP_MediaSkipForward));
rightmost_button->setToolTip("Last image");
rightmost_button->setFixedWidth(32);
addWidget(rightmost_button);
addWidget(new QLabel(" Jump:", this));
jump = new NumericComboBox({1, 2, 5, 10, 20, 100, 500}, 1, 999, this);
connect(jump, &NumericComboBox::valueChanged, this, &JFJochViewerToolbarImage::setImageJump);
addWidget(jump);
addWidget(new QLabel(" Sum:", this));
auto sum = new NumericComboBox({1, 2, 5, 10, 20, 50, 100}, 1, 10000, this);
connect(sum, &NumericComboBox::valueChanged, this, &JFJochViewerToolbarImage::setSummation);
addWidget(sum);
movie_button = makeButton(ToolbarIcons::movie(), "Play movie", true);
movie_button->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_M));
// The scrub slider is the prominent way to move across the dataset, so it expands to fill.
image_number_slider = new QSlider(Qt::Horizontal, this);
addWidget(image_number_slider);
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; }");
image_number_slider_timer = new QTimer(this);
image_number_slider_timer->setSingleShot(true);
leftmost_button = makeButton(ToolbarIcons::first(), "First image", false);
left_button = makeButton(ToolbarIcons::prev(), "Previous (by jump)", false);
movie_button = new QPushButton("&Movie");
movie_button->setIcon(style()->standardIcon(QStyle::SP_MediaPlay));
movie_button->setCheckable(true);
movie_button->setChecked(false);
movie_button->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_M));
addWidget(movie_button);
current_image_edit_validator = new QIntValidator(0, 1000, this);
current_image_edit = new QLineEdit("0", this);
current_image_edit->setFixedWidth(54);
current_image_edit->setAlignment(Qt::AlignmentFlag::AlignRight);
current_image_edit->setValidator(current_image_edit_validator);
current_image_edit->setToolTip("Current image — type a number to jump");
autoload_button = new QPushButton("HTTP Sync");
autoload_button->setCheckable(true);
autoload_button->setChecked(false);
// Fixed width so the tri-state glyph prefix never reflows the toolbar.
autoload_button->setFixedWidth(120);
autoload_button->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_S));
total_number_label = new QLabel("/0", this);
right_button = makeButton(ToolbarIcons::next(), "Next (by jump)", false);
rightmost_button = makeButton(ToolbarIcons::last(), "Last image", false);
jump = new NumericComboBox({1, 2, 5, 10, 20, 100, 500}, 1, 999, this);
connect(jump, &NumericComboBox::valueChanged, this, &JFJochViewerToolbarImage::setImageJump);
auto sum_box = new NumericComboBox({1, 2, 5, 10, 20, 50, 100}, 1, 10000, this);
connect(sum_box, &NumericComboBox::valueChanged, this, &JFJochViewerToolbarImage::setSummation);
// --- lay out: source actions, then the scrubber, then precise navigation ---
addWidget(open_button);
addWidget(autoload_button);
addWidget(movie_button);
addSeparator();
addWidget(image_number_slider);
addSeparator();
addWidget(leftmost_button);
addWidget(left_button);
addWidget(current_image_edit);
addWidget(total_number_label);
addWidget(right_button);
addWidget(rightmost_button);
addWidget(new QLabel(" Jump ", this));
addWidget(jump);
addWidget(new QLabel(" Sum ", this));
addWidget(sum_box);
reanalyze_button = new QPushButton("&Reanalyze");
reanalyze_button->setCheckable(true);
reanalyze_button->setChecked(false);
reanalyze_button->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_R));
addWidget(reanalyze_button);
connect(movie_button, &QPushButton::clicked, this, &JFJochViewerToolbarImage::movieButtonPressed);
connect(autoload_button, &QPushButton::clicked, this, &JFJochViewerToolbarImage::autoloadButtonPressed);
connect(reanalyze_button, &QPushButton::clicked, this, &JFJochViewerToolbarImage::reanalyzeButtonPressed);
auto *stretch = new QWidget(this);
stretch->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
addWidget(stretch);
connect(movie_button, &QToolButton::clicked, this, &JFJochViewerToolbarImage::movieButtonPressed);
connect(open_button, &QToolButton::clicked, this, [this] { emit requestOpenFile(); });
connect(autoload_button, &QToolButton::clicked, this, &JFJochViewerToolbarImage::autoloadButtonPressed);
setImageNumber(0, 0);
connect(left_button, &QPushButton::clicked, this, &JFJochViewerToolbarImage::leftButtonPressed);
connect(right_button, &QPushButton::clicked, this, &JFJochViewerToolbarImage::rightButtonPressed);
connect(leftmost_button, &QPushButton::clicked, this, &JFJochViewerToolbarImage::leftmostButtonPressed);
connect(rightmost_button, &QPushButton::clicked, this, &JFJochViewerToolbarImage::rightmostButtonPressed);
connect(left_button, &QToolButton::clicked, this, &JFJochViewerToolbarImage::leftButtonPressed);
connect(right_button, &QToolButton::clicked, this, &JFJochViewerToolbarImage::rightButtonPressed);
connect(leftmost_button, &QToolButton::clicked, this, &JFJochViewerToolbarImage::leftmostButtonPressed);
connect(rightmost_button, &QToolButton::clicked, this, &JFJochViewerToolbarImage::rightmostButtonPressed);
connect(current_image_edit, &QLineEdit::editingFinished, this, &JFJochViewerToolbarImage::editFinalized);
connect(image_number_slider, &QSlider::sliderPressed, this, &JFJochViewerToolbarImage::imageNumberSliderPressed);
@@ -135,22 +130,42 @@ void JFJochViewerToolbarImage::updateButtons() {
total_number_label->setText("/" + QString::number(image_count_in_dataset));
}
void JFJochViewerToolbarImage::updateHttpButton() {
ToolbarIcons::HttpState s;
QString tip;
if (!http_connected) {
s = ToolbarIcons::HttpState::Disconnected;
tip = "Connect to a live source…";
} else if (autoload_mode == JFJochImageReadingWorker::AutoloadMode::HTTPSync) {
s = ToolbarIcons::HttpState::Live;
tip = "Following live (image + data) — click to stop";
} else {
s = ToolbarIcons::HttpState::Frozen;
tip = "Connected, not following — click to follow live";
}
autoload_button->setIcon(ToolbarIcons::httpSync(s));
autoload_button->setToolTip(tip);
}
void JFJochViewerToolbarImage::setImageNumber(int64_t total_images, int64_t current_image) {
image_count_in_dataset = total_images;
curr_image = current_image;
updateButtons();
}
void JFJochViewerToolbarImage::setHttpConnection(bool connected, QString) {
http_connected = connected;
updateHttpButton();
}
void JFJochViewerToolbarImage::leftButtonPressed() {
if (curr_image >= jump_value) {
if (curr_image >= jump_value)
emit loadImage(curr_image - jump_value, sum);
}
}
void JFJochViewerToolbarImage::rightButtonPressed() {
if (curr_image < image_count_in_dataset - jump_value) {
if (curr_image < image_count_in_dataset - jump_value)
emit loadImage(curr_image + jump_value, sum);
}
}
void JFJochViewerToolbarImage::rightmostButtonPressed() {
@@ -167,8 +182,7 @@ void JFJochViewerToolbarImage::imageNumberSliderPressed() {
void JFJochViewerToolbarImage::imageNumberSliderReleased() {
image_number_slider_manual = false;
int val = image_number_slider->value();
emit loadImage(val, sum);
emit loadImage(image_number_slider->value(), sum);
}
void JFJochViewerToolbarImage::imageNumberSliderMoved(int val) {
@@ -181,13 +195,10 @@ void JFJochViewerToolbarImage::imageNumberSliderMoved(int val) {
void JFJochViewerToolbarImage::editFinalized() {
bool ok;
int editedValue = current_image_edit->text().toInt(&ok);
if (ok && editedValue >= 1 && editedValue <= image_count_in_dataset) {
if (ok && editedValue >= 1 && editedValue <= image_count_in_dataset)
emit loadImage(editedValue - 1, sum);
}
}
void JFJochViewerToolbarImage::setImageJump(int val) {
jump_value = val;
updateButtons();
@@ -198,24 +209,20 @@ void JFJochViewerToolbarImage::setSummation(int val) {
sum = val;
jump_value = val;
jump->setValue(val);
updateButtons();
emit loadImage(curr_image, sum);
}
void JFJochViewerToolbarImage::autoloadButtonPressed() {
// The button either starts/resumes live following or stops it; the data-only "frozen" state
// is entered automatically by manually selecting an image while following.
switch (autoload_mode) {
case JFJochImageReadingWorker::AutoloadMode::HTTPSync:
emit autoLoadButtonPressed(JFJochImageReadingWorker::AutoloadMode::None);
break;
case JFJochImageReadingWorker::AutoloadMode::HTTPSyncDataset:
case JFJochImageReadingWorker::AutoloadMode::Movie:
case JFJochImageReadingWorker::AutoloadMode::None:
emit autoLoadButtonPressed(JFJochImageReadingWorker::AutoloadMode::HTTPSync);
break;
// Not connected: ask the window to open the connection dialog. Connected: toggle live following.
if (!http_connected) {
emit requestHttpConnect();
return;
}
if (autoload_mode == JFJochImageReadingWorker::AutoloadMode::HTTPSync)
emit autoLoadButtonPressed(JFJochImageReadingWorker::AutoloadMode::None);
else
emit autoLoadButtonPressed(JFJochImageReadingWorker::AutoloadMode::HTTPSync);
}
void JFJochViewerToolbarImage::movieButtonPressed() {
@@ -225,36 +232,10 @@ void JFJochViewerToolbarImage::movieButtonPressed() {
emit autoLoadButtonPressed(JFJochImageReadingWorker::AutoloadMode::None);
}
void JFJochViewerToolbarImage::reanalyzeButtonPressed() {
emit reanalyzeImages(reanalyze_button->isChecked());
}
void JFJochViewerToolbarImage::setAutoloadMode(JFJochImageReadingWorker::AutoloadMode input) {
autoload_mode = input;
switch (input) {
case JFJochImageReadingWorker::AutoloadMode::HTTPSync:
autoload_button->setText("▣ HTTP Sync");
autoload_button->setToolTip("Following live (image + data)");
autoload_button->setChecked(true);
movie_button->setChecked(false);
break;
case JFJochImageReadingWorker::AutoloadMode::HTTPSyncDataset:
autoload_button->setText("◐ HTTP Sync");
autoload_button->setToolTip("Following data only — image frozen (click to resume live)");
autoload_button->setChecked(true);
movie_button->setChecked(false);
break;
case JFJochImageReadingWorker::AutoloadMode::Movie:
autoload_button->setText("HTTP Sync");
autoload_button->setToolTip("");
autoload_button->setChecked(false);
movie_button->setChecked(true);
break;
case JFJochImageReadingWorker::AutoloadMode::None:
autoload_button->setText("HTTP Sync");
autoload_button->setToolTip("");
autoload_button->setChecked(false);
movie_button->setChecked(false);
break;
}
const bool playing = (input == JFJochImageReadingWorker::AutoloadMode::Movie);
movie_button->setChecked(playing); // checked (navy) = movie running; icon stays the camera
movie_button->setToolTip(playing ? "Stop movie" : "Play movie");
updateHttpButton();
}
+15 -11
View File
@@ -5,7 +5,7 @@
#include <QToolBar>
#include <QLabel>
#include <QPushButton>
#include <QToolButton>
#include <QSlider>
#include <QTimer>
@@ -13,10 +13,13 @@
#include "../widgets/NumericComboBox.h"
#include "../JFJochImageReadingWorker.h"
// Navigation / acquisition toolbar: which image is shown (first/prev/next/last, an editable
// number, and a prominent scrub slider), Jump/Sum, movie playback, and live HTTP sync.
class JFJochViewerToolbarImage : public QToolBar {
Q_OBJECT
JFJochImageReadingWorker::AutoloadMode autoload_mode = JFJochImageReadingWorker::AutoloadMode::None;
bool http_connected = false;
size_t image_count_in_dataset;
int64_t curr_image;
@@ -27,32 +30,35 @@ class JFJochViewerToolbarImage : public QToolBar {
QLineEdit *current_image_edit;
QIntValidator *current_image_edit_validator;
QPushButton *leftmost_button;
QPushButton *left_button;
QPushButton *right_button;
QPushButton *rightmost_button;
QToolButton *leftmost_button;
QToolButton *left_button;
QToolButton *right_button;
QToolButton *rightmost_button;
QSlider *image_number_slider;
QTimer *image_number_slider_timer;
bool image_number_slider_manual = false;
QPushButton *movie_button;
QPushButton *autoload_button;
QPushButton *reanalyze_button;
QToolButton *movie_button;
QToolButton *open_button;
QToolButton *autoload_button;
NumericComboBox *jump;
void updateButtons();
void updateHttpButton();
signals:
void loadImage(int64_t number, int summation);
void autoLoadButtonPressed(JFJochImageReadingWorker::AutoloadMode mode);
void imageJumpChanged(int64_t val);
void reanalyzeImages(bool input);
void requestHttpConnect(); // clicked while not connected: ask the window to open the dialog
void requestOpenFile(); // open-dataset button: ask the window to open the file dialog
public:
explicit JFJochViewerToolbarImage(QWidget *parent = nullptr);
public slots:
void setImageNumber(int64_t total_images, int64_t current_image);
void setAutoloadMode(JFJochImageReadingWorker::AutoloadMode input);
void setHttpConnection(bool connected, QString addr);
private slots:
void leftButtonPressed();
void rightButtonPressed();
@@ -69,6 +75,4 @@ private slots:
void movieButtonPressed();
void autoloadButtonPressed();
void reanalyzeButtonPressed();
};
+173
View File
@@ -0,0 +1,173 @@
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#include "ToolbarIcons.h"
#include <QPainter>
#include <QPainterPath>
#include <QPolygonF>
#include <functional>
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);
pm.fill(Qt::transparent);
QPainter p(&pm);
p.setRenderHint(QPainter::Antialiasing);
fn(p, color);
return pm;
}
// Toggle-capable icon: ink when off, white when on (checked, navy background), grey when disabled.
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;
}
void fillTriangle(QPainter &p, const QColor &c, QPointF a, QPointF b, QPointF tip) {
p.setPen(Qt::NoPen);
p.setBrush(c);
p.drawPolygon(QPolygonF({a, b, tip}));
}
void bar(QPainter &p, const QColor &c, double x) {
p.setPen(Qt::NoPen);
p.setBrush(c);
p.drawRoundedRect(QRectF(x, 9, 3.2, 14), 1.2, 1.2);
}
// A pair of curved arrows forming a refresh/sync ring.
void syncArrows(QPainter &p, const QColor &c) {
QPen pen(c, 2.4, Qt::SolidLine, Qt::RoundCap);
p.setPen(pen);
p.setBrush(Qt::NoBrush);
const QRectF r(9, 9, 14, 14);
p.drawArc(r, 40 * 16, 230 * 16);
p.drawArc(r, (40 + 180) * 16, 230 * 16);
// arrowheads at the two open ends
p.setPen(Qt::NoPen);
p.setBrush(c);
p.drawPolygon(QPolygonF({QPointF(22, 7.5), QPointF(22, 13.5), QPointF(17.5, 11)}));
p.drawPolygon(QPolygonF({QPointF(10, 24.5), QPointF(10, 18.5), QPointF(14.5, 21)}));
}
}
namespace ToolbarIcons {
QIcon first() {
return toggleIcon([](QPainter &p, const QColor &c) {
bar(p, c, 9);
fillTriangle(p, c, {24, 9}, {24, 23}, {14, 16});
});
}
QIcon prev() {
return toggleIcon([](QPainter &p, const QColor &c) {
fillTriangle(p, c, {22, 8}, {22, 24}, {11, 16});
});
}
QIcon next() {
return toggleIcon([](QPainter &p, const QColor &c) {
fillTriangle(p, c, {10, 8}, {10, 24}, {21, 16});
});
}
QIcon last() {
return toggleIcon([](QPainter &p, const QColor &c) {
fillTriangle(p, c, {8, 9}, {8, 23}, {18, 16});
bar(p, c, 19.8);
});
}
QIcon movie() {
return toggleIcon([](QPainter &p, const QColor &c) {
// Two reels on top of a camera body with a lens barrel — an old-fashioned movie camera.
p.setBrush(Qt::NoBrush);
p.setPen(QPen(c, 1.8));
p.drawEllipse(QPointF(10, 11.5), 3.0, 3.0);
p.drawEllipse(QPointF(16.5, 11.5), 3.0, 3.0);
p.setPen(Qt::NoPen);
p.setBrush(c);
p.drawRoundedRect(QRectF(5, 15, 16, 10), 2, 2);
p.drawRoundedRect(QRectF(21, 17, 5, 6), 1, 1); // lens barrel
});
}
QIcon openFile() {
return toggleIcon([](QPainter &p, const QColor &c) {
// 3.5" diskette ("save" floppy): square jacket with a cut top-right corner, the metal
// shutter across the top, and a label panel below.
p.setPen(QPen(c, 1.8));
p.setBrush(Qt::NoBrush);
QPolygonF body({{6, 6}, {20, 6}, {26, 12}, {26, 26}, {6, 26}});
p.drawPolygon(body); // jacket
p.setPen(Qt::NoPen);
p.setBrush(c);
p.drawRect(QRectF(10, 6, 8, 5.5)); // metal shutter (top)
p.setBrush(Qt::NoBrush);
p.setPen(QPen(c, 1.6));
p.drawRect(QRectF(9.5, 15.5, 13, 8.5)); // label panel
});
}
QIcon reanalyzeImage() {
return toggleIcon([](QPainter &p, const QColor &c) {
p.setPen(QPen(c, 2.0));
p.setBrush(Qt::NoBrush);
p.drawRoundedRect(QRectF(8, 8, 16, 16), 2.5, 2.5);
p.setBrush(c);
p.drawEllipse(QPointF(16, 16), 2.2, 2.2);
});
}
QIcon reanalyzeDataset() {
return toggleIcon([](QPainter &p, const QColor &c) {
p.setPen(QPen(c, 2.0));
p.setBrush(Qt::NoBrush);
for (int i = 2; i >= 0; --i)
p.drawRoundedRect(QRectF(5 + i * 4.0, 5 + i * 4.0, 14, 14), 2.0, 2.0);
p.setBrush(c);
p.drawEllipse(QPointF(12, 12), 2.0, 2.0);
});
}
QIcon httpSync(HttpState state) {
QColor dot;
switch (state) {
case HttpState::Disconnected: dot = QColor("#9AA6B3"); break; // grey
case HttpState::Live: dot = QColor("#5CB85C"); break; // green
case HttpState::Frozen: dot = QColor("#E9A23B"); break; // amber
}
auto draw = [dot](QPainter &p, const QColor &c) {
syncArrows(p, c);
p.setPen(QPen(Qt::white, 1.2));
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;
}
QString buttonStyle() {
return QStringLiteral(
"QToolButton { border:none; border-radius:4px; padding:4px 6px; color:#2B2B2B;"
" background:transparent; }"
" QToolButton:hover { background:rgba(250,114,104,0.20); }"
" QToolButton:checked { background:#1F3A5F; color:white; }"
" QToolButton:disabled { color:#9AA6B3; }");
}
} // namespace ToolbarIcons
+26
View File
@@ -0,0 +1,26 @@
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include <QIcon>
// Vector toolbar glyphs drawn with QPainter (Qt SVG module is not a build dependency). Toggle
// icons carry a white "On" variant so they read on the navy checked-button background; momentary
// icons are ink with a greyed disabled variant. Colours match the viewer palette.
namespace ToolbarIcons {
QIcon first();
QIcon prev();
QIcon next();
QIcon last();
QIcon movie(); // reel-on-top movie camera (distinct from the next/play triangle)
QIcon openFile(); // 5.25" diskette with an outward arrow
QIcon reanalyzeImage(); // single diffraction frame
QIcon reanalyzeDataset(); // a stack of frames
enum class HttpState { Disconnected, Live, Frozen };
QIcon httpSync(HttpState state); // sync arrows + a status dot (grey / green / amber)
// Shared stylesheet for flat toolbar buttons (coral hover, navy checked + white content).
QString buttonStyle();
}
@@ -57,7 +57,7 @@ JFJochProcessingJobsWindow::JFJochProcessingJobsWindow(JFJochImageReadingWorker
toolbar_ = new QToolBar("Jobs", this);
toolbar_->setMovable(false);
toolbar_->addAction("New job…", this, &JFJochProcessingJobsWindow::newJob);
// "New job" is launched by the "Reanalyze dataset" hero button, not from this dock's toolbar.
toolbar_->addAction("Cancel", this, &JFJochProcessingJobsWindow::cancelJob);
toolbar_->addAction("Remove result", this, &JFJochProcessingJobsWindow::removeResult);
toolbar_->addSeparator();
@@ -110,7 +110,7 @@ void JFJochProcessingJobsWindow::onHttpConnectionChanged(bool connected, QString
}
int JFJochProcessingJobsWindow::askJob(const ReprocessingInputs &inputs, JobSpec &spec) {
QDialog dlg(this);
QDialog dlg(window()); // centre on the main window, not inside the processing dock
dlg.setWindowTitle("New processing job");
auto *mode = new QComboBox(&dlg);