Files
Jungfraujoch/viewer/JFJochViewerWindow.cpp
T
leonarski_fandClaude Opus 5 d2ce65f857 Viewer: magnifier displays the frame the main view already rendered
The magnifier and the main view are two views of the same image at different
position and zoom, but the magnifier ran the whole pipeline again on its own
copy: it wrapped the same int32 buffer in a SimpleImage, converted it to float,
coloured every pixel and kept its own full-size QImage. That is a second
conversion and two extra full-detector buffers (20 MB at 2.8 Mpx, 138 MB at
18 Mpx) to feed a 320x320 window.

Separate producing a frame from displaying one:

- JFJochImage keeps the rendered frame in a shared_ptr<QImage> (the pointer is
  stable for the widget's lifetime; only the contents change, so the existing
  buffer reuse is unaffected), publishes it via Frame() and announces new
  pixels with frameRendered().
- JFJochImageItem holds that shared_ptr instead of a reference to a member of
  its owner, which also removes a lifetime coupling.
- JFJochFollowerImage is a small read-only view of such a frame with its own
  zoom and centre. It shows only the image: overlays, ROI tools and per-pixel
  labels belong to the view that owns the data.
- The magnifier becomes one of those, fed from frameRendered().

Consequences beyond the saving: the magnifier now agrees with the main view on
colour map, contrast and HDR mode, which it never did -- it was wired to
neither, so it always drew with its own defaults. And the visibility guard
added in 6d1af4921 is gone: there is no longer any per-frame work to skip, so
nothing needs guarding. That guard was a workaround for this design.

Stepping 30 frames with the magnifier open: 5550 -> 4810 ms CPU, which is what
it costs with the magnifier closed (4770 ms) -- it is now free either way.

Verified: main image panel and a drag-pan stay pixel-identical to the
pre-refactor binary (AE=0); the magnifier follows the cursor, updates on a new
frame, and now tracks a colour-map change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 21:11:42 +02:00

