// SPDX-FileCopyrightText: 2025 Filip Leonarski, Paul Scherrer Institute // SPDX-License-Identifier: GPL-3.0-only #include "JFJochViewerToolbarImage.h" #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; 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::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(); }