From a7c1560dcbcf46f2b2e82fa34b521f5263583621 Mon Sep 17 00:00:00 2001 From: leonarski_f Date: Tue, 23 Jun 2026 11:08:14 +0200 Subject: [PATCH] =?UTF-8?q?viewer:=20toolbar=20rework=20=E2=80=94=20icons,?= =?UTF-8?q?=20prominent=20scrubber,=20HTTP=20states=20(g=E2=80=93k)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- viewer/CMakeLists.txt | 2 + viewer/JFJochViewerMenu.h | 6 +- viewer/JFJochViewerWindow.cpp | 21 +- viewer/toolbar/JFJochViewerToolbarDisplay.cpp | 37 ++- viewer/toolbar/JFJochViewerToolbarDisplay.h | 9 +- viewer/toolbar/JFJochViewerToolbarImage.cpp | 235 ++++++++---------- viewer/toolbar/JFJochViewerToolbarImage.h | 26 +- viewer/widgets/ToolbarIcons.cpp | 173 +++++++++++++ viewer/widgets/ToolbarIcons.h | 26 ++ viewer/windows/JFJochProcessingJobsWindow.cpp | 4 +- 10 files changed, 377 insertions(+), 162 deletions(-) create mode 100644 viewer/widgets/ToolbarIcons.cpp create mode 100644 viewer/widgets/ToolbarIcons.h diff --git a/viewer/CMakeLists.txt b/viewer/CMakeLists.txt index 85ed9d89..667a9f74 100644 --- a/viewer/CMakeLists.txt +++ b/viewer/CMakeLists.txt @@ -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 diff --git a/viewer/JFJochViewerMenu.h b/viewer/JFJochViewerMenu.h index 2a07ef2c..50f49ef0 100644 --- a/viewer/JFJochViewerMenu.h +++ b/viewer/JFJochViewerMenu.h @@ -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(); diff --git a/viewer/JFJochViewerWindow.cpp b/viewer/JFJochViewerWindow.cpp index fda87567..09013595 100644 --- a/viewer/JFJochViewerWindow.cpp +++ b/viewer/JFJochViewerWindow.cpp @@ -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); diff --git a/viewer/toolbar/JFJochViewerToolbarDisplay.cpp b/viewer/toolbar/JFJochViewerToolbarDisplay.cpp index f9cee39b..7048192a 100644 --- a/viewer/toolbar/JFJochViewerToolbarDisplay.cpp +++ b/viewer/toolbar/JFJochViewerToolbarDisplay.cpp @@ -4,29 +4,42 @@ #include "JFJochViewerToolbarDisplay.h" #include #include "../../common/ColorScale.h" +#include "../widgets/ToolbarIcons.h" constexpr double MAX_SLIDER_NO_IMAGE = 65000; JFJochViewerToolbarDisplay::JFJochViewerToolbarDisplay(QWidget *parent) : QToolBar(parent) { - addWidget(new QLabel("Foreground   ")); + 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("  Color map  ")); + 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::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); diff --git a/viewer/toolbar/JFJochViewerToolbarDisplay.h b/viewer/toolbar/JFJochViewerToolbarDisplay.h index 4db1efef..673fd39b 100644 --- a/viewer/toolbar/JFJochViewerToolbarDisplay.h +++ b/viewer/toolbar/JFJochViewerToolbarDisplay.h @@ -5,18 +5,21 @@ #include #include "../widgets/SliderPlusBox.h" -#include +#include #include #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); diff --git a/viewer/toolbar/JFJochViewerToolbarImage.cpp b/viewer/toolbar/JFJochViewerToolbarImage.cpp index 497550d9..1fbddc77 100644 --- a/viewer/toolbar/JFJochViewerToolbarImage.cpp +++ b/viewer/toolbar/JFJochViewerToolbarImage.cpp @@ -3,100 +3,95 @@ #include "JFJochViewerToolbarImage.h" #include -#include +#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(); } diff --git a/viewer/toolbar/JFJochViewerToolbarImage.h b/viewer/toolbar/JFJochViewerToolbarImage.h index c7679865..93792ef2 100644 --- a/viewer/toolbar/JFJochViewerToolbarImage.h +++ b/viewer/toolbar/JFJochViewerToolbarImage.h @@ -5,7 +5,7 @@ #include #include -#include +#include #include #include @@ -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(); }; - diff --git a/viewer/widgets/ToolbarIcons.cpp b/viewer/widgets/ToolbarIcons.cpp new file mode 100644 index 00000000..fe24de1c --- /dev/null +++ b/viewer/widgets/ToolbarIcons.cpp @@ -0,0 +1,173 @@ +// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute +// SPDX-License-Identifier: GPL-3.0-only + +#include "ToolbarIcons.h" + +#include +#include +#include +#include + +namespace { + constexpr int S = 32; + const QColor INK("#2B2B2B"); + const QColor DISABLED("#9AA6B3"); + + using DrawFn = std::function; + + 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 diff --git a/viewer/widgets/ToolbarIcons.h b/viewer/widgets/ToolbarIcons.h new file mode 100644 index 00000000..81a8316d --- /dev/null +++ b/viewer/widgets/ToolbarIcons.h @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute +// SPDX-License-Identifier: GPL-3.0-only + +#pragma once + +#include + +// 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(); +} diff --git a/viewer/windows/JFJochProcessingJobsWindow.cpp b/viewer/windows/JFJochProcessingJobsWindow.cpp index ad41f9fa..0fc02790 100644 --- a/viewer/windows/JFJochProcessingJobsWindow.cpp +++ b/viewer/windows/JFJochProcessingJobsWindow.cpp @@ -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);