721 lines
36 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 <QScreen>
#include <QScrollArea>
#include <QSettings>
#include <QCloseEvent>
#include <QRandomGenerator>
#include <QLabel>
#include <QPainter>
#include <QPushButton>
#include "JFJochImageReadingWorker.h"
#include "image_viewer/JFJochDiffractionImage.h"
#include "JFJochViewerSidePanel.h"
#include "widgets/JFJochViewerSettingsDock.h"
#include "widgets/JFJochViewerImageStrip.h"
#include "JFJochViewerStatusBar.h"
#include "../common/CUDAWrapper.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 "windows/JFJochViewerReciprocalSpaceWindow.h"
#include "toolbar/JFJochViewerToolbarDisplay.h"
#include "toolbar/JFJochViewerToolbarImage.h"
#include "windows/JFJoch2DAzintImageWindow.h"
#include "windows/JFJochMagnifierWindow.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 = 2;
JFJochViewerWindow::JFJochViewerWindow(QWidget *parent, bool dbus, const QString &file) : QMainWindow(parent) {
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);
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();
spot_finding_settings.high_resolution_limit = 1.5;
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);
// Central area: the diffraction image. Everything else (inspector, plots, processing) is a
// dock, so the layout can be rearranged, saved, and switched between perspectives.
auto viewer = new JFJochDiffractionImage(this);
viewer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
setCentralWidget(viewer);
auto side_panel = new JFJochViewerSidePanel(this);
auto side_panel_scroll = new QScrollArea(this);
side_panel_scroll->setWidget(side_panel);
side_panel_scroll->setWidgetResizable(true);
side_panel_scroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
side_panel_scroll->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
side_panel_scroll->setMinimumWidth(450);
side_panel_scroll->setMaximumWidth(600);
inspectorDock = new QDockWidget("Inspector", this);
inspectorDock->setObjectName("inspectorDock");
inspectorDock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea);
inspectorDock->setWidget(side_panel_scroll);
addDockWidget(Qt::RightDockWidgetArea, inspectorDock);
menuBar->AddDockEntry(inspectorDock, "Inspector");
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 reciprocalWindow = new JFJochViewerReciprocalSpaceWindow(this);
auto azintImageWindow = new JFJoch2DAzintImageWindow(this);
auto magnifierWindow = new JFJochMagnifierWindow(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(reciprocalWindow, "Reciprocal space viewer");
menuBar->AddWindowEntry(azintImageWindow, "Azimuthal integration 2D image");
menuBar->AddWindowEntry(magnifierWindow, "Magnifier");
// 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::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(toolBarDisplay, &JFJochViewerToolbarDisplay::setForeground, viewer,
&JFJochDiffractionImage::changeForeground);
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(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(calibrationWindow, &JFJochCalibrationWindow::loadCalibration,
reading_worker, &JFJochImageReadingWorker::LoadCalibration);
connect(this, &JFJochViewerWindow::datasetReady,
calibrationWindow, &JFJochCalibrationWindow::datasetLoaded);
connect(reading_worker, &JFJochImageReadingWorker::simpleImageLoaded,
calibrationWindow, &JFJochCalibrationWindow::calibrationLoaded);
connect(this, &JFJochViewerWindow::datasetReady,
reciprocalWindow, &JFJochViewerReciprocalSpaceWindow::datasetLoaded);
connect(this, &JFJochViewerWindow::imageReady,
reciprocalWindow, &JFJochViewerReciprocalSpaceWindow::imageLoaded);
connect(reciprocalWindow, &JFJochViewerReciprocalSpaceWindow::loadSpotsRequest,
reading_worker, &JFJochImageReadingWorker::LoadSpots);
connect(reading_worker, &JFJochImageReadingWorker::spotsLoaded,
reciprocalWindow, &JFJochViewerReciprocalSpaceWindow::spotsLoaded);
connect(reciprocalWindow, &JFJochHelperWindow::zoom,
viewer, &JFJochDiffractionImage::centerOnSpot);
connect(side_panel, &JFJochViewerSidePanel::setSpotColor,
reciprocalWindow, &JFJochViewerReciprocalSpaceWindow::setSpotColor);
connect(side_panel, &JFJochViewerSidePanel::setFeatureColor,
reciprocalWindow, &JFJochViewerReciprocalSpaceWindow::setFeatureColor);
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, magnifierWindow, [viewer, magnifierWindow] {
magnifierWindow->setFrame(viewer->Frame());
});
connect(viewer, &JFJochImage::hoverScenePos,
magnifierWindow, &JFJochMagnifierWindow::centerAt);
// 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::setAutoForeground,
viewer, &JFJochDiffractionImage::setAutoForeground);
// 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("Settings", 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, "Settings");
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(reading_worker, &JFJochImageReadingWorker::referenceMtzChanged,
settingsPanel, &JFJochViewerSettingsDock::referenceLoaded);
// Processing panel: hidden by default and narrower; it reveals itself (to the right of the
// plots) only when a reprocessing job starts.
processingDock = new QDockWidget("Processing", 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, "Processing");
connect(processingJobsWindow, &JFJochProcessingJobsWindow::jobStarted, this, [this] {
processingDock->show();
processingDock->raise();
});
// Image strip / hit feed: thumbnails of representative images, rendered off-thread; click opens.
// Stacked below the plots — both want vertical room, and the strip needs less of it.
auto *imageStrip = new JFJochViewerImageStrip(this);
imageStripDock = new QDockWidget("Image strip", this);
imageStripDock->setObjectName("imageStripDock");
imageStripDock->setWidget(imageStrip);
addDockWidget(Qt::BottomDockWidgetArea, imageStripDock);
if (lastDatasetInfoDock) {
splitDockWidget(lastDatasetInfoDock, imageStripDock, Qt::Vertical);
resizeDocks({lastDatasetInfoDock, imageStripDock}, {260, 140}, Qt::Vertical);
}
menuBar->AddDockEntry(imageStripDock, "Image strip");
connect(this, &JFJochViewerWindow::datasetReady,
imageStrip, &JFJochViewerImageStrip::datasetLoaded);
connect(reading_worker, &JFJochImageReadingWorker::fileOpened,
imageStrip, &JFJochViewerImageStrip::resetForNewFile);
connect(imageStrip, &JFJochViewerImageStrip::requestThumbnails,
reading_worker, &JFJochImageReadingWorker::RenderThumbnails);
connect(reading_worker, &JFJochImageReadingWorker::thumbnailReady,
imageStrip, &JFJochViewerImageStrip::thumbnailReady);
connect(imageStrip, &JFJochViewerImageStrip::imageSelected,
reading_worker, &JFJochImageReadingWorker::LoadImage);
connect(toolBarDisplay, &JFJochViewerToolbarDisplay::colorMapChanged,
reading_worker, &JFJochImageReadingWorker::SetThumbnailColorMap);
connect(side_panel, &JFJochViewerSidePanel::setFeatureColor,
reading_worker, &JFJochImageReadingWorker::SetThumbnailFeatureColor);
connect(side_panel, &JFJochViewerSidePanel::setSpotColor,
reading_worker, &JFJochImageReadingWorker::SetThumbnailSpotColor);
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); });
// 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);
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. Processing: also settings, dataset-info plots, jobs panel.
const bool processing = (p == Perspective::Processing);
if (inspectorDock) inspectorDock->setVisible(true);
if (settingsDock) settingsDock->setVisible(processing);
if (imageStripDock) imageStripDock->setVisible(processing);
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::closeEvent(QCloseEvent *event) {
// Persist the dock/toolbar arrangement and window geometry so the next launch resumes it.
QSettings settings("PSI", "jfjoch_viewer");
settings.setValue("geometry", saveGeometry());
settings.setValue("windowState", saveState(kLayoutVersion));
QMainWindow::closeEvent(event);
}
void JFJochViewerWindow::LoadFile(const QString &filename, qint64 image_number, qint64 summation, bool retry) {
emit LoadFileRequest(filename, image_number, summation, true);
}
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
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::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);
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);
}
void JFJochViewerWindow::keyPressEvent(QKeyEvent *event) {
if (event->key() == Qt::Key_F) {
emit adjustForegroundButton(true);
event->accept();
return;
}
if (event->key() == Qt::Key_A && ! event->isAutoRepeat()) {
emit setAutoForeground(true);
event->accept();
return;
}
QMainWindow::keyPressEvent(event);
}
void JFJochViewerWindow::keyReleaseEvent(QKeyEvent *event) {
if (event->key() == Qt::Key_F) {
emit adjustForegroundButton(false);
event->accept();
return;
}
QMainWindow::keyReleaseEvent(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;
}
}
}