75e401f0e5
Build Packages / Unit tests (push) Successful in 1h31m59s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 8m43s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 10m5s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 9m27s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 8m56s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 9m24s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 10m27s
Build Packages / build:rpm (rocky8) (push) Successful in 9m20s
Build Packages / build:rpm (rocky9) (push) Successful in 10m50s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 9m54s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 8m38s
Build Packages / DIALS test (push) Successful in 12m13s
Build Packages / XDS test (durin plugin) (push) Successful in 7m8s
Build Packages / XDS test (JFJoch plugin) (push) Successful in 7m8s
Build Packages / XDS test (neggia plugin) (push) Successful in 7m50s
Build Packages / Generate python client (push) Successful in 16s
Build Packages / Build documentation (push) Successful in 50s
Build Packages / Create release (push) Skipped
This is an UNSTABLE release. It includes many experimental features, as well as many AI generated fixes. We recommend using rc.152 for production use. * jfjoch_broker: Add EXPERIMENTAL pixelrefine mode for image processing * jfjoch_broker: Allow to load user mask from 8-bit and 16-bit TIFF files * jfjoch_broker: Add ROI calculation in non-FPGA workflow * jfjoch_broker: Fixes to TCP image pusher * jfjoch_broker: Remove NUMA bindings * jfjoch_broker: Improvements to indexing * jfjoch_broker: For PSI EIGER, trimming energies are taken from the detector configuration (now compulsory) instead of hardcoded values * jfjoch_writer: Save ROI definitions and the per-pixel ROI bitmap in the master file; azimuthal ROIs support phi (angular) sectors * jfjoch_viewer: Major redesign with dockable panels and saved layouts, plus on-canvas creation/move/resize of box, circle and azimuthal ROIs * jfjoch_viewer: Run jfjoch_process reprocessing jobs from inside the GUI and overlay per-run results Reviewed-on: #63
732 lines
36 KiB
C++
732 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/JFJochViewerProcessingWindow.h"
|
|
#include "windows/JFJochProcessingJobsWindow.h"
|
|
#include "windows/JFJochSettingsWindow.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/JFJochAzIntWindow.h"
|
|
#include "windows/JFJochMagnifierWindow.h"
|
|
#include "image_viewer/JFJochImage.h"
|
|
#include "image_viewer/JFJochSimpleImage.h"
|
|
#include <QMessageBox>
|
|
|
|
namespace {
|
|
// Paired "reanalyze" glyphs: a single diffraction frame (image) vs a stack of frames (dataset),
|
|
// drawn white so they read on the navy hero buttons.
|
|
QIcon FramesIcon(int frames) {
|
|
const int S = 28;
|
|
QPixmap pm(S, S);
|
|
pm.fill(Qt::transparent);
|
|
QPainter p(&pm);
|
|
p.setRenderHint(QPainter::Antialiasing);
|
|
p.setPen(QPen(Qt::white, 2.0));
|
|
p.setBrush(Qt::NoBrush);
|
|
const double side = 13.0, step = 4.0;
|
|
for (int i = frames - 1; i >= 0; --i)
|
|
p.drawRoundedRect(QRectF(4 + i * step, 4 + i * step, side, side), 2.5, 2.5);
|
|
p.setBrush(Qt::white); // a diffraction "spot" in the front frame
|
|
p.drawEllipse(QPointF(4 + side / 2.0, 4 + side / 2.0), 2.0, 2.0);
|
|
p.end();
|
|
return QIcon(pm);
|
|
}
|
|
}
|
|
|
|
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 settingsWindow = new JFJochSettingsWindow(spot_finding_settings, indexing_settings,
|
|
experiment.GetAzimuthalIntegrationSettings(),
|
|
experiment.GetBraggIntegrationSettings(),
|
|
experiment.GetScalingSettings(), 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);
|
|
|
|
// Two hero actions — the capabilities that set Jungfraujoch apart — promoted as prominent navy
|
|
// buttons at the right of the display toolbar: re-analyse the current image, or the whole dataset.
|
|
{
|
|
const QString heroStyle =
|
|
"QPushButton { background-color:#1F3A5F; color:white; font-weight:bold;"
|
|
" padding:3px 10px; border:none; border-radius:3px; }"
|
|
" QPushButton:hover { background-color:#16314f; }"
|
|
" QPushButton:checked { background-color:#FA7268; }"
|
|
" QPushButton:disabled { background-color:#9aa6b3; }";
|
|
// Reanalyze image is a toggle: re-analyse now and keep re-analysing on every image /
|
|
// settings / processing change while armed (coral = active). Reanalyze dataset is one-shot.
|
|
auto *reanalyzeImageBtn = new QPushButton(FramesIcon(1), " Reanalyze image", this);
|
|
reanalyzeImageBtn->setStyleSheet(heroStyle);
|
|
reanalyzeImageBtn->setCheckable(true);
|
|
reanalyzeImageBtn->setToolTip("Re-analyse the current image now, and keep re-analysing on"
|
|
" every image / settings / processing change while active");
|
|
auto *reanalyzeDatasetBtn = new QPushButton(FramesIcon(3), " Reanalyze dataset", this);
|
|
reanalyzeDatasetBtn->setStyleSheet(heroStyle);
|
|
reanalyzeDatasetBtn->setToolTip("Re-process the whole dataset (opens a new processing job)");
|
|
toolBarDisplay->addWidget(reanalyzeImageBtn);
|
|
toolBarDisplay->addWidget(reanalyzeDatasetBtn);
|
|
connect(reanalyzeImageBtn, &QPushButton::toggled,
|
|
reading_worker, &JFJochImageReadingWorker::ReanalyzeImages);
|
|
connect(reanalyzeDatasetBtn, &QPushButton::clicked,
|
|
processingJobsWindow, &JFJochProcessingJobsWindow::newJob);
|
|
}
|
|
|
|
menuBar->AddWindowEntry(tableWindow, "Image list");
|
|
menuBar->AddWindowEntry(spotWindow, "Spot list");
|
|
menuBar->AddWindowEntry(reflectionWindow, "Reflection list");
|
|
menuBar->AddWindowEntry(metadataWindow, "Image metadata");
|
|
menuBar->AddWindowEntry(settingsWindow, "Processing settings");
|
|
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(reading_worker, &JFJochImageReadingWorker::datasetLoaded,
|
|
settingsWindow, &JFJochSettingsWindow::datasetLoaded);
|
|
connect(settingsWindow, &JFJochSettingsWindow::spotFindingChanged,
|
|
reading_worker, &JFJochImageReadingWorker::UpdateSpotFindingSettings);
|
|
connect(settingsWindow, &JFJochSettingsWindow::braggChanged,
|
|
reading_worker, &JFJochImageReadingWorker::UpdateBraggIntegrationSettings);
|
|
connect(settingsWindow, &JFJochSettingsWindow::scalingChanged,
|
|
reading_worker, &JFJochImageReadingWorker::UpdateScalingSettings);
|
|
|
|
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(settingsWindow, &JFJochSettingsWindow::azintChanged,
|
|
reading_worker, &JFJochImageReadingWorker::UpdateAzintSettings);
|
|
|
|
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(), this);
|
|
auto *settingsScroll = new QScrollArea(this);
|
|
settingsScroll->setWidget(settingsPanel);
|
|
settingsScroll->setWidgetResizable(true);
|
|
settingsScroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
|
settingsScroll->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
|
|
// Constrain the width like the inspector, so it neither stretches ugly when widened nor
|
|
// refuses to fold back narrow.
|
|
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::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);
|
|
|
|
// 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");
|
|
processingDock->setAllowedAreas(Qt::BottomDockWidgetArea);
|
|
processingDock->setFeatures(
|
|
QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable | QDockWidget::DockWidgetClosable);
|
|
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); });
|
|
|
|
// Remember the freshly-built layout so "Reset layout" can return to it, then restore the
|
|
// user's last-used arrangement if they have one.
|
|
defaultLayoutState = saveState();
|
|
QSettings settings("PSI", "jfjoch_viewer");
|
|
restoreGeometry(settings.value("geometry").toByteArray());
|
|
restoreState(settings.value("windowState").toByteArray());
|
|
|
|
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());
|
|
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;
|
|
}
|
|
}
|
|
} |