// SPDX-FileCopyrightText: 2025 Filip Leonarski, Paul Scherrer Institute // SPDX-License-Identifier: GPL-3.0-only #include "JFJochViewerWindow.h" #include #include #include #include #include #include #include #include #include #include #include #include #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 // 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 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 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()) 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()) 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 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; } } }