Plots: - A "+ Plot" button on the dataset-info panel spawns another plot dock, placed beside the previous one (horizontal split) so several metrics can be watched side by side. Same path as the Charts menu, now one click away. Image strip: - The image number is painted into the thumbnail bitmap (coral badge, top-left) instead of a text label under the icon, so no height is lost to it. - Thumbnails are icon-only and scale to the available dock height (resizeEvent), so the strip never needs more room than it has; lowered its minimum. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
714 lines
35 KiB
C++
714 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/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);
|
|
|
|
connect(reading_worker, &JFJochImageReadingWorker::imageLoaded,
|
|
viewer, &JFJochDiffractionImage::loadImage);
|
|
|
|
connect(reading_worker, &JFJochImageReadingWorker::imageLoaded,
|
|
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(reading_worker, &JFJochImageReadingWorker::imageLoaded,
|
|
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(reading_worker, &JFJochImageReadingWorker::imageLoaded,
|
|
tableWindow, &JFJochViewerImageListWindow::imageLoaded);
|
|
|
|
connect(reading_worker, &JFJochImageReadingWorker::imageLoaded,
|
|
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(reading_worker, &JFJochImageReadingWorker::imageLoaded,
|
|
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(reading_worker, &JFJochImageReadingWorker::imageLoaded,
|
|
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(reading_worker, &JFJochImageReadingWorker::imageLoaded,
|
|
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); });
|
|
connect(reading_worker, &JFJochImageReadingWorker::imageLoaded, this,
|
|
[this](std::shared_ptr<const JFJochReaderImage> im) { lastImage = std::move(im); });
|
|
|
|
connect(reading_worker, &JFJochImageReadingWorker::imageLoaded,
|
|
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);
|
|
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(reading_worker, &JFJochImageReadingWorker::imageLoaded,
|
|
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::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(reading_worker, &JFJochImageReadingWorker::imageLoaded,
|
|
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;
|
|
}
|
|
}
|
|
} |