The right end of the display toolbar now carries three "A" buttons for the 100/125/150 % font sizes and a light/dark switch (a sun with a half-filled disc), so both settings are visible without opening the View menu. The size buttons and View > Font size stay in sync through JFJochViewerMenu's new SetFontZoom / fontZoomChanged; the View > Theme tick follows ThemeNotifier, so it moves when the toolbar switches the theme. With Qt 6.8+ an explicit Light or Dark choice is also requested from the OS (QStyleHints::setColorScheme), so the title bar macOS and Windows draw matches the window, as apps with their own theme switch do. Follow system withdraws the request first, since Qt otherwise reports the requested scheme instead of the desktop's. The image counter reads "1 / 1800" instead of "1/1800". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
263 lines
10 KiB
C++
263 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 "../ViewerTheme.h"
|
|
#include <algorithm>
|
|
#include <QTimer>
|
|
#include "../RemoteDisplayMode.h"
|
|
#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);
|
|
SetThemedStyleSheet(b, 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");
|
|
SetThemedStyleSheet(image_number_slider, SliderStyle);
|
|
image_number_slider_timer = new QTimer(this);
|
|
image_number_slider_timer->setSingleShot(true);
|
|
|
|
number_tail_timer_ = new QTimer(this);
|
|
number_tail_timer_->setSingleShot(true);
|
|
connect(number_tail_timer_, &QTimer::timeout, this, [this] {
|
|
number_rate_.restart();
|
|
updateButtons();
|
|
});
|
|
|
|
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;
|
|
if (RemoteDisplayMode() && number_rate_.isValid() && number_rate_.elapsed() < kRemoteRepaintIntervalMs) {
|
|
number_tail_timer_->start(kRemoteRepaintIntervalMs);
|
|
return;
|
|
}
|
|
number_tail_timer_->stop();
|
|
number_rate_.restart();
|
|
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();
|
|
}
|