Files
Jungfraujoch/viewer/JFJochViewerWindow.cpp
leonarski_f 383072ac80 Consolidate viewer settings into the panel + add the merge-stats window
Make the inline settings dock the single home for processing settings and
retire the separate Processing-settings window (and the dock<->window sync):
- "Analyze image" / "Analyze dataset" move to the top of the panel; the
  MX/AzInt toggle decides the dataset-job kind, so the job dialog drops its
  mode combo.
- The panel gains the most-used spot-finding (max spots, high-resolution,
  min pixels), Bragg (Gaussian profile-fit, r1/r2/r3) and scaling (partiality,
  "3D rotation scaling" = rot3d combine + scale-fulls, merge Friedel, refine
  B, resolution limit) handles, a live indexing-algorithm description line,
  and now owns the Bragg/Scaling settings. The now-unused window tab classes
  are deleted.
- Complete the PixelRefine removal on the viewer side (the "Pixel refinement"
  option + profile-multiplier widget), fixing the transient HEAD breakage.

New JFJochMergeStatsWindow: an analysis pop-up for a finished merge (hero
numbers over a per-resolution plot / per-shell table), auto-opened on
completion and reopenable from the processing-jobs dock.

Fixes: disable tear-off dock floating (a floated dock is a dead off-screen
window under WSLg, which has no window manager); version the saved dock
layout so a stale arrangement is discarded instead of restoring a broken one;
keep the Analyze-button icons; right-align and equal-width the stats table;
line-plot / table toggle icons (ToolbarIcons gains linePlot + table).

Add ScalingSettings::HighResolutionLimit_A(optional) so the panel can clear
the resolution limit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 20:43:04 +02:00

702 lines
35 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);
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(reading_worker, &JFJochImageReadingWorker::datasetLoaded,
tableWindow, &JFJochViewerImageListWindow::datasetLoaded);
connect(reading_worker, &JFJochImageReadingWorker::datasetLoaded,
spotWindow, &JFJochViewerSpotListWindow::datasetLoaded);
connect(reading_worker, &JFJochImageReadingWorker::datasetLoaded,
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(reading_worker, &JFJochImageReadingWorker::datasetLoaded,
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(reading_worker, &JFJochImageReadingWorker::datasetLoaded,
calibrationWindow, &JFJochCalibrationWindow::datasetLoaded);
connect(reading_worker, &JFJochImageReadingWorker::simpleImageLoaded,
calibrationWindow, &JFJochCalibrationWindow::calibrationLoaded);
connect(reading_worker, &JFJochImageReadingWorker::datasetLoaded,
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 ---
connect(this, &JFJochViewerWindow::imageReady,
magnifierWindow, &JFJochHelperWindow::imageLoaded);
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);
connect(reading_worker, &JFJochImageReadingWorker::datasetLoaded, this,
[this](std::shared_ptr<const JFJochReaderDataset> ds) { lastDataset = std::move(ds); });
// lastImage is captured in OnImageReady, which sees every frame before fanning it 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 jfjoch_process 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);
connect(settingsPanel, &JFJochViewerSettingsDock::experimentChanged,
reading_worker, &JFJochImageReadingWorker::UpdateDataset);
connect(settingsPanel, &JFJochViewerSettingsDock::findBeamCenter,
reading_worker, &JFJochImageReadingWorker::FindCenter);
connect(reading_worker, &JFJochImageReadingWorker::datasetLoaded,
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(reading_worker, &JFJochImageReadingWorker::datasetLoaded,
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::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(reading_worker, &JFJochImageReadingWorker::datasetLoaded,
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;
}
}
}