Build Packages / build:rugnux:aarch64 (cross) (push) Successful in 8m57s
Build Packages / build:windows:nocuda (push) Successful in 17m15s
Build Packages / build:windows:cuda (push) Successful in 19m35s
Build Packages / build:rugnux-tgz (x86_64) (push) Successful in 19m39s
Build Packages / build:viewer-tgz:cpu (push) Successful in 20m38s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 22m45s
Build Packages / build:viewer-tgz:cuda (push) Successful in 22m54s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 27m16s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 27m19s
Build Packages / build:rugnux:windows (push) Successful in 11m1s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 19m22s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 21m33s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 26m50s
Build Packages / build:rpm (rocky9) (push) Successful in 24m20s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 22m45s
Build Packages / Generate python client (push) Successful in 40s
Build Packages / build:rpm (rocky8) (push) Successful in 29m1s
Build Packages / Create release (push) Skipped
Build Packages / Build documentation (push) Successful in 1m4s
Build Packages / XDS test (durin plugin) (push) Successful in 11m2s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 27m23s
Build Packages / DIALS test (push) Successful in 26m51s
Build Packages / XDS test (neggia plugin) (push) Successful in 8m44s
Build Packages / XDS test (JFJoch plugin) (push) Successful in 9m37s
Build Packages / Unit tests (push) Successful in 1h25m14s
The wheel already zooms, and with Ctrl or Shift it moves the foreground; Alt now moves through the dataset, one image per notch, wheel up forward - the direction QAbstractSlider's own wheel handling uses, which is what the toolbar's scrub slider follows. The view does not know which image it is showing, so it emits the step and the navigation toolbar applies it: the same loadImage() every other control ends in, with the same clamp and the same Sum setting, so nothing about loading is duplicated. An empty dataset is left alone, because loadImage(-1) does not mean "before the first image" but "the latest one" when the viewer is following a running collection over HTTP. The signal is on JFJochImage, so the other views emit it too; only the diffraction view is connected, which leaves them as they were. Note for a desktop where Alt+wheel does nothing: many window managers grab Alt-modified mouse events before the application sees them, and that is a window-manager setting, not something the viewer can take back. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FBumeJVx4oeXxiBRpkrE5H
252 lines
10 KiB
C++
252 lines
10 KiB
C++
// SPDX-FileCopyrightText: 2025 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
#include "JFJochViewerToolbarImage.h"
|
|
#include <algorithm>
|
|
#include <QTimer>
|
|
#include "../widgets/ToolbarIcons.h"
|
|
|
|
JFJochViewerToolbarImage::JFJochViewerToolbarImage(QWidget *parent) : QToolBar(parent) {
|
|
setWindowTitle("Navigation"); // shown in the toolbar/dock context menu
|
|
image_count_in_dataset = 0;
|
|
|
|
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;
|
|
};
|
|
|
|
// --- create widgets ---
|
|
open_button = makeButton(ToolbarIcons::openFile(), "Open dataset…", false);
|
|
open_button->setShortcut(QKeySequence::Open);
|
|
|
|
autoload_button = makeButton(ToolbarIcons::httpSync(ToolbarIcons::HttpState::Disconnected),
|
|
"Connect to a live source…", false);
|
|
autoload_button->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_S));
|
|
|
|
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);
|
|
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);
|
|
|
|
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");
|
|
|
|
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);
|
|
|
|
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, &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);
|
|
connect(image_number_slider, &QSlider::sliderReleased, this, &JFJochViewerToolbarImage::imageNumberSliderReleased);
|
|
connect(image_number_slider, &QSlider::sliderMoved, this, &JFJochViewerToolbarImage::imageNumberSliderMoved);
|
|
}
|
|
|
|
void JFJochViewerToolbarImage::updateButtons() {
|
|
if (image_count_in_dataset > 0) {
|
|
current_image_edit->setDisabled(false);
|
|
current_image_edit->setText(QString::number(curr_image + 1));
|
|
current_image_edit_validator->setRange(1, image_count_in_dataset);
|
|
|
|
left_button->setDisabled(curr_image < jump_value);
|
|
right_button->setDisabled(curr_image >= image_count_in_dataset - jump_value);
|
|
leftmost_button->setDisabled(curr_image == 0);
|
|
rightmost_button->setDisabled(curr_image == image_count_in_dataset - 1);
|
|
|
|
image_number_slider->setRange(0, image_count_in_dataset - 1);
|
|
image_number_slider->setEnabled(true);
|
|
if (!image_number_slider_manual)
|
|
image_number_slider->setValue(curr_image);
|
|
} else {
|
|
left_button->setDisabled(true);
|
|
right_button->setDisabled(true);
|
|
leftmost_button->setDisabled(true);
|
|
rightmost_button->setDisabled(true);
|
|
|
|
current_image_edit->setText(QString::number(0));
|
|
image_number_slider->setValue(0);
|
|
image_number_slider->setDisabled(true);
|
|
}
|
|
if (image_count_in_dataset == 0)
|
|
total_number_label->setText("/ - ");
|
|
else
|
|
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)
|
|
emit loadImage(curr_image - jump_value, sum);
|
|
}
|
|
|
|
void JFJochViewerToolbarImage::rightButtonPressed() {
|
|
if (curr_image < image_count_in_dataset - jump_value)
|
|
emit loadImage(curr_image + jump_value, sum);
|
|
}
|
|
|
|
void JFJochViewerToolbarImage::rightmostButtonPressed() {
|
|
emit loadImage(image_count_in_dataset - 1, sum);
|
|
}
|
|
|
|
void JFJochViewerToolbarImage::leftmostButtonPressed() {
|
|
emit loadImage(0, sum);
|
|
}
|
|
|
|
void JFJochViewerToolbarImage::stepImage(int steps) {
|
|
if (image_count_in_dataset == 0)
|
|
return;
|
|
const auto target = std::clamp<int64_t>(curr_image + steps, 0,
|
|
static_cast<int64_t>(image_count_in_dataset) - 1);
|
|
if (target != curr_image)
|
|
emit loadImage(target, sum);
|
|
}
|
|
|
|
void JFJochViewerToolbarImage::imageNumberSliderPressed() {
|
|
image_number_slider_manual = true;
|
|
}
|
|
|
|
void JFJochViewerToolbarImage::imageNumberSliderReleased() {
|
|
image_number_slider_manual = false;
|
|
emit loadImage(image_number_slider->value(), sum);
|
|
}
|
|
|
|
void JFJochViewerToolbarImage::imageNumberSliderMoved(int val) {
|
|
if (!image_number_slider_timer->isActive()) {
|
|
emit loadImage(val, sum);
|
|
image_number_slider_timer->start(300);
|
|
}
|
|
}
|
|
|
|
void JFJochViewerToolbarImage::editFinalized() {
|
|
bool ok;
|
|
int editedValue = current_image_edit->text().toInt(&ok);
|
|
if (ok && editedValue >= 1 && editedValue <= image_count_in_dataset)
|
|
emit loadImage(editedValue - 1, sum);
|
|
}
|
|
|
|
void JFJochViewerToolbarImage::setImageJump(int val) {
|
|
jump_value = val;
|
|
updateButtons();
|
|
emit imageJumpChanged(jump_value);
|
|
}
|
|
|
|
void JFJochViewerToolbarImage::setSummation(int val) {
|
|
sum = val;
|
|
jump_value = val;
|
|
jump->setValue(val);
|
|
updateButtons();
|
|
emit loadImage(curr_image, sum);
|
|
}
|
|
|
|
void JFJochViewerToolbarImage::autoloadButtonPressed() {
|
|
// 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() {
|
|
if (movie_button->isChecked())
|
|
emit autoLoadButtonPressed(JFJochImageReadingWorker::AutoloadMode::Movie);
|
|
else
|
|
emit autoLoadButtonPressed(JFJochImageReadingWorker::AutoloadMode::None);
|
|
}
|
|
|
|
void JFJochViewerToolbarImage::setAutoloadMode(JFJochImageReadingWorker::AutoloadMode input) {
|
|
autoload_mode = input;
|
|
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();
|
|
}
|