viewer: Phase 1a — dockable shell with saved perspectives

Make the layout reconfigurable, the foundation for the redesign:

- The diffraction image becomes the central widget; the right-hand side panel
  is now a dockable "Inspector" (QDockWidget, right area).
- Every dock and toolbar gets a stable objectName so QMainWindow saveState /
  restoreState round-trips. Dataset-info docks are numbered uniquely.
- Persist geometry + dock state to QSettings on close and restore on launch,
  so the user's arrangement resumes.
- New "View" menu with Image / Processing perspectives (show/hide the bottom
  plots + jobs panel) and "Reset layout" (back to the as-built arrangement,
  captured at startup).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-23 16:26:15 +02:00
co-authored by Claude Opus 4.8
parent ecc06d9abd
commit f5a146d212
4 changed files with 72 additions and 11 deletions
+9
View File
@@ -65,6 +65,15 @@ JFJochViewerMenu::JFJochViewerMenu(QWidget *parent) : QMenuBar(parent) {
const QAction *dockCalibration = dockMenu->addAction("New dataset info plot");
connect(dockCalibration, &QAction::triggered, [this] { emit openDatasetInfo();});
QMenu *viewMenu = addMenu("View");
const QAction *imageLayout = viewMenu->addAction("Image layout");
connect(imageLayout, &QAction::triggered, this, &JFJochViewerMenu::imageLayoutSelected);
const QAction *processingLayout = viewMenu->addAction("Processing layout");
connect(processingLayout, &QAction::triggered, this, &JFJochViewerMenu::processingLayoutSelected);
viewMenu->addSeparator();
const QAction *resetLayout = viewMenu->addAction("Reset layout");
connect(resetLayout, &QAction::triggered, this, &JFJochViewerMenu::resetLayoutSelected);
QMenu *helpMenu = addMenu("Help");
// Add "About" action
const QAction *aboutAction = helpMenu->addAction("About");
+4
View File
@@ -39,6 +39,10 @@ signals:
void clearUserMaskSelected();
void openDatasetInfo();
void imageLayoutSelected();
void processingLayoutSelected();
void resetLayoutSelected();
private slots:
void aboutSelected();
void licensesSelected();
+50 -11
View File
@@ -3,12 +3,14 @@
#include "JFJochViewerWindow.h"
#include <QSplitter>
#include <QThread>
#include <QDockWidget>
#include <QKeyEvent>
#include <QGuiApplication>
#include <QScreen>
#include <QScrollArea>
#include <QSettings>
#include <QCloseEvent>
#include "JFJochImageReadingWorker.h"
#include "image_viewer/JFJochDiffractionImage.h"
@@ -43,10 +45,12 @@ JFJochViewerWindow::JFJochViewerWindow(QWidget *parent, bool dbus, const QString
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);
@@ -77,14 +81,11 @@ JFJochViewerWindow::JFJochViewerWindow(QWidget *parent, bool dbus, const QString
experiment.ImportIndexingSettings(indexing_settings);
experiment.DetectIceRings(true);
// Central area: only the main horizontal splitter (image + side panel)
auto h_splitter = new QSplitter(this);
h_splitter->setOrientation(Qt::Horizontal);
setCentralWidget(h_splitter);
// 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::Preferred, QSizePolicy::Expanding);
h_splitter->addWidget(viewer);
viewer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
setCentralWidget(viewer);
auto side_panel = new JFJochViewerSidePanel(this);
auto side_panel_scroll = new QScrollArea(this);
@@ -94,9 +95,13 @@ JFJochViewerWindow::JFJochViewerWindow(QWidget *parent, bool dbus, const QString
side_panel_scroll->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
side_panel_scroll->setMinimumWidth(450);
side_panel_scroll->setMaximumWidth(600);
side_panel_scroll->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
h_splitter->addWidget(side_panel_scroll);
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);
@@ -417,7 +422,8 @@ JFJochViewerWindow::JFJochViewerWindow(QWidget *parent, bool dbus, const QString
});
// Dock the processing panel in the bottom-right corner, next to the dataset-info plots.
auto processingDock = new QDockWidget("Processing", this);
processingDock = new QDockWidget("Processing", this);
processingDock->setObjectName("processingDock");
processingDock->setAllowedAreas(Qt::BottomDockWidgetArea);
processingDock->setFeatures(
QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable | QDockWidget::DockWidgetClosable);
@@ -432,6 +438,20 @@ JFJochViewerWindow::JFJochViewerWindow(QWidget *parent, bool dbus, const QString
}
menuBar->AddDockEntry(processingDock, "Processing");
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);
}
@@ -443,6 +463,24 @@ JFJochViewerWindow::~JFJochViewerWindow() {
}
}
void JFJochViewerWindow::ApplyPerspective(Perspective p) {
// Image: just the image + inspector. Processing: also the dataset-info plots and jobs panel.
const bool processing = (p == Perspective::Processing);
if (inspectorDock) inspectorDock->setVisible(true);
if (processingDock) processingDock->setVisible(processing);
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);
}
@@ -458,6 +496,7 @@ void JFJochViewerWindow::NewDatasetInfo() {
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 |
+9
View File
@@ -39,11 +39,20 @@ private:
QProgressDialog *retryDialog = nullptr;
QDockWidget *lastDatasetInfoDock = nullptr; // most recent dataset-info dock, for docking layout
QDockWidget *inspectorDock = nullptr; // image inspector (former right-hand side panel)
QDockWidget *processingDock = nullptr; // processing jobs panel
QByteArray defaultLayoutState; // captured after construction, for "Reset layout"
int datasetInfoCounter = 0; // gives each dataset-info dock a unique objectName
QThread *reading_thread;
// Named layouts. Image = just the image + inspector; Processing = also the plots + jobs panel.
enum class Perspective { Image, Processing };
void ApplyPerspective(Perspective p);
void keyPressEvent(QKeyEvent *event) override;
void keyReleaseEvent(QKeyEvent *event) override;
void closeEvent(QCloseEvent *event) override;
public slots:
void LoadFile(const QString &filename, qint64 image_number, qint64 summation, bool retry);
void LoadImage(qint64 image_number, qint64 summation);