Files
Jungfraujoch/viewer/JFJochViewerWindow.cpp
leonarski_fandFilip Leonarski 19b135186d
Build Packages / Create release (push) Successful in 7s
Build Packages / build:portable:macos-arm64:nocuda (push) Successful in 4m56s
Build Packages / build:rugnux:linux-aarch64:cuda (push) Successful in 9m10s
Build Packages / build:portable:linux-x86_64:nocuda (push) Successful in 13m51s
Build Packages / build:portable:linux-x86_64:cuda (push) Successful in 16m56s
Build Packages / build:jfjoch:rocky8:nocuda (push) Successful in 20m59s
Build Packages / build:portable:windows-x86_64:nocuda (push) Successful in 21m25s
Build Packages / build:jfjoch:rocky9:nocuda (push) Successful in 21m17s
Build Packages / build:jfjoch:ubuntu2204:nocuda (push) Successful in 14m14s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 25m59s
Build Packages / build:portable:windows-x86_64:cuda (push) Successful in 25m48s
Build Packages / build:jfjoch:ubuntu2404:nocuda (push) Successful in 15m52s
Build Packages / build:jfjoch:rocky8:cuda-sls9 (push) Successful in 19m33s
Build Packages / Generate python client (push) Successful in 29s
Build Packages / Build documentation (push) Successful in 1m19s
Build Packages / build:jfjoch:ubuntu2204:cuda (push) Successful in 20m49s
Build Packages / build:jfjoch:rocky9:cuda-sls9 (push) Successful in 25m31s
Build Packages / build:jfjoch:rocky8:cuda (push) Successful in 25m24s
Build Packages / build:jfjoch:rocky9:cuda (push) Successful in 26m5s
Build Packages / build:jfjoch:ubuntu2404:cuda (push) Successful in 22m19s
Build Packages / Unit tests (push) Failing after 2h28m24s
Build Packages / Validation Battery (push) Successful in 40m33s
v1.0.0-rc.175 (#85)
* Rugnux: Processes a collection live while jfjoch_broker writes it (`rugnux <master.h5> <broker URL>`).
* Rugnux: Processing starts on the first images and confirms its choices once all are in, so analysis can begin during collection.
* Rugnux: Faster processing, especially on servers with several GPUs and CPU sockets and on computers without a GPU (`--no-gpu`, macOS).
* Rugnux: Uses much less GPU memory, so large data sets stay on the GPU of smaller cards, and a data set too large for the card is processed on the CPU instead.
* Rugnux: On macOS, Bragg integration runs on the Apple GPU.
* Rugnux: Better handling of twinning and pseudo-symmetry, with the twin hypothesis written as `<prefix>_twin_<SG>.mtz`, and a wrong `--model` no longer confirms the data or decides the hand.
* Rugnux: `--model` accepts predicted (AlphaFold) models and falls back to molecular replacement when the model is not placed as a rigid body (experimental `--mr` forces it), settling space group, hand and twinning and writing a different answer as `<prefix>_mr.mtz`.
* Rugnux: Molecular replacement is faster and settles the space group in more cases with several copies in the asymmetric unit.
* Rugnux: Deposition-ready mmCIF, a richer report, and numbered steps on the terminal with the full log in `<prefix>_log.txt`; `_P1.mtz`, `--estimate-beam-center` and `--beam-center-search` are removed.
* Rugnux: The beam-stop shadow is found correctly on tilted detectors.
* Rugnux: Stills files that record a stationary goniometer axis get the default stills geometry refinement.
* jfjoch_viewer: "Analyze dataset" uses the same defaults as a bare `rugnux <file>`, and shows the results of processed CBF, marCCD and SMV sweeps.
* jfjoch_viewer: The Files panel refreshes on F5 and every few seconds, and the plot type and other selections stay put during live collection.
* jfjoch_writer: Memory stays bounded and old data files are closed efficiently on long runs, any failure ends the run in an error state the broker is told about, and the master file appears only after its data files; the ZeroMQ pusher logs when a stalled writer makes it drop images.
* Fixes: The TCP image pusher no longer blocks the broker's receiver, GPU out-of-memory failures stop with a clear message, and the XDS plugin masks module and chip edges.

---------

Co-authored-by: Filip Leonarski <jungfrau@filips-mac-mini.home>
Reviewed-on: #85
Co-authored-by: Filip Leonarski <filip.leonarski@psi.ch>
2026-10-16 15:34:43 +02:00

1009 lines
51 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 <QToolButton>
#include <QDesktopServices>
#include <QUrl>
#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 "ViewerTheme.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, linking to the PSI website. There are four interchangeable dot
// designs; pick one at random each launch, for fun. The logo is black only, so the negative for the
// dark theme is the same image with its colours inverted.
{
const int n = QRandomGenerator::global()->bounded(1, 5); // 1..4
// As tall as the menu entries make the bar, so the logo does not stretch it.
const QImage logoImage = QImage(QStringLiteral(":/psi_%1.png").arg(n, 2, 10, QLatin1Char('0')))
.convertToFormat(QImage::Format_ARGB32)
.scaledToHeight(menuBar->sizeHint().height(), Qt::SmoothTransformation);
auto *logo = new QToolButton(this);
logo->setAutoRaise(true);
// No button padding or frame, which would make the bar taller all the same.
logo->setStyleSheet("QToolButton { padding: 0px 8px 0px 6px; border: none; }");
logo->setCursor(Qt::PointingHandCursor);
logo->setToolTip("Paul Scherrer Institute - www.psi.ch");
logo->setIconSize(logoImage.size());
auto setLogoIcon = [logo, logoImage] {
QImage image = logoImage;
if (DarkTheme())
image.invertPixels(QImage::InvertRgb);
logo->setIcon(QPixmap::fromImage(image));
};
setLogoIcon();
connect(ThemeNotifier::Instance(), &ThemeNotifier::changed, logo, setLogoIcon);
connect(logo, &QToolButton::clicked, [] { QDesktopServices::openUrl(QUrl("https://www.psi.ch")); });
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));
// The interactive viewer is an offline tool looking at one dataset at a time, so it starts from what
// a bare `rugnux` uses rather than numbers the user would have to tune: the per-ring adaptive
// threshold, min-pix chosen per image (the settings dock fixes it for rotation, as rugnux does), and
// detection out to the detector edge. All three are switchable in the settings dock.
SpotFindingSettings spot_finding_settings = RugnuxDefaultSpotFinding(false);
// Auto, as in rugnux: the GPU FFT where there is a GPU, FFTW on the CPU otherwise.
IndexingSettings indexing_settings;
indexing_settings.IndexingThreads(1);
indexing_settings.Algorithm(IndexingAlgorithmEnum::Auto);
// Fewer FFT directions keep single-image indexing interactive on the CPU; a processing job puts the
// rugnux number back (JFJochProcessingJobsWindow::newJob).
if (get_gpu_count() == 0)
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);
// Both arrive after the datasetLoaded that fills the panel from the new file / run.
connect(reading_worker, &JFJochImageReadingWorker::fileOpened,
settingsPanel, &JFJochViewerSettingsDock::forgetStoredCrystal);
connect(reading_worker, &JFJochImageReadingWorker::snapshotsChanged,
settingsPanel, &JFJochViewerSettingsDock::forgetStoredCrystal);
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);
menuBar->AddViewToggle(toolBarDisplay->backgroundSliderAction());
toolBarDisplay->showBackgroundSlider(settings.value("backgroundSlider", false).toBool());
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);
toolBarDisplay->showBackgroundSlider(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());
settings.setValue("backgroundSlider", toolBarDisplay->backgroundSliderShown());
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;
}
}
}