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:
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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> "));
|
||||
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(" <b>Color map</b> "));
|
||||
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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user