Files
Jungfraujoch/viewer/JFJochViewerWindow.cpp
T
leonarski_fandClaude Fable 5.1 7f884f67c7 Per-dataset bearer tokens in the broker; viewer token, model option, grid-scan plot, dark theme, tutorial
jfjoch_broker: /start takes an optional `tokens` list (any number, all equivalent; writeOnly in the
API). While the current dataset has any, the endpoints that expose it - /statistics/data_collection,
/result/scan, /image_buffer/{start.cbor,image.cbor,image.jpeg,image.tiff}, /preview/plot{,.bin} -
answer 401 unless the request carries `Authorization: Bearer <token>`; /statistics keeps serving
the instrument view and only omits its `measurement` block. Enforcement is one pre-routing hook
over a named path set (the same set carries `bearerAuth` in jfjoch_api.yaml); tokens are compared
in constant time and never read back or logged. The tokens are replaced only by an accepted start,
and atomically with clearing the previous run's status, plots and image buffer, in this order:
clear, swap, import the new settings - so no moment serves the old run under the new tokens or the
new run's name under the old ones. Settings are validated on a copy first so a refused start
changes nothing.

jfjoch_viewer: a Token field (password echo) next to the http/https scheme in Open HTTP Connection,
JUNGFRAUJOCH_HTTP_TOKEN, and D-Bus LoadFile(..., token) / SetHttpToken; the dialog overrides the
others, nothing is persisted. A 401 clears the display, stops following and puts a line on the
status bar - no dialog, since a dataset changing hands is the normal cause.

Web frontend: key button in the top bar (token in sessionStorage, applied to the bearerAuth
operations by the generated client, and to the raw preview fetch), a tokens field in the start
form, and a "token required" hint on the plots. Python client: Configuration(access_token=...)
after regeneration.

Viewer: reference dataset accepts a structure-factor mmCIF (already read by content; the dialog
now offers it) and a model file can be chosen next to it (ProcessConfig::model_path, as
rugnux --model); the job also carries the reference's free-R flags, cell and setting, and the
copied command line states -z / --reference-column / --model. A grid scan keeps its own preferred
dataset-info plot and "Spots + background" means the spot count there. Dark theme: the navy hero
buttons, checked segments, warning texts and chart guide lines follow the theme instead of their
light-theme colours.

Docs: SECURITY.md section 3 is now the implemented scheme; a guided tour with four screenshots
in JFJOCH_VIEWER.md; broker, OpenAPI, Python client and frontend pages mention the token.

Tests: BearerTokens unit test and an HTTP round trip over the real broker HTTP layer (jfjoch_test
now compiles JFJochBrokerHttp.cpp and links httplib).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-27 10:30:06 +02:00

981 lines
50 KiB
C++

