Files
Jungfraujoch/viewer/JFJochViewerWindow.cpp
T
Filip LeonarskiandClaude Opus 5.5 22d4e94bd8 Viewer: inspector width from its content, fold it away on a narrow window
The inspector's width bounds were 75-100 average character widths, which on
macOS (8 px) came to 600 px for content that needs 397. They are now
measured: at least the panel's minimum size hint plus the scroll bar, at
most a third more. CollapsibleSection reports its folded content's width
in minimumSizeHint, so the panel keeps its width when a section opens. On a
font change the measurement waits a turn, until the children have the new
font.

When shrinking the window would leave the image narrower than the
inspector, the inspector folds away, with the magnifier if it shares the
inspector's column; they come back once the image would still be at least
as wide as the inspector (plus 40 px of hysteresis). Only a shrink folds, a
dock shown by anyone else is no longer the window's to restore, and folded
docks are saved as open. Measured on a 1920 px screen: folds at 1140 px,
returns at 1260 px; at 960 px the image gets 620 px instead of ~145.

Dark theme: disabled text is set explicitly (#858AAE, ~4.8:1 on the panels)
instead of the derived colour, which was nearly the panel colour and made
greyed-out fields look empty. The splash text is black in either theme:
its message is rich text, which takes the palette's text colour rather
than showMessage's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 18:04:42 +02:00

963 lines
49 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 <QLineEdit>
#include <QAbstractSpinBox>
#include <QComboBox>
#include <QTextEdit>
#include <QPlainTextEdit>
#include <QAbstractItemView>
#include <limits>
#include <QScreen>
#include <QApplication>
#include <QScrollArea>
#include <QScrollBar>
#include <QResizeEvent>
#include <utility>
#include <QSettings>
#include <QCloseEvent>
#include <QRandomGenerator>
#include <QLabel>
#include <QPainter>
#include <QPushButton>
#include <utility>
#include "JFJochImageReadingWorker.h"
#include "RemoteDisplayMode.h"
#include "../common/ColorScale.h"
#include "image_viewer/JFJochDiffractionImage.h"
#include "JFJochViewerSidePanel.h"
#include "widgets/JFJochViewerSettingsDock.h"
#include "widgets/JFJochViewerFileBrowser.h"
#include "JFJochViewerStatusBar.h"
#include "../common/CUDAWrapper.h"
#include "../rugnux/RugnuxDefaults.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 "toolbar/JFJochViewerToolbarDisplay.h"
#include "toolbar/JFJochViewerToolbarImage.h"
#include "windows/JFJoch2DAzintImageWindow.h"
#include "widgets/JFJochViewerMagnifier.h"
#include "windows/JFJochHelperWindow.h"
#include "image_viewer/JFJochImage.h"
#include "image_viewer/JFJochSimpleImage.h"
#include <QMessageBox>
// Dock-layout version for saveState/restoreState: bump whenever the dock set / structure changes so a
// stale saved layout (which can restore a dock as a broken, non-responsive floating window) is rejected
// and the default layout applies instead.
static constexpr int kLayoutVersion = 5;
JFJochViewerWindow::JFJochViewerWindow(QWidget *parent, bool dbus, const QString &file) : QMainWindow(parent) {
image_tail_timer_ = new QTimer(this);
image_tail_timer_->setSingleShot(true);
connect(image_tail_timer_, &QTimer::timeout, this, &JFJochViewerWindow::DeliverPendingImage);
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);
// The font size lives in the menu; the toolbar's "A" buttons are a second way to it.
connect(toolBarDisplay, &JFJochViewerToolbarDisplay::fontZoomSelected, menuBar, &JFJochViewerMenu::SetFontZoom);
connect(menuBar, &JFJochViewerMenu::fontZoomChanged, toolBarDisplay, &JFJochViewerToolbarDisplay::updateFontZoom);
toolBarDisplay->updateFontZoom(menuBar->FontZoom());
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();
// The interactive viewer is an offline tool looking at one dataset at a time, so it starts from the
// self-calibrating settings rather than numbers the user would have to tune: the per-ring adaptive
// threshold, min-pix chosen per image (std::nullopt), and detection out to the detector edge
// (high_resolution_limit unset). All three are switchable in the settings dock.
spot_finding_settings.adaptive_threshold = true;
spot_finding_settings.min_pix_per_spot = std::nullopt;
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);
// Files carry no polarization factor, so without this the interactive analysis would integrate
// without the Lp correction that a processing run applies. Start from the same policy `rugnux`
// does; the settings panel shows it and can change it per dataset.
ApplyRugnuxExperimentDefaults(experiment);
// Central area: the diffraction image. Everything else (inspector, plots, processing) is a
// dock, so the layout can be rearranged, saved, and switched between perspectives.
viewer = new JFJochDiffractionImage(this);
viewer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
setCentralWidget(viewer);
auto side_panel = new JFJochViewerSidePanel(this);
inspectorScroll = new QScrollArea(this);
inspectorScroll->setWidget(side_panel);
inspectorScroll->setWidgetResizable(true);
inspectorScroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
inspectorScroll->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
UpdateInspectorWidth();
inspectorDock = new QDockWidget("Inspector", this);
inspectorDock->setObjectName("inspectorDock");
inspectorDock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea);
inspectorDock->setWidget(inspectorScroll);
addDockWidget(Qt::RightDockWidgetArea, inspectorDock);
menuBar->AddDockEntry(inspectorDock, "Inspector");
// The magnifier sits under the inspector, whose sections are usually folded, and takes the rest
// of the column; as its own dock it can be moved elsewhere or closed.
magnifier = new JFJochViewerMagnifier(this);
magnifierDock = new QDockWidget("Magnifier", this);
magnifierDock->setObjectName("magnifierDock");
magnifierDock->setAllowedAreas(Qt::AllDockWidgetAreas);
magnifierDock->setWidget(magnifier);
addDockWidget(Qt::RightDockWidgetArea, magnifierDock);
splitDockWidget(inspectorDock, magnifierDock, Qt::Vertical);
resizeDocks({inspectorDock, magnifierDock}, {600, 400}, Qt::Vertical);
menuBar->AddDockEntry(magnifierDock, "Magnifier");
// Shown by anyone - the Window menu, a layout, a reset - it is no longer the window's to restore.
for (auto *dock: {inspectorDock, magnifierDock})
connect(dock, &QDockWidget::visibilityChanged, this,
[this, dock](bool visible) { if (visible) autoHiddenDocks.removeAll(dock); });
magnifierWindow = new JFJochHelperWindow(this);
magnifierWindow->setWindowTitle("Magnifier");
magnifierWindow->resize(500, 500);
connect(magnifier, &JFJochViewerMagnifier::popOutClicked, this, [this] {
if (magnifierWindow->isVisible())
magnifierWindow->close(); // docks it back, below
else
PopOutMagnifier();
});
connect(magnifierWindow, &JFJochHelperWindow::closing, this, [this] {
if (!magnifierWindow->centralWidget())
return; // closed while not popped out (a settings reset): the magnifier is in its dock
magnifierDock->setWidget(magnifierWindow->takeCentralWidget());
magnifierDock->show();
magnifier->setPoppedOut(false);
});
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 azintImageWindow = new JFJoch2DAzintImageWindow(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(azintImageWindow, "Azimuthal integration 2D image");
// 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::fileOpenSelected,
this, &JFJochViewerWindow::FollowInFileBrowser);
connect(menuBar, &JFJochViewerMenu::fileCloseSelected,
reading_worker, &JFJochImageReadingWorker::CloseFile);
// The worker runs in its own thread; its imageLoaded crosses the thread boundary exactly once,
// into OnImageReady, which re-emits imageReady to every GUI consumer synchronously (same thread)
// and then acks the worker (imageConsumed). Routing all consumers through one window signal makes
// the ack land after they have all run, and lets the worker cap how many frames are in flight.
connect(reading_worker, &JFJochImageReadingWorker::imageLoaded,
this, &JFJochViewerWindow::OnImageReady);
connect(this, &JFJochViewerWindow::imageConsumed,
reading_worker, &JFJochImageReadingWorker::ImageConsumed);
// Dataset stream routed the same way, so it gets the same backpressure: the worker's datasetLoaded
// crosses the thread boundary once into OnDatasetReady, which re-emits datasetReady to every GUI
// consumer synchronously and then acks the worker (datasetConsumed).
connect(reading_worker, &JFJochImageReadingWorker::datasetLoaded,
this, &JFJochViewerWindow::OnDatasetReady);
connect(this, &JFJochViewerWindow::datasetConsumed,
reading_worker, &JFJochImageReadingWorker::DatasetConsumed);
connect(this, &JFJochViewerWindow::imageReady,
viewer, &JFJochDiffractionImage::loadImage);
connect(this, &JFJochViewerWindow::imageReady,
side_panel, &JFJochViewerSidePanel::loadImage);
connect(reading_worker, &JFJochImageReadingWorker::imageStatsUpdated,
side_panel, &JFJochViewerSidePanel::loadImage);
connect(reading_worker, &JFJochImageReadingWorker::imageNumberChanged, toolBarImage,
&JFJochViewerToolbarImage::setImageNumber);
connect(toolBarImage, &JFJochViewerToolbarImage::loadImage, reading_worker, &JFJochImageReadingWorker::LoadImage);
connect(viewer, &JFJochDiffractionImage::stepImage, toolBarImage, &JFJochViewerToolbarImage::stepImage);
connect(toolBarDisplay, &JFJochViewerToolbarDisplay::setForeground, viewer,
&JFJochDiffractionImage::changeForeground);
connect(toolBarDisplay, &JFJochViewerToolbarDisplay::setBackground, viewer,
&JFJochDiffractionImage::changeBackground);
connect(viewer, &JFJochDiffractionImage::backgroundChanged,
toolBarDisplay, &JFJochViewerToolbarDisplay::updateBackground);
connect(viewer, &JFJochDiffractionImage::autoForegroundChanged,
toolBarDisplay, &JFJochViewerToolbarDisplay::updateAutoForeground);
connect(toolBarDisplay, &JFJochViewerToolbarDisplay::setAutoForeground, viewer,
&JFJochDiffractionImage::setAutoForeground);
connect(toolBarDisplay, &JFJochViewerToolbarDisplay::setHDRMode, viewer,
&JFJochDiffractionImage::setHDRMode);
connect(toolBarDisplay, &JFJochViewerToolbarDisplay::colorMapChanged, viewer,
&JFJochDiffractionImage::setColorMap);
connect(toolBarDisplay, &JFJochViewerToolbarDisplay::colorMapChanged, azintImageWindow,
&JFJoch2DAzintImageWindow::setColorMap);
connect(this, &JFJochViewerWindow::imageReady,
azintImageWindow, &JFJoch2DAzintImageWindow::imageLoaded);
connect(viewer, &JFJochDiffractionImage::foregroundChanged,
toolBarDisplay, &JFJochViewerToolbarDisplay::updateForeground);
connect(side_panel, &JFJochViewerSidePanel::roisChanged,
reading_worker, &JFJochImageReadingWorker::SetROIDefinition);
connect(side_panel, &JFJochViewerSidePanel::selectedROIChanged,
viewer, &JFJochDiffractionImage::setSelectedROI);
connect(viewer, &JFJochDiffractionImage::roiGeometryEdited,
reading_worker, &JFJochImageReadingWorker::SetROIDefinition);
connect(viewer, &JFJochDiffractionImage::roiSelected,
side_panel, &JFJochViewerSidePanel::selectROIInList);
connect(side_panel, &JFJochViewerSidePanel::downloadROIs,
reading_worker, &JFJochImageReadingWorker::DownloadROIsFromServer);
connect(side_panel, &JFJochViewerSidePanel::uploadROIs,
reading_worker, &JFJochImageReadingWorker::UploadROIsToServer);
connect(side_panel, &JFJochViewerSidePanel::maskFromROI,
reading_worker, &JFJochImageReadingWorker::MaskFromSelectedROI);
connect(menuBar, &JFJochViewerMenu::clearUserMaskSelected,
reading_worker, &JFJochImageReadingWorker::ClearUserMask);
connect(menuBar, &JFJochViewerMenu::saveUserMaskTiffSelected,
reading_worker, &JFJochImageReadingWorker::SaveUserMaskTIFF);
connect(menuBar, &JFJochViewerMenu::loadUserMaskTiffSelected,
reading_worker, &JFJochImageReadingWorker::LoadUserMaskTIFF);
connect(menuBar, &JFJochViewerMenu::uploadUserMaskSelected,
reading_worker, &JFJochImageReadingWorker::UploadUserMask);
connect(this, &JFJochViewerWindow::datasetReady,
tableWindow, &JFJochViewerImageListWindow::datasetLoaded);
connect(this, &JFJochViewerWindow::datasetReady,
spotWindow, &JFJochViewerSpotListWindow::datasetLoaded);
connect(this, &JFJochViewerWindow::datasetReady,
metadataWindow, &JFJochViewerMetadataWindow::datasetLoaded);
connect(this, &JFJochViewerWindow::imageReady,
tableWindow, &JFJochViewerImageListWindow::imageLoaded);
connect(this, &JFJochViewerWindow::imageReady,
spotWindow, &JFJochViewerSpotListWindow::imageLoaded);
connect(tableWindow, &JFJochViewerImageListWindow::imageSelected,
reading_worker, &JFJochImageReadingWorker::LoadImage);
connect(reading_worker, &JFJochImageReadingWorker::autoloadChanged,
toolBarImage, &JFJochViewerToolbarImage::setAutoloadMode);
connect(toolBarImage, &JFJochViewerToolbarImage::autoLoadButtonPressed,
reading_worker, &JFJochImageReadingWorker::setAutoLoadMode);
connect(toolBarImage, &JFJochViewerToolbarImage::imageJumpChanged,
reading_worker, &JFJochImageReadingWorker::setAutoLoadJump);
// HTTP-sync button: reflect the live connection, and open the connect dialog when clicked
// while no live source is attached (i.e. a plain file is open).
connect(reading_worker, &JFJochImageReadingWorker::httpConnectionChanged,
toolBarImage, &JFJochViewerToolbarImage::setHttpConnection);
connect(toolBarImage, &JFJochViewerToolbarImage::requestHttpConnect,
menuBar, &JFJochViewerMenu::openHttpSelected);
connect(toolBarImage, &JFJochViewerToolbarImage::requestOpenFile,
menuBar, &JFJochViewerMenu::openSelected);
connect(side_panel, &JFJochViewerSidePanel::analyze,
reading_worker, &JFJochImageReadingWorker::Analyze);
connect(side_panel, &JFJochViewerSidePanel::findBeamCenter,
reading_worker, &JFJochImageReadingWorker::FindCenter);
connect(this, &JFJochViewerWindow::datasetReady,
reflectionWindow, &JFJochViewerReflectionListWindow::datasetLoaded);
connect(this, &JFJochViewerWindow::imageReady,
reflectionWindow, &JFJochViewerReflectionListWindow::imageLoaded);
connect(reflectionWindow, &JFJochHelperWindow::zoom, viewer, &JFJochDiffractionImage::centerOnSpot);
connect(spotWindow, &JFJochHelperWindow::zoom, viewer, &JFJochDiffractionImage::centerOnSpot);
connect(side_panel, &JFJochViewerSidePanel::showSpots,
viewer, &JFJochDiffractionImage::showSpots);
connect(side_panel, &JFJochViewerSidePanel::showPredictions,
viewer, &JFJochDiffractionImage::showPredictions);
connect(side_panel, &JFJochViewerSidePanel::showROILabels,
viewer, &JFJochDiffractionImage::showROILabels);
connect(side_panel, &JFJochViewerSidePanel::showROIFill,
viewer, &JFJochDiffractionImage::showROIFill);
connect(side_panel, &JFJochViewerSidePanel::setFeatureColor,
viewer, &JFJochDiffractionImage::setFeatureColor);
connect(side_panel, &JFJochViewerSidePanel::setFeatureColor,
calibrationWindow, &JFJochCalibrationWindow::setFeatureColor);
connect(side_panel, &JFJochViewerSidePanel::setSpotColor,
viewer, &JFJochDiffractionImage::setSpotColor);
connect(side_panel, &JFJochViewerSidePanel::showHighestPixels,
viewer, &JFJochDiffractionImage::showHighestPixels);
connect(side_panel, &JFJochViewerSidePanel::showSaturatedPixels,
viewer, &JFJochDiffractionImage::showSaturation);
connect(side_panel, &JFJochViewerSidePanel::showBeamStop,
viewer, &JFJochDiffractionImage::showBeamStop);
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(side_panel, &JFJochViewerSidePanel::hideUnindexedSpots,
viewer, &JFJochDiffractionImage::hideUnindexedSpots);
connect(side_panel, &JFJochViewerSidePanel::hideIceRingSpots,
viewer, &JFJochDiffractionImage::hideIceRingSpots);
connect(side_panel, &JFJochViewerSidePanel::outlineSpots,
viewer, &JFJochDiffractionImage::outlineSpots);
connect(calibrationWindow, &JFJochCalibrationWindow::loadCalibration,
reading_worker, &JFJochImageReadingWorker::LoadCalibration);
connect(this, &JFJochViewerWindow::datasetReady,
calibrationWindow, &JFJochCalibrationWindow::datasetLoaded);
connect(reading_worker, &JFJochImageReadingWorker::simpleImageLoaded,
calibrationWindow, &JFJochCalibrationWindow::calibrationLoaded);
connect(azintImageWindow, &JFJoch2DAzintImageWindow::zoomOnBin,
viewer, &JFJochDiffractionImage::centerOnSpot);
// --- Magnifier ---
// The magnifier shows the frame the main view has already rendered - the same pixels with
// its own zoom and centre - so it neither converts nor colours anything itself.
connect(viewer, &JFJochImage::frameRendered, magnifier, [this] {
magnifier->setFrame(viewer->Frame());
});
// ... and the raw counts behind it, for the per-pixel labels. Stores a pointer, nothing more.
connect(this, &JFJochViewerWindow::imageReady,
magnifier, &JFJochViewerMagnifier::setPixelValues);
connect(viewer, &JFJochDiffractionImage::magnifierMoved,
magnifier, &JFJochViewerMagnifier::centerAt);
connect(magnifier, &JFJochViewerMagnifier::fieldChanged,
viewer, &JFJochDiffractionImage::setMagnifierField);
// Ensure worker is deleted in its own thread when the thread stops
connect(reading_thread, &QThread::finished, reading_worker, &QObject::deleteLater);
// lastDataset and lastImage are captured in OnDatasetReady / OnImageReady, which see every
// dataset / frame before fanning them out.
connect(this, &JFJochViewerWindow::imageReady,
toolBarDisplay, &JFJochViewerToolbarDisplay::imageLoaded);
connect(reading_worker, &JFJochImageReadingWorker::fileLoadError,
this, &JFJochViewerWindow::OnFileLoadError);
connect(reading_worker, &JFJochImageReadingWorker::fileLoadRetryStatus,
this, &JFJochViewerWindow::OnFileLoadRetryStatus);
connect(menuBar, &JFJochViewerMenu::openDatasetInfo, this, &JFJochViewerWindow::NewDatasetInfo);
NewDatasetInfo();
connect(this, &JFJochViewerWindow::adjustForegroundButton,
viewer, &JFJochDiffractionImage::adjustForeground);
connect(this, &JFJochViewerWindow::adjustBackgroundButton,
viewer, &JFJochDiffractionImage::adjustBackground);
connect(this, &JFJochViewerWindow::setAutoForeground,
viewer, &JFJochDiffractionImage::setAutoForeground);
connect(this, &JFJochViewerWindow::oneShotAutoForeground,
viewer, &JFJochDiffractionImage::oneShotAutoForeground);
connect(this, &JFJochViewerWindow::stepImage,
toolBarImage, &JFJochViewerToolbarImage::stepImage);
// See eventFilter: window-wide keyboard shortcuts, whatever widget has the focus.
qApp->installEventFilter(this);
// Processing jobs: run rugnux locally / copy a cluster command line, and surface
// finished runs as switchable dataset snapshots.
connect(processingJobsWindow, &JFJochProcessingJobsWindow::registerSnapshot,
reading_worker, &JFJochImageReadingWorker::RegisterProcessingSnapshot);
connect(processingJobsWindow, &JFJochProcessingJobsWindow::activateSnapshot,
reading_worker, &JFJochImageReadingWorker::SetActiveSnapshot);
connect(processingJobsWindow, &JFJochProcessingJobsWindow::writeStatusBar,
statusbar, &JFJochViewerStatusBar::display);
connect(processingJobsWindow, &JFJochProcessingJobsWindow::renameRun,
reading_worker, &JFJochImageReadingWorker::RenameRun);
connect(processingJobsWindow, &JFJochProcessingJobsWindow::removeRun,
reading_worker, &JFJochImageReadingWorker::RemoveRun);
connect(reading_worker, &JFJochImageReadingWorker::httpConnectionChanged,
processingJobsWindow, &JFJochProcessingJobsWindow::onHttpConnectionChanged);
connect(reading_worker, &JFJochImageReadingWorker::fileOpened,
processingJobsWindow, &JFJochProcessingJobsWindow::clearJobs);
connect(reading_worker, &JFJochImageReadingWorker::snapshotsChanged, processingJobsWindow,
[pw = processingJobsWindow](QStringList, QString active) { pw->setActiveRun(active); });
connect(reading_worker, &JFJochImageReadingWorker::runsChanged, this,
[this](QVector<RunData> runs, QString active) {
lastRuns = std::move(runs);
lastActiveRunId = std::move(active);
});
// Inline settings dock: the surgical MX/AzInt subset, always visible (left), wired straight
// into the running analysis so edits re-run on the current image / dataset.
auto *settingsPanel = new JFJochViewerSettingsDock(spot_finding_settings, indexing_settings,
experiment.GetAzimuthalIntegrationSettings(),
experiment.GetBraggIntegrationSettings(),
experiment.GetScalingSettings(), this);
// The panel's natural minimum (a long reference-consistency warning, the geometry rows, …) would
// otherwise pin the dock wide and stop it folding back; setMinimumWidth(0) lets the scroll area's
// own minimum (330) govern the fold. The horizontal scrollbar is AlwaysOff (like the inspector
// dock): with widgetResizable, both-axes ScrollBarAsNeeded oscillates (a vertical bar narrows the
// viewport -> a horizontal bar appears -> shortens it -> vertical fits -> ...), which pegs the CPU
// and freezes the panel when floated.
settingsPanel->setMinimumWidth(0);
auto *settingsScroll = new QScrollArea(this);
settingsScroll->setWidget(settingsPanel);
settingsScroll->setWidgetResizable(true);
settingsScroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
settingsScroll->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
settingsScroll->setMinimumWidth(330);
settingsScroll->setMaximumWidth(480);
settingsDock = new QDockWidget("Processing", 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, "Processing");
// Files panel: an alternative to the processing panel in the same left-hand slot, tabbed together (the
// canonical Qt way to offer two panels one on top of the other without doubling the width) - most
// beamline users care about opening images, not reprocessing settings, so this one starts on top.
auto *fileBrowser = new JFJochViewerFileBrowser(this);
fileBrowserDock = new QDockWidget("Files", this);
fileBrowserDock->setObjectName("fileBrowserDock");
fileBrowserDock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea);
fileBrowserDock->setWidget(fileBrowser);
addDockWidget(Qt::LeftDockWidgetArea, fileBrowserDock);
tabifyDockWidget(settingsDock, fileBrowserDock);
fileBrowserDock->raise();
menuBar->AddDockEntry(fileBrowserDock, "Files");
connect(fileBrowser, &JFJochViewerFileBrowser::openDataset,
reading_worker, &JFJochImageReadingWorker::LoadFile);
connect(settingsPanel, &JFJochViewerSettingsDock::spotFindingChanged,
reading_worker, &JFJochImageReadingWorker::UpdateSpotFindingSettings);
connect(settingsPanel, &JFJochViewerSettingsDock::azintChanged,
reading_worker, &JFJochImageReadingWorker::UpdateAzintSettings);
connect(settingsPanel, &JFJochViewerSettingsDock::braggChanged,
reading_worker, &JFJochImageReadingWorker::UpdateBraggIntegrationSettings);
connect(settingsPanel, &JFJochViewerSettingsDock::scalingChanged,
reading_worker, &JFJochImageReadingWorker::UpdateScalingSettings);
// The two analysis actions now live on top of the panel.
connect(settingsPanel, &JFJochViewerSettingsDock::reanalyzeImage,
reading_worker, &JFJochImageReadingWorker::ReanalyzeImages);
connect(settingsPanel, &JFJochViewerSettingsDock::analyzeDataset,
processingJobsWindow, &JFJochProcessingJobsWindow::newJob);
// Grey out "Analyze dataset" on a live HTTP source (re-processing needs a stored file).
connect(reading_worker, &JFJochImageReadingWorker::httpConnectionChanged,
settingsPanel, &JFJochViewerSettingsDock::setHttpConnection);
connect(settingsPanel, &JFJochViewerSettingsDock::experimentChanged,
reading_worker, &JFJochImageReadingWorker::UpdateDataset);
connect(settingsPanel, &JFJochViewerSettingsDock::findBeamCenter,
reading_worker, &JFJochImageReadingWorker::FindCenter);
connect(this, &JFJochViewerWindow::datasetReady,
settingsPanel, &JFJochViewerSettingsDock::datasetLoaded);
connect(this, &JFJochViewerWindow::imageReady,
settingsPanel, &JFJochViewerSettingsDock::loadImage);
connect(settingsPanel, &JFJochViewerSettingsDock::ringsFromCalibration,
side_panel, &JFJochViewerSidePanel::SetRings);
connect(settingsPanel, &JFJochViewerSettingsDock::referenceSelected,
reading_worker, &JFJochImageReadingWorker::SetReferenceMtz);
connect(reading_worker, &JFJochImageReadingWorker::referenceMtzChanged,
settingsPanel, &JFJochViewerSettingsDock::referenceLoaded);
// Jobs panel: hidden by default and narrower; it reveals itself (to the right of the
// plots) only when a reprocessing job starts.
processingDock = new QDockWidget("Jobs", 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, "Jobs");
connect(processingJobsWindow, &JFJochProcessingJobsWindow::jobStarted, this, [this] {
processingDock->show();
processingDock->raise();
});
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); });
connect(menuBar, &JFJochViewerMenu::resetSettingsSelected, this, &JFJochViewerWindow::ResetAllSettings);
// Tear-off floating is disabled for all docks: a floated QDockWidget is unreliable across platforms
// (on WSLg it becomes an off-screen, override-redirect window that can't be focused or recovered, so
// the undocked panel goes dead). Docks stay movable between dock areas and closable, just not floated.
for (QDockWidget *dock : findChildren<QDockWidget *>())
dock->setFeatures(dock->features() & ~QDockWidget::DockWidgetFloatable);
// Remember the freshly-built layout so "Reset layout" can return to it, then restore the
// user's last-used arrangement if they have one (and if it matches the current layout version).
defaultLayoutState = saveState(kLayoutVersion);
QSettings settings("PSI", "jfjoch_viewer");
restoreGeometry(settings.value("geometry").toByteArray());
restoreState(settings.value("windowState").toByteArray(), kLayoutVersion);
magnifierWindow->restoreGeometry(settings.value("magnifierGeometry").toByteArray());
// Once the main window is up: opened from here, the window would end up behind it
if (settings.value("magnifierPoppedOut", false).toBool())
QTimer::singleShot(0, this, &JFJochViewerWindow::PopOutMagnifier);
// Display settings (color map, Auto, HDR) persist across sessions the same way the layout does.
const int colorMap = settings.value("colorMap", static_cast<int>(ColorScaleEnum::Indigo)).toInt();
viewer->setColorMap(colorMap);
toolBarDisplay->updateColorMap(colorMap);
const bool autoForeground = settings.value("autoForeground", false).toBool();
viewer->setAutoForeground(autoForeground); // emits autoForegroundChanged -> toolbar button state
const bool hdrMode = settings.value("hdrMode", false).toBool();
viewer->setHDRMode(hdrMode);
toolBarDisplay->updateHDRMode(hdrMode);
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 (+ file manager, on top of its tab with Processing).
// Processing: also dataset-info plots and the jobs panel, with the processing panel raised instead.
const bool processing = (p == Perspective::Processing);
if (inspectorDock) inspectorDock->setVisible(true);
// Processing and the file manager stay tabbed together either way; only which one is on top changes.
if (settingsDock) settingsDock->setVisible(true);
if (fileBrowserDock) fileBrowserDock->setVisible(true);
(processing ? settingsDock : fileBrowserDock)->raise();
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::ResetAllSettings() {
const auto reply = QMessageBox::question(
this, "Reset all settings",
"Reset all viewer settings to defaults - window layout, display settings, plot choices "
"and the file manager root?",
QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
if (reply != QMessageBox::Yes)
return;
QSettings("PSI", "jfjoch_viewer").clear();
// Bring the running session to the state a first start builds, so normal saving on close is
// correct again afterwards. Same calls as the constructor, in the same order.
magnifierWindow->close(); // docks the magnifier back, if it was popped out
magnifierWindow->resize(500, 500);
if (!defaultLayoutState.isEmpty())
restoreState(defaultLayoutState, kLayoutVersion);
showNormal();
const QRect avail = QGuiApplication::primaryScreen()->availableGeometry();
resize(qMin(1200, avail.width() - 100), qMin(1100, avail.height() - 100));
viewer->setColorMap(static_cast<int>(ColorScaleEnum::Indigo));
toolBarDisplay->updateColorMap(static_cast<int>(ColorScaleEnum::Indigo));
viewer->setAutoForeground(false); // emits autoForegroundChanged -> toolbar button state
viewer->setHDRMode(false);
toolBarDisplay->updateHDRMode(false);
for (auto *info : findChildren<JFJochViewerDatasetInfo *>())
info->resetPlotDefaults();
for (auto *browser : findChildren<JFJochViewerFileBrowser *>())
browser->ResetToDefaultRoot();
menuBar->ResetFontZoom();
}
// The inspector's width bounds are measured from its content: at least what its widest section needs
// (folded ones included, see CollapsibleSection) plus the scroll bar, so nothing is cut off, and a third
// more at most - wider only adds empty space next to the text, and takes it from the image. Measured
// rather than counted in characters, as they once were, because a character count that suits one
// platform's font is far too wide on another's (600 px for 400 px of content on macOS).
void JFJochViewerWindow::UpdateInspectorWidth() {
const int need = inspectorScroll->widget()->minimumSizeHint().width()
+ inspectorScroll->verticalScrollBar()->sizeHint().width();
inspectorScroll->setMinimumWidth(need);
inspectorScroll->setMaximumWidth(need * 4 / 3);
}
void JFJochViewerWindow::changeEvent(QEvent *event) {
QMainWindow::changeEvent(event);
// The menu bar sets the application font while the window is still being built, so the inspector
// is not there yet on the first one. The window hears of a font change before its children do,
// so the content is measured once they have all taken the new font.
if (event->type() == QEvent::FontChange && inspectorScroll)
QTimer::singleShot(0, this, &JFJochViewerWindow::UpdateInspectorWidth);
}
// The image comes first. When shrinking the window would leave it narrower than the inspector next
// to it, the inspector folds away, with the magnifier if it shares the inspector's column. They come
// back once the image would still be at least as wide as the inspector with it restored (plus a
// margin, so a window resized around the threshold does not flicker). Only a shrink folds: a panel
// the user reopens on a narrow window stays open, and one the user closed is never brought back.
void JFJochViewerWindow::resizeEvent(QResizeEvent *event) {
QMainWindow::resizeEvent(event);
if (!inspectorDock || !magnifierDock)
return;
const int image_width = centralWidget()->width();
const bool shrinking = event->oldSize().width() < 0 || event->size().width() < event->oldSize().width();
if (shrinking && inspectorDock->isVisible() && image_width < inspectorDock->width()) {
autoHiddenWidth = inspectorDock->width();
QList<QDockWidget *> docks = {inspectorDock};
if (magnifierDock->isVisible() && dockWidgetArea(magnifierDock) == dockWidgetArea(inspectorDock))
docks.append(magnifierDock);
for (auto *dock: docks) {
dock->hide();
if (!autoHiddenDocks.contains(dock))
autoHiddenDocks.append(dock);
}
} else if (!autoHiddenDocks.isEmpty() && image_width - autoHiddenWidth >= autoHiddenWidth + 40) {
for (auto *dock: std::exchange(autoHiddenDocks, {}))
dock->show();
}
}
void JFJochViewerWindow::closeEvent(QCloseEvent *event) {
// Persist the dock/toolbar arrangement and window geometry so the next launch resumes it. Docks
// folded away only for a narrow window are saved as open: the next start may have the room.
for (auto *dock: std::exchange(autoHiddenDocks, {}))
dock->show();
QSettings settings("PSI", "jfjoch_viewer");
settings.setValue("geometry", saveGeometry());
settings.setValue("windowState", saveState(kLayoutVersion));
settings.setValue("magnifierGeometry", magnifierWindow->saveGeometry());
settings.setValue("magnifierPoppedOut", magnifierWindow->isVisible());
settings.setValue("colorMap", toolBarDisplay->colorMap());
settings.setValue("autoForeground", toolBarDisplay->autoForeground());
settings.setValue("hdrMode", toolBarDisplay->hdrMode());
QMainWindow::closeEvent(event);
}
void JFJochViewerWindow::PopOutMagnifier() {
magnifierWindow->setCentralWidget(magnifier);
magnifierDock->hide();
magnifier->setPoppedOut(true);
magnifierWindow->open();
}
void JFJochViewerWindow::LoadFile(const QString &filename, qint64 image_number, qint64 summation, bool retry) {
emit LoadFileRequest(filename, image_number, summation, true);
FollowInFileBrowser(filename);
}
void JFJochViewerWindow::FollowInFileBrowser(const QString &filename) {
for (auto *browser : findChildren<JFJochViewerFileBrowser *>())
browser->FollowPath(filename);
}
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
// On a remote display every frame shown is a full-window upload, so a fast producer (live
// follow, movie playback, scrubbing) is displayed at the hover cadence: only the newest frame
// is kept and the intermediates go undisplayed. The ack is still per frame - the in-flight
// counter is all it feeds - so the producer never waits on the display.
if (RemoteDisplayMode()) {
pendingImage = std::move(image);
if (!image_rate_.isValid() || image_rate_.elapsed() >= kRemoteRepaintIntervalMs) {
image_tail_timer_->stop();
DeliverPendingImage();
} else {
image_tail_timer_->start(kRemoteRepaintIntervalMs);
}
emit imageConsumed();
return;
}
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::DeliverPendingImage() {
image_rate_.restart();
if (!pendingImage)
return;
emit imageReady(std::exchange(pendingImage, nullptr));
}
void JFJochViewerWindow::OnDatasetReady(std::shared_ptr<const JFJochReaderDataset> dataset) {
lastDataset = dataset; // newest dataset, for dataset-info docks opened later
emit datasetReady(dataset); // fan out to every GUI consumer synchronously (same thread)
emit datasetConsumed(); // ack the worker so it may produce the next dataset refresh
}
void JFJochViewerWindow::NewDatasetInfo() {
auto info = new JFJochViewerDatasetInfo(this);
info->datasetLoaded(lastDataset);
info->imageLoaded(lastImage);
info->runsChanged(lastRuns, lastActiveRunId);
auto dock = new QDockWidget(QString("Dataset info"), this);
dock->setObjectName(QStringLiteral("datasetInfoDock%1").arg(datasetInfoCounter++));
dock->setAllowedAreas(Qt::BottomDockWidgetArea);
dock->setFeatures(
QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable | QDockWidget::DockWidgetClosable |
QDockWidget::DockWidgetVerticalTitleBar);
dock->setAttribute(Qt::WA_DeleteOnClose);
dock->setWidget(info);
addDockWidget(Qt::BottomDockWidgetArea, dock);
// Place additional plots beside the previous one so "+ Plot" gives a visible row of plots.
if (lastDatasetInfoDock)
splitDockWidget(lastDatasetInfoDock, dock, Qt::Horizontal);
lastDatasetInfoDock = dock;
// Wire signals like the initial dataset_info
connect(this, &JFJochViewerWindow::datasetReady,
info, &JFJochViewerDatasetInfo::datasetLoaded);
connect(this, &JFJochViewerWindow::imageReady,
info, &JFJochViewerDatasetInfo::imageLoaded);
// All runs (original + reprocessing snapshots) overlay as separate lines.
connect(reading_worker, &JFJochImageReadingWorker::runsChanged,
info, &JFJochViewerDatasetInfo::runsChanged);
// Live processing results: the in-progress run as its own overlay line.
connect(processingJobsWindow, &JFJochProcessingJobsWindow::liveDataset,
info, &JFJochViewerDatasetInfo::liveRunUpdated);
// A live (HTTP) source defaults the plot to spots + background.
connect(reading_worker, &JFJochImageReadingWorker::httpConnectionChanged,
info, &JFJochViewerDatasetInfo::setHttpConnection);
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);
}
bool JFJochViewerWindow::eventFilter(QObject *obj, QEvent *event) {
// The filter sees every key of this window before the focused widget does, so F/B/A and the
// frame-navigation keys work with any panel focused, not only the image. It steps aside when
// the focused widget genuinely uses the key: any text-entry widget for all of them, item
// views (the file tree) for the navigation keys, and everything while a popup is open.
const auto type = event->type();
if ((type == QEvent::KeyPress || type == QEvent::KeyRelease)
&& QApplication::activeWindow() == this && !QApplication::activePopupWidget()) {
const auto *ke = static_cast<QKeyEvent *>(event);
const int key = ke->key();
// Shift is released wherever the focus is; the magnifier frame must not outlive it
if (type == QEvent::KeyRelease && key == Qt::Key_Shift)
viewer->hideMagnifierFrame();
QWidget *focus = QApplication::focusWidget();
const bool editing = qobject_cast<QLineEdit *>(focus) || qobject_cast<QAbstractSpinBox *>(focus)
|| qobject_cast<QComboBox *>(focus) || qobject_cast<QTextEdit *>(focus)
|| qobject_cast<QPlainTextEdit *>(focus);
const bool plain = (ke->modifiers() & ~Qt::KeypadModifier) == Qt::NoModifier;
if (!editing && plain) {
if (type == QEvent::KeyPress) {
switch (key) {
case Qt::Key_F: emit adjustForegroundButton(true); return true;
case Qt::Key_B: emit adjustBackgroundButton(true); return true;
case Qt::Key_A:
if (!ke->isAutoRepeat())
emit oneShotAutoForeground();
return true;
default: break;
}
if (!qobject_cast<QAbstractItemView *>(focus)) {
switch (key) {
case Qt::Key_Home: emit stepImage(std::numeric_limits<int>::min()); return true;
case Qt::Key_End: emit stepImage(std::numeric_limits<int>::max()); return true;
case Qt::Key_PageUp: emit stepImage(1); return true;
case Qt::Key_PageDown: emit stepImage(-1); return true;
default: break;
}
}
} else {
if (key == Qt::Key_F) { emit adjustForegroundButton(false); return true; }
if (key == Qt::Key_B) { emit adjustBackgroundButton(false); return true; }
}
}
}
return QMainWindow::eventFilter(obj, 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;
}
}
}