// SPDX-FileCopyrightText: 2025 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#include "JFJochViewerWindow.h"
#include <QThread>
#include <QDockWidget>
#include <QKeyEvent>
#include <QGuiApplication>
#include <QLineEdit>
#include <QAbstractSpinBox>
#include <QComboBox>
#include <QTextEdit>
#include <QPlainTextEdit>
#include <QAbstractItemView>
#include <limits>
#include <QScreen>
#include <QApplication>
#include <QScrollArea>
#include <QScrollBar>
#include <QResizeEvent>
#include <utility>
#include <QSettings>
#include <QCloseEvent>
#include <QRandomGenerator>
#include <QLabel>
#include <QPainter>
#include <QPushButton>
#include <utility>
#include "JFJochImageReadingWorker.h"
#include "RemoteDisplayMode.h"
#include "../common/ColorScale.h"
#include "image_viewer/JFJochDiffractionImage.h"
#include "JFJochViewerSidePanel.h"
#include "widgets/JFJochViewerSettingsDock.h"
#include "widgets/JFJochViewerFileBrowser.h"
#include "JFJochViewerStatusBar.h"
#include "../common/CUDAWrapper.h"
#include "../rugnux/RugnuxDefaults.h"
#include "windows/JFJochViewerImageListWindow.h"
#include "windows/JFJochViewerMetadataWindow.h"
#ifdef JFJOCH_VIEWER_DBUS
#include "dbus/JFJochViewerAdaptor.h"
#endif
#include "windows/JFJochProcessingJobsWindow.h"
#include "windows/JFJochViewerSpotListWindow.h"
#include "windows/JFJochViewerReflectionListWindow.h"
#include "windows/JFJochCalibrationWindow.h"
#include "toolbar/JFJochViewerToolbarDisplay.h"
#include "toolbar/JFJochViewerToolbarImage.h"
#include "windows/JFJoch2DAzintImageWindow.h"
#include "widgets/JFJochViewerMagnifier.h"
#include "windows/JFJochHelperWindow.h"
#include "image_viewer/JFJochImage.h"
#include "image_viewer/JFJochSimpleImage.h"
#include <QMessageBox>
// Dock-layout version for saveState/restoreState: bump whenever the dock set / structure changes so a
// stale saved layout (which can restore a dock as a broken, non-responsive floating window) is rejected
// and the default layout applies instead.
static constexpr int kLayoutVersion = 5;
JFJochViewerWindow::JFJochViewerWindow(QWidget *parent, bool dbus, const QString &file) : QMainWindow(parent) {
image_tail_timer_ = new QTimer(this);
image_tail_timer_->setSingleShot(true);
connect(image_tail_timer_, &QTimer::timeout, this, &JFJochViewerWindow::DeliverPendingImage);
menuBar = new JFJochViewerMenu(this);
setMenuBar(menuBar);
// PSI logo in the menu-bar corner. There are four interchangeable dot designs; pick one at
// random each launch, for fun.
{
const int n = QRandomGenerator::global()->bounded(1, 5); // 1..4
QPixmap logoPixmap(QStringLiteral(":/psi_%1.png").arg(n, 2, 10, QLatin1Char('0')));
auto *logo = new QLabel(this);
logo->setPixmap(logoPixmap.scaledToHeight(22, Qt::SmoothTransformation));
logo->setContentsMargins(6, 0, 8, 0);
menuBar->setCornerWidget(logo, Qt::TopRightCorner);
}
setFocusPolicy(Qt::StrongFocus);
auto toolBarImage = new JFJochViewerToolbarImage(this);
toolBarImage->setObjectName("toolBarImage"); // objectName required for saveState/restoreState
addToolBar(Qt::TopToolBarArea, toolBarImage);
addToolBarBreak(Qt::TopToolBarArea);
toolBarDisplay = new JFJochViewerToolbarDisplay(this);
toolBarDisplay->setObjectName("toolBarDisplay");
addToolBar(Qt::TopToolBarArea, toolBarDisplay);
// The font size lives in the menu; the toolbar's "A" buttons are a second way to it.
connect(toolBarDisplay, &JFJochViewerToolbarDisplay::fontZoomSelected, menuBar, &JFJochViewerMenu::SetFontZoom);
connect(menuBar, &JFJochViewerMenu::fontZoomChanged, toolBarDisplay, &JFJochViewerToolbarDisplay::updateFontZoom);
toolBarDisplay->updateFontZoom(menuBar->FontZoom());
statusbar = new JFJochViewerStatusBar(this);
setStatusBar(statusbar);
setDockOptions(dockOptions() & ~QMainWindow::DockOption::AllowTabbedDocks);
setWindowTitle("Jungfraujoch image viewer");
// Start large on a big display but fit within a laptop screen.
const QRect avail = QGuiApplication::primaryScreen()->availableGeometry();
resize(qMin(1200, avail.width() - 100), qMin(1100, avail.height() - 100));
SpotFindingSettings spot_finding_settings = DiffractionExperiment::DefaultDataProcessingSettings();
// The interactive viewer is an offline tool looking at one dataset at a time, so it starts from the
// self-calibrating settings rather than numbers the user would have to tune: the per-ring adaptive
// threshold, min-pix chosen per image (std::nullopt), and detection out to the detector edge
// (high_resolution_limit unset). All three are switchable in the settings dock.
spot_finding_settings.adaptive_threshold = true;
spot_finding_settings.min_pix_per_spot = std::nullopt;
spot_finding_settings.indexing = true;
IndexingSettings indexing_settings;
indexing_settings.IndexingThreads(1);
indexing_settings.Algorithm(IndexingAlgorithmEnum::Auto);
if (get_gpu_count() == 0) {
indexing_settings.Algorithm(IndexingAlgorithmEnum::FFTW);
indexing_settings.FFT_NumVectors(8 * 1024);
}
indexing_settings.GeomRefinementAlgorithm(GeomRefinementAlgorithmEnum::BeamCenter);
DiffractionExperiment experiment;
experiment.ImportIndexingSettings(indexing_settings);
experiment.DetectIceRings(true);
// Files carry no polarization factor, so without this the interactive analysis would integrate
// without the Lp correction that a processing run applies. Start from the same policy `rugnux`
// does; the settings panel shows it and can change it per dataset.
ApplyRugnuxExperimentDefaults(experiment);
// Central area: the diffraction image. Everything else (inspector, plots, processing) is a
// dock, so the layout can be rearranged, saved, and switched between perspectives.
viewer = new JFJochDiffractionImage(this);
viewer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
setCentralWidget(viewer);
auto side_panel = new JFJochViewerSidePanel(this);
inspectorScroll = new QScrollArea(this);
inspectorScroll->setWidget(side_panel);
inspectorScroll->setWidgetResizable(true);
inspectorScroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
inspectorScroll->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
UpdateInspectorWidth();
inspectorDock = new QDockWidget("Inspector", this);
inspectorDock->setObjectName("inspectorDock");
inspectorDock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea);
inspectorDock->setWidget(inspectorScroll);
addDockWidget(Qt::RightDockWidgetArea, inspectorDock);
menuBar->AddDockEntry(inspectorDock, "Inspector");
// The magnifier sits under the inspector, whose sections are usually folded, and takes the rest
// of the column; as its own dock it can be moved elsewhere or closed.
magnifier = new JFJochViewerMagnifier(this);
magnifierDock = new QDockWidget("Magnifier", this);
magnifierDock->setObjectName("magnifierDock");
magnifierDock->setAllowedAreas(Qt::AllDockWidgetAreas);
magnifierDock->setWidget(magnifier);
addDockWidget(Qt::RightDockWidgetArea, magnifierDock);
splitDockWidget(inspectorDock, magnifierDock, Qt::Vertical);
resizeDocks({inspectorDock, magnifierDock}, {600, 400}, Qt::Vertical);
menuBar->AddDockEntry(magnifierDock, "Magnifier");
// Shown by anyone - the Window menu, a layout, a reset - it is no longer the window's to restore.
for (auto *dock: {inspectorDock, magnifierDock})
connect(dock, &QDockWidget::visibilityChanged, this,
[this, dock](bool visible) { if (visible) autoHiddenDocks.removeAll(dock); });
magnifierWindow = new JFJochHelperWindow(this);
magnifierWindow->setWindowTitle("Magnifier");
magnifierWindow->resize(500, 500);
connect(magnifier, &JFJochViewerMagnifier::popOutClicked, this, [this] {
if (magnifierWindow->isVisible())
magnifierWindow->close(); // docks it back, below
else
PopOutMagnifier();
});
connect(magnifierWindow, &JFJochHelperWindow::closing, this, [this] {
if (!magnifierWindow->centralWidget())
return; // closed while not popped out (a settings reset): the magnifier is in its dock
magnifierDock->setWidget(magnifierWindow->takeCentralWidget());
magnifierDock->show();
magnifier->setPoppedOut(false);
});
reading_worker = new JFJochImageReadingWorker(spot_finding_settings, experiment);
reading_thread = new QThread(this);
reading_worker->moveToThread(reading_thread);
reading_thread->start();
auto tableWindow = new JFJochViewerImageListWindow(this);
auto metadataWindow = new JFJochViewerMetadataWindow(this);
auto spotWindow = new JFJochViewerSpotListWindow(this);
auto reflectionWindow = new JFJochViewerReflectionListWindow(this);
auto calibrationWindow = new JFJochCalibrationWindow(this);
auto azintImageWindow = new JFJoch2DAzintImageWindow(this);
processingJobsWindow = new JFJochProcessingJobsWindow(reading_worker, this);
// The two hero actions (re-analyse the current image / the whole dataset) live at the top of the
// settings panel now — see the JFJochViewerSettingsDock wiring below.
menuBar->AddWindowEntry(tableWindow, "Image list");
menuBar->AddWindowEntry(spotWindow, "Spot list");
menuBar->AddWindowEntry(reflectionWindow, "Reflection list");
menuBar->AddWindowEntry(metadataWindow, "Image metadata");
menuBar->AddWindowEntry(calibrationWindow, "Calibration image viewer");
menuBar->AddWindowEntry(azintImageWindow, "Azimuthal integration 2D image");
// processingJobsWindow is docked (bottom, next to the plots), not a standalone window - see below.
#ifdef JFJOCH_VIEWER_DBUS
if (dbus) {
// Create adaptor attached to this window
new JFJochViewerAdaptor(this);
QDBusConnection connection = QDBusConnection::sessionBus();
if (!connection.registerService("ch.psi.jfjoch_viewer")) {
qWarning("Failed to register D-Bus service: %s", qPrintable(connection.lastError().message()));
} else {
if (!connection.registerObject("/", this, QDBusConnection::ExportAdaptors)) {
qFatal("Failed to register D-Bus object: %s", qPrintable(connection.lastError().message()));
}
}
}
#else
(void) dbus;
#endif
connect(this, &JFJochViewerWindow::LoadFileRequest,
reading_worker, &JFJochImageReadingWorker::LoadFile);
connect(this, &JFJochViewerWindow::LoadImageRequest,
reading_worker, &JFJochImageReadingWorker::LoadImage);
connect(menuBar, &JFJochViewerMenu::fileOpenSelected,
reading_worker, &JFJochImageReadingWorker::LoadFile);
connect(menuBar, &JFJochViewerMenu::fileOpenSelected,
this, &JFJochViewerWindow::FollowInFileBrowser);
// Dataset token: dialog and D-Bus both go through SetHttpToken; the worker applies it on its
// own thread, queued in order with the open request that follows it.
connect(this, &JFJochViewerWindow::HttpTokenRequest,
reading_worker, &JFJochImageReadingWorker::SetHttpToken);
connect(menuBar, &JFJochViewerMenu::httpTokenSelected,
this, &JFJochViewerWindow::SetHttpToken);
connect(reading_worker, &JFJochImageReadingWorker::httpAuthRequired,
statusbar, &JFJochViewerStatusBar::display);
if (const QString env_token = qEnvironmentVariable("JUNGFRAUJOCH_HTTP_TOKEN"); !env_token.isEmpty())
SetHttpToken(env_token);
connect(menuBar, &JFJochViewerMenu::fileCloseSelected,
reading_worker, &JFJochImageReadingWorker::CloseFile);
// The worker runs in its own thread; its imageLoaded crosses the thread boundary exactly once,
// into OnImageReady, which re-emits imageReady to every GUI consumer synchronously (same thread)
// and then acks the worker (imageConsumed). Routing all consumers through one window signal makes
// the ack land after they have all run, and lets the worker cap how many frames are in flight.
connect(reading_worker, &JFJochImageReadingWorker::imageLoaded,
this, &JFJochViewerWindow::OnImageReady);
connect(this, &JFJochViewerWindow::imageConsumed,
reading_worker, &JFJochImageReadingWorker::ImageConsumed);
// Dataset stream routed the same way, so it gets the same backpressure: the worker's datasetLoaded
// crosses the thread boundary once into OnDatasetReady, which re-emits datasetReady to every GUI
// consumer synchronously and then acks the worker (datasetConsumed).
connect(reading_worker, &JFJochImageReadingWorker::datasetLoaded,
this, &JFJochViewerWindow::OnDatasetReady);
connect(this, &JFJochViewerWindow::datasetConsumed,
reading_worker, &JFJochImageReadingWorker::DatasetConsumed);
connect(this, &JFJochViewerWindow::imageReady,
viewer, &JFJochDiffractionImage::loadImage);
connect(this, &JFJochViewerWindow::imageReady,
side_panel, &JFJochViewerSidePanel::loadImage);
connect(reading_worker, &JFJochImageReadingWorker::imageStatsUpdated,
side_panel, &JFJochViewerSidePanel::loadImage);
connect(reading_worker, &JFJochImageReadingWorker::imageNumberChanged, toolBarImage,
&JFJochViewerToolbarImage::setImageNumber);
connect(toolBarImage, &JFJochViewerToolbarImage::loadImage, reading_worker, &JFJochImageReadingWorker::LoadImage);
connect(viewer, &JFJochDiffractionImage::stepImage, toolBarImage, &JFJochViewerToolbarImage::stepImage);
connect(toolBarDisplay, &JFJochViewerToolbarDisplay::setForeground, viewer,
&JFJochDiffractionImage::changeForeground);
connect(toolBarDisplay, &JFJochViewerToolbarDisplay::setBackground, viewer,
&JFJochDiffractionImage::changeBackground);
connect(viewer, &JFJochDiffractionImage::backgroundChanged,
toolBarDisplay, &JFJochViewerToolbarDisplay::updateBackground);
connect(viewer, &JFJochDiffractionImage::autoForegroundChanged,
toolBarDisplay, &JFJochViewerToolbarDisplay::updateAutoForeground);
connect(toolBarDisplay, &JFJochViewerToolbarDisplay::setAutoForeground, viewer,
&JFJochDiffractionImage::setAutoForeground);
connect(toolBarDisplay, &JFJochViewerToolbarDisplay::setHDRMode, viewer,
&JFJochDiffractionImage::setHDRMode);
connect(toolBarDisplay, &JFJochViewerToolbarDisplay::colorMapChanged, viewer,
&JFJochDiffractionImage::setColorMap);
connect(toolBarDisplay, &JFJochViewerToolbarDisplay::colorMapChanged, azintImageWindow,
&JFJoch2DAzintImageWindow::setColorMap);
connect(this, &JFJochViewerWindow::imageReady,
azintImageWindow, &JFJoch2DAzintImageWindow::imageLoaded);
connect(viewer, &JFJochDiffractionImage::foregroundChanged,
toolBarDisplay, &JFJochViewerToolbarDisplay::updateForeground);
connect(side_panel, &JFJochViewerSidePanel::roisChanged,
reading_worker, &JFJochImageReadingWorker::SetROIDefinition);
connect(side_panel, &JFJochViewerSidePanel::selectedROIChanged,
viewer, &JFJochDiffractionImage::setSelectedROI);
connect(viewer, &JFJochDiffractionImage::roiGeometryEdited,
reading_worker, &JFJochImageReadingWorker::SetROIDefinition);
connect(viewer, &JFJochDiffractionImage::roiSelected,
side_panel, &JFJochViewerSidePanel::selectROIInList);
connect(side_panel, &JFJochViewerSidePanel::downloadROIs,
reading_worker, &JFJochImageReadingWorker::DownloadROIsFromServer);
connect(side_panel, &JFJochViewerSidePanel::uploadROIs,
reading_worker, &JFJochImageReadingWorker::UploadROIsToServer);
connect(side_panel, &JFJochViewerSidePanel::maskFromROI,
reading_worker, &JFJochImageReadingWorker::MaskFromSelectedROI);
connect(menuBar, &JFJochViewerMenu::clearUserMaskSelected,
reading_worker, &JFJochImageReadingWorker::ClearUserMask);
connect(menuBar, &JFJochViewerMenu::saveUserMaskTiffSelected,
reading_worker, &JFJochImageReadingWorker::SaveUserMaskTIFF);
connect(menuBar, &JFJochViewerMenu::loadUserMaskTiffSelected,
reading_worker, &JFJochImageReadingWorker::LoadUserMaskTIFF);
connect(menuBar, &JFJochViewerMenu::uploadUserMaskSelected,
reading_worker, &JFJochImageReadingWorker::UploadUserMask);
connect(this, &JFJochViewerWindow::datasetReady,
tableWindow, &JFJochViewerImageListWindow::datasetLoaded);
connect(this, &JFJochViewerWindow::datasetReady,
spotWindow, &JFJochViewerSpotListWindow::datasetLoaded);
connect(this, &JFJochViewerWindow::datasetReady,
metadataWindow, &JFJochViewerMetadataWindow::datasetLoaded);
connect(this, &JFJochViewerWindow::imageReady,
tableWindow, &JFJochViewerImageListWindow::imageLoaded);
connect(this, &JFJochViewerWindow::imageReady,
spotWindow, &JFJochViewerSpotListWindow::imageLoaded);
connect(tableWindow, &JFJochViewerImageListWindow::imageSelected,
reading_worker, &JFJochImageReadingWorker::LoadImage);
connect(reading_worker, &JFJochImageReadingWorker::autoloadChanged,
toolBarImage, &JFJochViewerToolbarImage::setAutoloadMode);
connect(toolBarImage, &JFJochViewerToolbarImage::autoLoadButtonPressed,
reading_worker, &JFJochImageReadingWorker::setAutoLoadMode);
connect(toolBarImage, &JFJochViewerToolbarImage::imageJumpChanged,
reading_worker, &JFJochImageReadingWorker::setAutoLoadJump);
// 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);
connect(side_panel, &JFJochViewerSidePanel::findBeamCenter,
reading_worker, &JFJochImageReadingWorker::FindCenter);
connect(this, &JFJochViewerWindow::datasetReady,
reflectionWindow, &JFJochViewerReflectionListWindow::datasetLoaded);
connect(this, &JFJochViewerWindow::imageReady,
reflectionWindow, &JFJochViewerReflectionListWindow::imageLoaded);
connect(reflectionWindow, &JFJochHelperWindow::zoom, viewer, &JFJochDiffractionImage::centerOnSpot);
connect(spotWindow, &JFJochHelperWindow::zoom, viewer, &JFJochDiffractionImage::centerOnSpot);
connect(side_panel, &JFJochViewerSidePanel::showSpots,
viewer, &JFJochDiffractionImage::showSpots);
connect(side_panel, &JFJochViewerSidePanel::showPredictions,
viewer, &JFJochDiffractionImage::showPredictions);
connect(side_panel, &JFJochViewerSidePanel::showROILabels,
viewer, &JFJochDiffractionImage::showROILabels);
connect(side_panel, &JFJochViewerSidePanel::showROIFill,
viewer, &JFJochDiffractionImage::showROIFill);
connect(side_panel, &JFJochViewerSidePanel::setFeatureColor,
viewer, &JFJochDiffractionImage::setFeatureColor);
connect(side_panel, &JFJochViewerSidePanel::setFeatureColor,
calibrationWindow, &JFJochCalibrationWindow::setFeatureColor);
connect(side_panel, &JFJochViewerSidePanel::setSpotColor,
viewer, &JFJochDiffractionImage::setSpotColor);
connect(side_panel, &JFJochViewerSidePanel::showHighestPixels,
viewer, &JFJochDiffractionImage::showHighestPixels);
connect(side_panel, &JFJochViewerSidePanel::showSaturatedPixels,
viewer, &JFJochDiffractionImage::showSaturation);
connect(side_panel, &JFJochViewerSidePanel::showBeamStop,
viewer, &JFJochDiffractionImage::showBeamStop);
connect(viewer, &JFJochDiffractionImage::writeStatusBar,
statusbar, &JFJochViewerStatusBar::display);
connect(side_panel, &JFJochViewerSidePanel::writeStatusBar,
statusbar, &JFJochViewerStatusBar::display);
// Detector connection / broker state / live readouts in the status bar
connect(reading_worker, &JFJochImageReadingWorker::brokerStatusUpdated,
statusbar, &JFJochViewerStatusBar::setBrokerStatus);
connect(reading_worker, &JFJochImageReadingWorker::httpConnectionChanged,
statusbar, &JFJochViewerStatusBar::setHttpConnection);
connect(reading_worker, &JFJochImageReadingWorker::autoloadChanged,
statusbar, &JFJochViewerStatusBar::setAutoloadMode);
connect(reading_worker, &JFJochImageReadingWorker::imageNumberChanged,
statusbar, &JFJochViewerStatusBar::setImageNumber);
connect(reading_worker, &JFJochImageReadingWorker::liveRateChanged,
statusbar, &JFJochViewerStatusBar::setLiveRate);
connect(metadataWindow, &JFJochViewerMetadataWindow::datasetUpdated,
reading_worker, &JFJochImageReadingWorker::UpdateDataset);
connect(reading_worker, &JFJochImageReadingWorker::setRings,
side_panel, &JFJochViewerSidePanel::SetRings);
connect(side_panel, &JFJochViewerSidePanel::resRingsSet,
viewer, &JFJochDiffractionImage::setResolutionRing);
connect(side_panel, &JFJochViewerSidePanel::ringModeSet,
viewer, &JFJochDiffractionImage::setResolutionRingMode);
connect(side_panel, &JFJochViewerSidePanel::highlightIceRings,
viewer, &JFJochDiffractionImage::highlightIceRings);
connect(side_panel, &JFJochViewerSidePanel::hideUnindexedSpots,
viewer, &JFJochDiffractionImage::hideUnindexedSpots);
connect(side_panel, &JFJochViewerSidePanel::hideIceRingSpots,
viewer, &JFJochDiffractionImage::hideIceRingSpots);
connect(side_panel, &JFJochViewerSidePanel::outlineSpots,
viewer, &JFJochDiffractionImage::outlineSpots);
connect(calibrationWindow, &JFJochCalibrationWindow::loadCalibration,
reading_worker, &JFJochImageReadingWorker::LoadCalibration);
connect(this, &JFJochViewerWindow::datasetReady,
calibrationWindow, &JFJochCalibrationWindow::datasetLoaded);
connect(reading_worker, &JFJochImageReadingWorker::simpleImageLoaded,
calibrationWindow, &JFJochCalibrationWindow::calibrationLoaded);
connect(azintImageWindow, &JFJoch2DAzintImageWindow::zoomOnBin,
viewer, &JFJochDiffractionImage::centerOnSpot);
// --- Magnifier ---
// The magnifier shows the frame the main view has already rendered - the same pixels with
// its own zoom and centre - so it neither converts nor colours anything itself.
connect(viewer, &JFJochImage::frameRendered, magnifier, [this] {
magnifier->setFrame(viewer->Frame());
});
// ... and the raw counts behind it, for the per-pixel labels. Stores a pointer, nothing more.
connect(this, &JFJochViewerWindow::imageReady,
magnifier, &JFJochViewerMagnifier::setPixelValues);
connect(viewer, &JFJochDiffractionImage::magnifierMoved,
magnifier, &JFJochViewerMagnifier::centerAt);
connect(magnifier, &JFJochViewerMagnifier::fieldChanged,
viewer, &JFJochDiffractionImage::setMagnifierField);
// Ensure worker is deleted in its own thread when the thread stops
connect(reading_thread, &QThread::finished, reading_worker, &QObject::deleteLater);
// lastDataset and lastImage are captured in OnDatasetReady / OnImageReady, which see every
// dataset / frame before fanning them out.
connect(this, &JFJochViewerWindow::imageReady,
toolBarDisplay, &JFJochViewerToolbarDisplay::imageLoaded);
connect(reading_worker, &JFJochImageReadingWorker::fileLoadError,
this, &JFJochViewerWindow::OnFileLoadError);
connect(reading_worker, &JFJochImageReadingWorker::fileLoadRetryStatus,
this, &JFJochViewerWindow::OnFileLoadRetryStatus);
connect(menuBar, &JFJochViewerMenu::openDatasetInfo, this, &JFJochViewerWindow::NewDatasetInfo);
NewDatasetInfo();
connect(this, &JFJochViewerWindow::adjustForegroundButton,
viewer, &JFJochDiffractionImage::adjustForeground);
connect(this, &JFJochViewerWindow::adjustBackgroundButton,
viewer, &JFJochDiffractionImage::adjustBackground);
connect(this, &JFJochViewerWindow::setAutoForeground,
viewer, &JFJochDiffractionImage::setAutoForeground);
connect(this, &JFJochViewerWindow::oneShotAutoForeground,
viewer, &JFJochDiffractionImage::oneShotAutoForeground);
connect(this, &JFJochViewerWindow::stepImage,
toolBarImage, &JFJochViewerToolbarImage::stepImage);
// See eventFilter: window-wide keyboard shortcuts, whatever widget has the focus.
qApp->installEventFilter(this);
// Processing jobs: run rugnux locally / copy a cluster command line, and surface
// finished runs as switchable dataset snapshots.
connect(processingJobsWindow, &JFJochProcessingJobsWindow::registerSnapshot,
reading_worker, &JFJochImageReadingWorker::RegisterProcessingSnapshot);
connect(processingJobsWindow, &JFJochProcessingJobsWindow::activateSnapshot,
reading_worker, &JFJochImageReadingWorker::SetActiveSnapshot);
connect(processingJobsWindow, &JFJochProcessingJobsWindow::writeStatusBar,
statusbar, &JFJochViewerStatusBar::display);
connect(processingJobsWindow, &JFJochProcessingJobsWindow::renameRun,
reading_worker, &JFJochImageReadingWorker::RenameRun);
connect(processingJobsWindow, &JFJochProcessingJobsWindow::removeRun,
reading_worker, &JFJochImageReadingWorker::RemoveRun);
connect(reading_worker, &JFJochImageReadingWorker::httpConnectionChanged,
processingJobsWindow, &JFJochProcessingJobsWindow::onHttpConnectionChanged);
connect(reading_worker, &JFJochImageReadingWorker::fileOpened,
processingJobsWindow, &JFJochProcessingJobsWindow::clearJobs);
connect(reading_worker, &JFJochImageReadingWorker::snapshotsChanged, processingJobsWindow,
[pw = processingJobsWindow](QStringList, QString active) { pw->setActiveRun(active); });
connect(reading_worker, &JFJochImageReadingWorker::runsChanged, this,
[this](QVector<RunData> runs, QString active) {
lastRuns = std::move(runs);
lastActiveRunId = std::move(active);
});
// Inline settings dock: the surgical MX/AzInt subset, always visible (left), wired straight
// into the running analysis so edits re-run on the current image / dataset.
auto *settingsPanel = new JFJochViewerSettingsDock(spot_finding_settings, indexing_settings,
experiment.GetAzimuthalIntegrationSettings(),
experiment.GetBraggIntegrationSettings(),
experiment.GetScalingSettings(), this);
// The panel's natural minimum (a long reference-consistency warning, the geometry rows, …) would
// otherwise pin the dock wide and stop it folding back; setMinimumWidth(0) lets the scroll area's
// own minimum (330) govern the fold. The horizontal scrollbar is AlwaysOff (like the inspector
// dock): with widgetResizable, both-axes ScrollBarAsNeeded oscillates (a vertical bar narrows the
// viewport -> a horizontal bar appears -> shortens it -> vertical fits -> ...), which pegs the CPU
// and freezes the panel when floated.
settingsPanel->setMinimumWidth(0);
auto *settingsScroll = new QScrollArea(this);
settingsScroll->setWidget(settingsPanel);
settingsScroll->setWidgetResizable(true);
settingsScroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
settingsScroll->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
settingsScroll->setMinimumWidth(330);
settingsScroll->setMaximumWidth(480);
settingsDock = new QDockWidget("Processing", this);
settingsDock->setObjectName("settingsDock");
settingsDock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea);
settingsDock->setWidget(settingsScroll); // scrollable: don't force the window taller than the screen
addDockWidget(Qt::LeftDockWidgetArea, settingsDock);
menuBar->AddDockEntry(settingsDock, "Processing");
// Files panel: an alternative to the processing panel in the same left-hand slot, tabbed together (the
// canonical Qt way to offer two panels one on top of the other without doubling the width) - most
// beamline users care about opening images, not reprocessing settings, so this one starts on top.
auto *fileBrowser = new JFJochViewerFileBrowser(this);
fileBrowserDock = new QDockWidget("Files", this);
fileBrowserDock->setObjectName("fileBrowserDock");
fileBrowserDock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea);
fileBrowserDock->setWidget(fileBrowser);
addDockWidget(Qt::LeftDockWidgetArea, fileBrowserDock);
tabifyDockWidget(settingsDock, fileBrowserDock);
fileBrowserDock->raise();
menuBar->AddDockEntry(fileBrowserDock, "Files");
connect(fileBrowser, &JFJochViewerFileBrowser::openDataset,
reading_worker, &JFJochImageReadingWorker::LoadFile);
connect(settingsPanel, &JFJochViewerSettingsDock::spotFindingChanged,
reading_worker, &JFJochImageReadingWorker::UpdateSpotFindingSettings);
connect(settingsPanel, &JFJochViewerSettingsDock::azintChanged,
reading_worker, &JFJochImageReadingWorker::UpdateAzintSettings);
connect(settingsPanel, &JFJochViewerSettingsDock::braggChanged,
reading_worker, &JFJochImageReadingWorker::UpdateBraggIntegrationSettings);
connect(settingsPanel, &JFJochViewerSettingsDock::scalingChanged,
reading_worker, &JFJochImageReadingWorker::UpdateScalingSettings);
// The two analysis actions now live on top of the panel.
connect(settingsPanel, &JFJochViewerSettingsDock::reanalyzeImage,
reading_worker, &JFJochImageReadingWorker::ReanalyzeImages);
connect(settingsPanel, &JFJochViewerSettingsDock::analyzeDataset,
processingJobsWindow, &JFJochProcessingJobsWindow::newJob);
// Grey out "Analyze dataset" on a live HTTP source (re-processing needs a stored file).
connect(reading_worker, &JFJochImageReadingWorker::httpConnectionChanged,
settingsPanel, &JFJochViewerSettingsDock::setHttpConnection);
connect(settingsPanel, &JFJochViewerSettingsDock::experimentChanged,
reading_worker, &JFJochImageReadingWorker::UpdateDataset);
connect(settingsPanel, &JFJochViewerSettingsDock::findBeamCenter,
reading_worker, &JFJochImageReadingWorker::FindCenter);
connect(this, &JFJochViewerWindow::datasetReady,
settingsPanel, &JFJochViewerSettingsDock::datasetLoaded);
connect(this, &JFJochViewerWindow::imageReady,
settingsPanel, &JFJochViewerSettingsDock::loadImage);
connect(settingsPanel, &JFJochViewerSettingsDock::ringsFromCalibration,
side_panel, &JFJochViewerSidePanel::SetRings);
connect(settingsPanel, &JFJochViewerSettingsDock::referenceSelected,
reading_worker, &JFJochImageReadingWorker::SetReferenceMtz);
connect(settingsPanel, &JFJochViewerSettingsDock::modelSelected,
reading_worker, &JFJochImageReadingWorker::SetModelPath);
connect(reading_worker, &JFJochImageReadingWorker::referenceMtzChanged,
settingsPanel, &JFJochViewerSettingsDock::referenceLoaded);
// Jobs panel: hidden by default and narrower; it reveals itself (to the right of the
// plots) only when a reprocessing job starts.
processingDock = new QDockWidget("Jobs", this);
processingDock->setObjectName("processingDock");
// Allow any area so dragging it out (floating) and re-docking behave; the table's natural width
// (its columns) must not pin the dock wide, so let it fold like the settings dock.
processingDock->setAllowedAreas(Qt::AllDockWidgetAreas);
processingDock->setFeatures(
QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable | QDockWidget::DockWidgetClosable);
processingJobsWindow->setMinimumWidth(0);
processingDock->setWidget(processingJobsWindow);
addDockWidget(Qt::BottomDockWidgetArea, processingDock);
if (lastDatasetInfoDock) {
splitDockWidget(lastDatasetInfoDock, processingDock, Qt::Horizontal);
resizeDocks({lastDatasetInfoDock, processingDock}, {900, 280}, Qt::Horizontal);
}
processingDock->hide();
menuBar->AddDockEntry(processingDock, "Jobs");
connect(processingJobsWindow, &JFJochProcessingJobsWindow::jobStarted, this, [this] {
processingDock->show();
processingDock->raise();
});
connect(menuBar, &JFJochViewerMenu::imageLayoutSelected, this,
[this] { ApplyPerspective(Perspective::Image); });
connect(menuBar, &JFJochViewerMenu::processingLayoutSelected, this,
[this] { ApplyPerspective(Perspective::Processing); });
connect(menuBar, &JFJochViewerMenu::resetLayoutSelected, this,
[this] { if (!defaultLayoutState.isEmpty()) restoreState(defaultLayoutState, kLayoutVersion); });
connect(menuBar, &JFJochViewerMenu::resetSettingsSelected, this, &JFJochViewerWindow::ResetAllSettings);
// Tear-off floating is disabled for all docks: a floated QDockWidget is unreliable across platforms
// (on WSLg it becomes an off-screen, override-redirect window that can't be focused or recovered, so
// the undocked panel goes dead). Docks stay movable between dock areas and closable, just not floated.
for (QDockWidget *dock : findChildren<QDockWidget *>())
dock->setFeatures(dock->features() & ~QDockWidget::DockWidgetFloatable);
// Remember the freshly-built layout so "Reset layout" can return to it, then restore the
// user's last-used arrangement if they have one (and if it matches the current layout version).
defaultLayoutState = saveState(kLayoutVersion);
QSettings settings("PSI", "jfjoch_viewer");
restoreGeometry(settings.value("geometry").toByteArray());
restoreState(settings.value("windowState").toByteArray(), kLayoutVersion);
magnifierWindow->restoreGeometry(settings.value("magnifierGeometry").toByteArray());
// Once the main window is up: opened from here, the window would end up behind it
if (settings.value("magnifierPoppedOut", false).toBool())
QTimer::singleShot(0, this, &JFJochViewerWindow::PopOutMagnifier);
// Display settings (color map, Auto, HDR) persist across sessions the same way the layout does.
const int colorMap = settings.value("colorMap", static_cast<int>(ColorScaleEnum::Indigo)).toInt();
viewer->setColorMap(colorMap);
toolBarDisplay->updateColorMap(colorMap);
const bool autoForeground = settings.value("autoForeground", false).toBool();
viewer->setAutoForeground(autoForeground); // emits autoForegroundChanged -> toolbar button state
const bool hdrMode = settings.value("hdrMode", false).toBool();
viewer->setHDRMode(hdrMode);
toolBarDisplay->updateHDRMode(hdrMode);
if (!file.isEmpty())
LoadFile(file, 0, 1, false);
}
JFJochViewerWindow::~JFJochViewerWindow() {
if (reading_thread && reading_thread->isRunning()) {
reading_thread->quit();
reading_thread->wait();
}
}
void JFJochViewerWindow::ApplyPerspective(Perspective p) {
// Image: just the image + inspector (+ file manager, on top of its tab with Processing).
// Processing: also dataset-info plots and the jobs panel, with the processing panel raised instead.
const bool processing = (p == Perspective::Processing);
if (inspectorDock) inspectorDock->setVisible(true);
// Processing and the file manager stay tabbed together either way; only which one is on top changes.
if (settingsDock) settingsDock->setVisible(true);
if (fileBrowserDock) fileBrowserDock->setVisible(true);
(processing ? settingsDock : fileBrowserDock)->raise();
if (processingDock && !processing) processingDock->hide(); // shows only when a job starts
for (auto *d : findChildren<QDockWidget *>())
if (d->objectName().startsWith("datasetInfoDock"))
d->setVisible(processing);
}
void JFJochViewerWindow::ResetAllSettings() {
const auto reply = QMessageBox::question(
this, "Reset all settings",
"Reset all viewer settings to defaults - window layout, display settings, plot choices "
"and the file manager root?",
QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
if (reply != QMessageBox::Yes)
return;
QSettings("PSI", "jfjoch_viewer").clear();
// Bring the running session to the state a first start builds, so normal saving on close is
// correct again afterwards. Same calls as the constructor, in the same order.
magnifierWindow->close(); // docks the magnifier back, if it was popped out
magnifierWindow->resize(500, 500);
if (!defaultLayoutState.isEmpty())
restoreState(defaultLayoutState, kLayoutVersion);
showNormal();
const QRect avail = QGuiApplication::primaryScreen()->availableGeometry();
resize(qMin(1200, avail.width() - 100), qMin(1100, avail.height() - 100));
viewer->setColorMap(static_cast<int>(ColorScaleEnum::Indigo));
toolBarDisplay->updateColorMap(static_cast<int>(ColorScaleEnum::Indigo));
viewer->setAutoForeground(false); // emits autoForegroundChanged -> toolbar button state
viewer->setHDRMode(false);
toolBarDisplay->updateHDRMode(false);
for (auto *info : findChildren<JFJochViewerDatasetInfo *>())
info->resetPlotDefaults();
for (auto *browser : findChildren<JFJochViewerFileBrowser *>())
browser->ResetToDefaultRoot();
menuBar->ResetFontZoom();
}
// The inspector's width bounds are measured from its content: at least what its widest section needs
// (folded ones included, see CollapsibleSection) plus the scroll bar, so nothing is cut off, and a third
// more at most - wider only adds empty space next to the text, and takes it from the image. Measured
// rather than counted in characters, as they once were, because a character count that suits one
// platform's font is far too wide on another's (600 px for 400 px of content on macOS).
void JFJochViewerWindow::UpdateInspectorWidth() {
const int need = inspectorScroll->widget()->minimumSizeHint().width()
+ inspectorScroll->verticalScrollBar()->sizeHint().width();
inspectorScroll->setMinimumWidth(need);
inspectorScroll->setMaximumWidth(need * 4 / 3);
}
void JFJochViewerWindow::changeEvent(QEvent *event) {
QMainWindow::changeEvent(event);
// The menu bar sets the application font while the window is still being built, so the inspector
// is not there yet on the first one. The window hears of a font change before its children do,
// so the content is measured once they have all taken the new font.
if (event->type() == QEvent::FontChange && inspectorScroll)
QTimer::singleShot(0, this, &JFJochViewerWindow::UpdateInspectorWidth);
}
// The image comes first. When shrinking the window would leave it narrower than the inspector next
// to it, the inspector folds away, with the magnifier if it shares the inspector's column. They come
// back once the image would still be at least as wide as the inspector with it restored (plus a
// margin, so a window resized around the threshold does not flicker). Only a shrink folds: a panel
// the user reopens on a narrow window stays open, and one the user closed is never brought back.
void JFJochViewerWindow::resizeEvent(QResizeEvent *event) {
QMainWindow::resizeEvent(event);
if (!inspectorDock || !magnifierDock)
return;
const int image_width = centralWidget()->width();
const bool shrinking = event->oldSize().width() < 0 || event->size().width() < event->oldSize().width();
if (shrinking && inspectorDock->isVisible() && image_width < inspectorDock->width()) {
autoHiddenWidth = inspectorDock->width();
QList<QDockWidget *> docks = {inspectorDock};
if (magnifierDock->isVisible() && dockWidgetArea(magnifierDock) == dockWidgetArea(inspectorDock))
docks.append(magnifierDock);
for (auto *dock: docks) {
dock->hide();
if (!autoHiddenDocks.contains(dock))
autoHiddenDocks.append(dock);
}
} else if (!autoHiddenDocks.isEmpty() && image_width - autoHiddenWidth >= autoHiddenWidth + 40) {
for (auto *dock: std::exchange(autoHiddenDocks, {}))
dock->show();
}
}
void JFJochViewerWindow::closeEvent(QCloseEvent *event) {
// Persist the dock/toolbar arrangement and window geometry so the next launch resumes it. Docks
// folded away only for a narrow window are saved as open: the next start may have the room.
for (auto *dock: std::exchange(autoHiddenDocks, {}))
dock->show();
QSettings settings("PSI", "jfjoch_viewer");
settings.setValue("geometry", saveGeometry());
settings.setValue("windowState", saveState(kLayoutVersion));
settings.setValue("magnifierGeometry", magnifierWindow->saveGeometry());
settings.setValue("magnifierPoppedOut", magnifierWindow->isVisible());
settings.setValue("colorMap", toolBarDisplay->colorMap());
settings.setValue("autoForeground", toolBarDisplay->autoForeground());
settings.setValue("hdrMode", toolBarDisplay->hdrMode());
QMainWindow::closeEvent(event);
}
void JFJochViewerWindow::PopOutMagnifier() {
magnifierWindow->setCentralWidget(magnifier);
magnifierDock->hide();
magnifier->setPoppedOut(true);
magnifierWindow->open();
}
void JFJochViewerWindow::LoadFile(const QString &filename, qint64 image_number, qint64 summation, bool retry) {
emit LoadFileRequest(filename, image_number, summation, true);
FollowInFileBrowser(filename);
}
void JFJochViewerWindow::SetHttpToken(const QString &token) {
menuBar->setHttpToken(token);
emit HttpTokenRequest(token);
}
void JFJochViewerWindow::FollowInFileBrowser(const QString &filename) {
for (auto *browser : findChildren<JFJochViewerFileBrowser *>())
browser->FollowPath(filename);
}
void JFJochViewerWindow::LoadImage(qint64 image_number, qint64 summation) {
emit LoadImageRequest(image_number, summation);
}
void JFJochViewerWindow::OnImageReady(std::shared_ptr<const JFJochReaderImage> image) {
lastImage = image; // newest frame, for dataset-info docks opened later
// On a remote display every frame shown is a full-window upload, so a fast producer (live
// follow, movie playback, scrubbing) is displayed at the hover cadence: only the newest frame
// is kept and the intermediates go undisplayed. The ack is still per frame - the in-flight
// counter is all it feeds - so the producer never waits on the display.
if (RemoteDisplayMode()) {
pendingImage = std::move(image);
if (!image_rate_.isValid() || image_rate_.elapsed() >= kRemoteRepaintIntervalMs) {
image_tail_timer_->stop();
DeliverPendingImage();
} else {
image_tail_timer_->start(kRemoteRepaintIntervalMs);
}
emit imageConsumed();
return;
}
emit imageReady(image); // fan out to every GUI consumer synchronously (same thread)
emit imageConsumed(); // ack the worker so it may fetch/produce the next frame
}
void JFJochViewerWindow::DeliverPendingImage() {
image_rate_.restart();
if (!pendingImage)
return;
emit imageReady(std::exchange(pendingImage, nullptr));
}
void JFJochViewerWindow::OnDatasetReady(std::shared_ptr<const JFJochReaderDataset> dataset) {
lastDataset = dataset; // newest dataset, for dataset-info docks opened later
emit datasetReady(dataset); // fan out to every GUI consumer synchronously (same thread)
emit datasetConsumed(); // ack the worker so it may produce the next dataset refresh
}
void JFJochViewerWindow::NewDatasetInfo() {
auto info = new JFJochViewerDatasetInfo(this);
info->datasetLoaded(lastDataset);
info->imageLoaded(lastImage);
info->runsChanged(lastRuns, lastActiveRunId);
auto dock = new QDockWidget(QString("Dataset info"), this);
dock->setObjectName(QStringLiteral("datasetInfoDock%1").arg(datasetInfoCounter++));
dock->setAllowedAreas(Qt::BottomDockWidgetArea);
dock->setFeatures(
QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable | QDockWidget::DockWidgetClosable |
QDockWidget::DockWidgetVerticalTitleBar);
dock->setAttribute(Qt::WA_DeleteOnClose);
dock->setWidget(info);
addDockWidget(Qt::BottomDockWidgetArea, dock);
// Place additional plots beside the previous one so "+ Plot" gives a visible row of plots.
if (lastDatasetInfoDock)
splitDockWidget(lastDatasetInfoDock, dock, Qt::Horizontal);
lastDatasetInfoDock = dock;
// Wire signals like the initial dataset_info
connect(this, &JFJochViewerWindow::datasetReady,
info, &JFJochViewerDatasetInfo::datasetLoaded);
connect(this, &JFJochViewerWindow::imageReady,
info, &JFJochViewerDatasetInfo::imageLoaded);
// All runs (original + reprocessing snapshots) overlay as separate lines.
connect(reading_worker, &JFJochImageReadingWorker::runsChanged,
info, &JFJochViewerDatasetInfo::runsChanged);
// Live processing results: the in-progress run as its own overlay line.
connect(processingJobsWindow, &JFJochProcessingJobsWindow::liveDataset,
info, &JFJochViewerDatasetInfo::liveRunUpdated);
// A live (HTTP) source defaults the plot to spots + background.
connect(reading_worker, &JFJochImageReadingWorker::httpConnectionChanged,
info, &JFJochViewerDatasetInfo::setHttpConnection);
connect(info, &JFJochViewerDatasetInfo::imageSelected,
reading_worker, &JFJochImageReadingWorker::LoadImage);
connect(info, &JFJochViewerDatasetInfo::addPlot, this, &JFJochViewerWindow::NewDatasetInfo);
connect(toolBarDisplay, &JFJochViewerToolbarDisplay::colorMapChanged,
info, &JFJochViewerDatasetInfo::setColorMap);
connect(info, &JFJochViewerDatasetInfo::writeStatusBar,
statusbar, &JFJochViewerStatusBar::display);
}
bool JFJochViewerWindow::eventFilter(QObject *obj, QEvent *event) {
// The filter sees every key of this window before the focused widget does, so F/B/A and the
// frame-navigation keys work with any panel focused, not only the image. It steps aside when
// the focused widget genuinely uses the key: any text-entry widget for all of them, item
// views (the file tree) for the navigation keys, and everything while a popup is open.
const auto type = event->type();
if ((type == QEvent::KeyPress || type == QEvent::KeyRelease)
&& QApplication::activeWindow() == this && !QApplication::activePopupWidget()) {
const auto *ke = static_cast<QKeyEvent *>(event);
const int key = ke->key();
// Shift is released wherever the focus is; the magnifier frame must not outlive it
if (type == QEvent::KeyRelease && key == Qt::Key_Shift)
viewer->hideMagnifierFrame();
QWidget *focus = QApplication::focusWidget();
const bool editing = qobject_cast<QLineEdit *>(focus) || qobject_cast<QAbstractSpinBox *>(focus)
|| qobject_cast<QComboBox *>(focus) || qobject_cast<QTextEdit *>(focus)
|| qobject_cast<QPlainTextEdit *>(focus);
const bool plain = (ke->modifiers() & ~Qt::KeypadModifier) == Qt::NoModifier;
if (!editing && plain) {
if (type == QEvent::KeyPress) {
switch (key) {
case Qt::Key_F: emit adjustForegroundButton(true); return true;
case Qt::Key_B: emit adjustBackgroundButton(true); return true;
case Qt::Key_A:
if (!ke->isAutoRepeat())
emit oneShotAutoForeground();
return true;
default: break;
}
if (!qobject_cast<QAbstractItemView *>(focus)) {
switch (key) {
case Qt::Key_Home: emit stepImage(std::numeric_limits<int>::min()); return true;
case Qt::Key_End: emit stepImage(std::numeric_limits<int>::max()); return true;
case Qt::Key_PageUp: emit stepImage(1); return true;
case Qt::Key_PageDown: emit stepImage(-1); return true;
default: break;
}
}
} else {
if (key == Qt::Key_F) { emit adjustForegroundButton(false); return true; }
if (key == Qt::Key_B) { emit adjustBackgroundButton(false); return true; }
}
}
}
return QMainWindow::eventFilter(obj, event);
}
void JFJochViewerWindow::OnFileLoadError(QString title, QString message) {
QMessageBox::critical(this, title, message);
}
void JFJochViewerWindow::OnFileLoadRetryStatus(bool active, QString message) {
if (active) {
if (!retryDialog) {
retryDialog = new QProgressDialog(this);
retryDialog->setWindowModality(Qt::WindowModal);
retryDialog->setRange(0, 0); // Infinite/Busy indicator
retryDialog->setCancelButton(nullptr); // Disable cancel for now
retryDialog->setMinimumDuration(0); // Show immediately
retryDialog->setWindowTitle("Loading File");
}
retryDialog->setLabelText(message);
retryDialog->show();
} else {
if (retryDialog) {
retryDialog->close();
retryDialog->deleteLater();
retryDialog = nullptr;
}
}
}