viewer: the Help menu says what the mouse does and who is credited, and a spot can be hidden by what it is

The mouse bindings were only in the code and in one bullet of the viewer page: the wheel and its
four modifiers, the two ROI drags, and the hover and double-click behaviour of the grid scan, the
plots and the auxiliary views. They are now a window under Help, and the same table is a section of
docs/JFJOCH_VIEWER.md.

docs/ACKNOWLEDGEMENT.md is shipped in the viewer's resources and shown under Help beside the
third-party licenses, so the credit travels with the binary rather than only with the docs, and it
ends with a declaration of how generative AI was used in developing this code.

The image overlay draws every spot it has, which makes a frame with ice or with a large unindexed
population hard to read. The Inspector can now leave out the spots that were not assigned to a
lattice, and the spots that fall on an ice ring, independently of each other and of the ice-ring
highlight.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UdFBP5TjGsJ5Kvy2H8P9L9
This commit is contained in:
2026-09-08 21:17:07 +02:00
co-authored by Claude Opus 5
parent aa13143209
commit dccc415a53
16 changed files with 279 additions and 12 deletions
+7
View File
@@ -335,3 +335,10 @@ as in both that procedure and REFMAC5: G. N. Murshudov, P. Skubak, A. A. Lebedev
R. A. Steiner, R. A. Nicholls, M. D. Winn, F. Long and A. A. Vagin, "REFMAC5 for the refinement of
macromolecular crystal structures" (2011), Acta Cryst. D67, 355-367
[doi:10.1107/S0907444911001314](https://doi.org/10.1107/S0907444911001314).
## Generative AI usage declaration
Large language models were used extensively in developing this code. Jungfraujoch development was
supported with JetBrains AI (mostly GPT models) to refactor and verify particular code fragments.
rugnux was developed with the assistance of Claude Code (mostly the Opus model). This documentation
was written with the assistance of Claude Opus and Fable models.
+1
View File
@@ -24,6 +24,7 @@
* `rugnux --mode scale` reports the detector tilt and direct beam of the geometry it re-scaled at, instead of zeros that read as a flat detector, and no longer warns that no image was indexed on a run whose lattice came from its input file.
* Every rotation run that determined a space group and merged reports what the mounting cost: `SPINDLE_LOST_UNIQUE_FRACTION=` is the fraction (0-1) of unique reflections the mounting made unmeasurable under the measured point group, also written to the master as `/entry/MX/spindleLostUniqueFraction` and what the mounting warning fires on; `SPINDLE_SYMMETRY_AXIS_ANGLE_DEG=` / `SPINDLE_SYMMETRY_AXIS_ORDER=` describe the mounting in the `--developer` report.
* Stills and grid scans carry a per-image `spindle_blind_fraction` - how much of a rotation sweep's blind cone this orientation would make unrecoverable, 0.5 and above calling for a second orientation - through the CBOR stream, HDF5 (`/entry/MX/spindleBlindFraction`), the plot and scan-result APIs, and the viewer and frontend plots; an absent value means the frame could not be assessed and is not a 0.
* `jfjoch_viewer` gains Help entries for the mouse shortcuts and the acknowledgements, and Inspector toggles that hide non-indexed spots and spots on ice rings.
* The results report's `REPORT_VERSION` is 7.
### 1.0.0-rc.166
+55 -4
View File
@@ -59,12 +59,16 @@ install it.
viewer, 2D azimuthal-integration image, calibration-image viewer and a magnifier; plus the
*Inspector* (per-image statistics, image features, resolution rings, ROI statistics), the
*Image strip* thumbnail feed and dataset-info charts.
- The *Inspector*'s **Image features** section decides what the overlay draws — spots, predictions,
saturated and highest pixels, the beam stop — including whether the non-indexed spots and the
spots that fall on an ice ring are drawn at all.
- User-mask editing: build a user mask interactively, load one from TIFF (replacing or adding to the
current one), save it as TIFF, clear it, or upload it to a connected server.
- Mouse wheel on the diffraction image: **zoom** on its own, move the foreground with `Ctrl` or
`Shift`, and **step through the dataset one image per notch** with `Alt` (wheel up goes forward).
Some window managers grab `Alt`-modified mouse events before the application sees them; that is a
window-manager setting, not something the viewer can take back.
- Mouse-driven navigation of the image, the grid scan and the plots — see
[Mouse shortcuts](#mouse-shortcuts) below, which the viewer also shows under *Help ▸ Mouse
Shortcuts*.
- *Help* shows the mouse shortcuts, the [acknowledgements](ACKNOWLEDGEMENT.md) and the third-party
licenses.
- Layout presets (*View ▸ Image layout / Processing layout / Reset layout*) rearrange the docks for
looking at images or at processing results.
@@ -83,6 +87,53 @@ installer and the `.tgz` are CUDA 13 and CUDA 12 respectively, which also decide
they run on — a V100 needs the CUDA 12 `.tgz`. See
[Release contents ▸ GPU generations and the NVIDIA driver](RELEASE_CONTENTS.md#gpu-generations-and-the-nvidia-driver).
## Mouse shortcuts
The same list is available in the application under **Help ▸ Mouse Shortcuts**.
### Diffraction image
| Action | Effect |
| --- | --- |
| Wheel | Zoom in / out, centred on the cursor |
| `Shift` + wheel | Move the foreground (upper contrast limit) in linear steps |
| `Ctrl` + wheel | Move the foreground in multiplicative steps (×1.15 per notch) |
| `Alt` + wheel | Step through the dataset, one image per notch (up = next image) |
| `F` held + wheel | Same as `Shift` + wheel, for as long as `F` is held |
| `A` | Switch on the automatic foreground |
| Hover | Status bar shows the pixel position, its value and the resolution |
| Drag | Pan the image |
| `Shift` + drag | Draw a rectangular ROI |
| `Shift` + `Ctrl` + drag | Draw a circular ROI |
| Drag an ROI or its handle | Move or resize the selected ROI |
| Right click | Copy / save the image, fit to view, clear the ROI |
Some window managers take `Alt`-modified mouse events for themselves before the application sees
them; that is a window-manager setting, not something the viewer can take back.
### Grid scan
| Action | Effect |
| --- | --- |
| Hover | Status bar shows the image number, the grid position and its value |
| `Shift` + hover | Load the image under the cursor while moving over the grid |
| Double click | Load the image under the cursor |
### Other views
| Action | Effect |
| --- | --- |
| 2D azimuthal image: double click | Zoom the diffraction image on the corresponding detector position |
| Dataset-info plot: hover | Status bar shows the image number and the plotted value |
| Dataset-info plot: `Shift` + hover | Load the hovered image |
| Spot / reflection list: double click | Zoom the diffraction image on that spot or prediction |
| Image list: double click | Load that image |
| Reciprocal space: drag | Rotate the view |
| Reciprocal space: right drag | Pan the view |
| Reciprocal space: wheel | Zoom the view |
| Reciprocal space: double click | Zoom the diffraction image on the nearest spot |
| Magnifier: wheel | Zoom the magnifier; it follows the cursor on the main image |
## Opening data
- **File ▸ Open** (`Ctrl+O`) — open a local HDF5 file, or any frame of a miniCBF sweep.
+4
View File
@@ -89,6 +89,10 @@ ADD_EXECUTABLE(jfjoch_viewer jfjoch_viewer.cpp JFJochViewerWindow.cpp JFJochView
windows/JFJochHelperWindow.h
windows/JFJochLicenseWindow.cpp
windows/JFJochLicenseWindow.h
windows/JFJochMouseShortcutsWindow.cpp
windows/JFJochMouseShortcutsWindow.h
windows/JFJochAcknowledgementWindow.cpp
windows/JFJochAcknowledgementWindow.h
image_viewer/JFJochImage.cpp
image_viewer/JFJochImage.h
image_viewer/JFJochFollowerImage.cpp
+26
View File
@@ -18,6 +18,8 @@
#include "JFJochViewerWindow.h"
#include "windows/JFJochLicenseWindow.h"
#include "windows/JFJochMouseShortcutsWindow.h"
#include "windows/JFJochAcknowledgementWindow.h"
#include "../common/GitInfo.h"
#include "../common/CUDAWrapper.h"
@@ -81,6 +83,12 @@ JFJochViewerMenu::JFJochViewerMenu(QWidget *parent) : QMenuBar(parent) {
const QAction *aboutAction = helpMenu->addAction("About");
connect(aboutAction, &QAction::triggered, this, &JFJochViewerMenu::aboutSelected);
const QAction *mouseShortcutsAction = helpMenu->addAction("Mouse Shortcuts");
connect(mouseShortcutsAction, &QAction::triggered, this, &JFJochViewerMenu::mouseShortcutsSelected);
const QAction *acknowledgementAction = helpMenu->addAction("Acknowledgements");
connect(acknowledgementAction, &QAction::triggered, this, &JFJochViewerMenu::acknowledgementsSelected);
const QAction *licensesAction = helpMenu->addAction("Third-party Licenses");
connect(licensesAction, &QAction::triggered, this, &JFJochViewerMenu::licensesSelected);
}
@@ -148,6 +156,24 @@ void JFJochViewerMenu::licensesSelected() {
licenseWindow->activateWindow();
}
void JFJochViewerMenu::mouseShortcutsSelected() {
// Same pattern as the license window: modeless, created once, raised if already open.
if (mouseShortcutsWindow == nullptr)
mouseShortcutsWindow = new JFJochMouseShortcutsWindow(window());
mouseShortcutsWindow->show();
mouseShortcutsWindow->raise();
mouseShortcutsWindow->activateWindow();
}
void JFJochViewerMenu::acknowledgementsSelected() {
// Same pattern as the license window: modeless, created once, raised if already open.
if (acknowledgementWindow == nullptr)
acknowledgementWindow = new JFJochAcknowledgementWindow(window());
acknowledgementWindow->show();
acknowledgementWindow->raise();
acknowledgementWindow->activateWindow();
}
void JFJochViewerMenu::openSelected() {
QString fileName = QFileDialog::getOpenFileName(
this,
+6
View File
@@ -8,6 +8,8 @@
#include "windows/JFJochHelperWindow.h"
class JFJochLicenseWindow;
class JFJochMouseShortcutsWindow;
class JFJochAcknowledgementWindow;
class QDockWidget;
class JFJochViewerMenu : public QMenuBar {
@@ -21,6 +23,8 @@ class JFJochViewerMenu : public QMenuBar {
QMenu *windowMenu = nullptr;
JFJochLicenseWindow *licenseWindow = nullptr;
JFJochMouseShortcutsWindow *mouseShortcutsWindow = nullptr;
JFJochAcknowledgementWindow *acknowledgementWindow = nullptr;
public:
explicit JFJochViewerMenu(QWidget *parent = nullptr);
~JFJochViewerMenu() override = default;
@@ -50,6 +54,8 @@ signals:
private slots:
void aboutSelected();
void licensesSelected();
void mouseShortcutsSelected();
void acknowledgementsSelected();
void quitSelected();
void closeSelected();
+26 -8
View File
@@ -43,6 +43,14 @@ JFJochViewerSidePanel::JFJochViewerSidePanel(QWidget *parent) : QWidget(parent)
highlightIceRingToggleCheckBox->setCheckState(Qt::CheckState::Checked);
connect(highlightIceRingToggleCheckBox, &QCheckBox::toggled, this,&JFJochViewerSidePanel::highlightIceRingsToggled);
auto hideUnindexedCheckBox = new QCheckBox("Hide non-indexed spots", this);
hideUnindexedCheckBox->setToolTip("Draw only the spots that were assigned to a lattice.");
connect(hideUnindexedCheckBox, &QCheckBox::toggled, this, &JFJochViewerSidePanel::hideUnindexedSpotsToggled);
auto hideIceRingSpotsCheckBox = new QCheckBox("Hide spots on ice rings", this);
hideIceRingSpotsCheckBox->setToolTip("Leave the spots that fall on an ice ring out of the overlay.");
connect(hideIceRingSpotsCheckBox, &QCheckBox::toggled, this, &JFJochViewerSidePanel::hideIceRingSpotsToggled);
auto predictionsToggleCheckBox = new QCheckBox("Show predictions", this);
predictionsToggleCheckBox->setCheckState(Qt::CheckState::Unchecked);
@@ -100,13 +108,15 @@ JFJochViewerSidePanel::JFJochViewerSidePanel(QWidget *parent) : QWidget(parent)
auto image_feature_grid = new QGridLayout();
image_feature_grid->addWidget(spotToggleCheckBox, 0, 0);
image_feature_grid->addWidget(highlightIceRingToggleCheckBox, 0, 1);
image_feature_grid->addWidget(predictionsToggleCheckBox, 1, 0);
image_feature_grid->addWidget(beamStopCheckBox, 1, 1);
image_feature_grid->addWidget(hideUnindexedCheckBox, 1, 0);
image_feature_grid->addWidget(hideIceRingSpotsCheckBox, 1, 1);
image_feature_grid->addWidget(predictionsToggleCheckBox, 2, 0);
image_feature_grid->addWidget(beamStopCheckBox, 2, 1);
image_feature_grid->addWidget(saturatedPixelsCheckBox, 2, 0);
image_feature_grid->addWidget(highestPixelsComboBox, 2, 1);
image_feature_grid->addWidget(colorSelectButton, 3, 0);
image_feature_grid->addWidget(spotColorSelectButton, 3, 1);
image_feature_grid->addWidget(saturatedPixelsCheckBox, 3, 0);
image_feature_grid->addWidget(highestPixelsComboBox, 3, 1);
image_feature_grid->addWidget(colorSelectButton, 4, 0);
image_feature_grid->addWidget(spotColorSelectButton, 4, 1);
// Legend: what each overlay colour means (matches JFJochDiffractionImage draw colours).
legend_spot_swatch_ = MakeColorSwatch(this, Qt::green);
@@ -130,8 +140,8 @@ JFJochViewerSidePanel::JFJochViewerSidePanel(QWidget *parent) : QWidget(parent)
legend_row++;
}
legendLayout->setColumnStretch(1, 1);
image_feature_grid->addWidget(new QLabel(tr("Legend"), this), 4, 0, 1, 2);
image_feature_grid->addLayout(legendLayout, 5, 0, 1, 2);
image_feature_grid->addWidget(new QLabel(tr("Legend"), this), 5, 0, 1, 2);
image_feature_grid->addLayout(legendLayout, 6, 0, 1, 2);
featuresSection->setContentLayout(image_feature_grid);
layout->addWidget(featuresSection);
@@ -210,6 +220,14 @@ void JFJochViewerSidePanel::highlightIceRingsToggled(bool input) {
emit highlightIceRings(input);
}
void JFJochViewerSidePanel::hideUnindexedSpotsToggled(bool input) {
emit hideUnindexedSpots(input);
}
void JFJochViewerSidePanel::hideIceRingSpotsToggled(bool input) {
emit hideIceRingSpots(input);
}
void JFJochViewerSidePanel::selectROIInList(QString name) {
roi_list->setSelected(name);
}
+4
View File
@@ -35,6 +35,8 @@ signals:
void showBeamStop(bool input);
void showPredictions(bool input);
void highlightIceRings(bool input);
void hideUnindexedSpots(bool input);
void hideIceRingSpots(bool input);
void showROILabels(bool input);
void showROIFill(bool input);
void roisChanged(ROIDefinition rois);
@@ -62,6 +64,8 @@ private slots:
void spotsToggled(bool input);
void predictionsToggled(bool input);
void highlightIceRingsToggled(bool input);
void hideUnindexedSpotsToggled(bool input);
void hideIceRingSpotsToggled(bool input);
void saturatedPixelsToggled(bool input);
};
+4
View File
@@ -378,6 +378,10 @@ JFJochViewerWindow::JFJochViewerWindow(QWidget *parent, bool dbus, const QString
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(calibrationWindow, &JFJochCalibrationWindow::loadCalibration,
reading_worker, &JFJochImageReadingWorker::LoadCalibration);
@@ -162,6 +162,11 @@ void JFJochDiffractionImage::DrawSpots() {
if (!visibleRect.contains(QPointF{s.x, s.y}))
continue;
if (hide_unindexed_spots && !s.indexed)
continue;
if (hide_ice_ring_spots && s.ice_ring)
continue;
const qreal desired_half_px = 8.0;
const qreal spot_size = desired_half_px / std::sqrt(std::max(0.0001, scale_factor));
@@ -990,6 +995,16 @@ void JFJochDiffractionImage::highlightIceRings(bool input) {
updateOverlay();
}
void JFJochDiffractionImage::hideUnindexedSpots(bool input) {
hide_unindexed_spots = input;
updateOverlay();
}
void JFJochDiffractionImage::hideIceRingSpots(bool input) {
hide_ice_ring_spots = input;
updateOverlay();
}
void JFJochDiffractionImage::setResolutionRingMode(RingMode mode) {
ring_mode = mode;
updateOverlay();
@@ -113,6 +113,9 @@ private:
bool highlight_ice_rings = true;
bool hide_unindexed_spots = false;
bool hide_ice_ring_spots = false;
void mouseHover(const QPointF &scenePos, Qt::KeyboardModifiers modifiers) override;
signals:
@@ -139,6 +142,8 @@ public slots:
void showBeamStop(bool input);
void highlightIceRings(bool input);
void hideUnindexedSpots(bool input);
void hideIceRingSpots(bool input);
void setHDRMode(bool input);
};
+1
View File
@@ -7,6 +7,7 @@
<qresource prefix="/">
<file>jfjoch.png</file>
<file>third_party_licenses.html</file>
<file alias="ACKNOWLEDGEMENT.md">../../docs/ACKNOWLEDGEMENT.md</file>
<file>psi_01.png</file>
<file>psi_02.png</file>
<file>psi_03.png</file>
@@ -0,0 +1,23 @@
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#include "JFJochAcknowledgementWindow.h"
#include <QFile>
#include <QTextBrowser>
#include <QVBoxLayout>
JFJochAcknowledgementWindow::JFJochAcknowledgementWindow(QWidget *parent) : QDialog(parent) {
setWindowTitle("Acknowledgements");
resize(800, 600);
auto *browser = new QTextBrowser(this);
browser->setOpenExternalLinks(true); // DOI and repository links open in the system browser
QFile file(":/ACKNOWLEDGEMENT.md");
if (file.open(QIODevice::ReadOnly | QIODevice::Text))
browser->setMarkdown(QString::fromUtf8(file.readAll()));
auto *layout = new QVBoxLayout(this);
layout->addWidget(browser);
}
@@ -0,0 +1,14 @@
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include <QDialog>
// Modeless window showing the acknowledgements (from :/ACKNOWLEDGEMENT.md, the same file the
// documentation is built from).
class JFJochAcknowledgementWindow : public QDialog {
Q_OBJECT
public:
explicit JFJochAcknowledgementWindow(QWidget *parent = nullptr);
};
@@ -0,0 +1,75 @@
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#include "JFJochMouseShortcutsWindow.h"
#include <QTextBrowser>
#include <QVBoxLayout>
namespace {
struct Shortcut { QString action; QString effect; };
struct Section { QString title; QVector<Shortcut> rows; };
QString BuildHtml() {
const QVector<Section> sections = {
// JFJochImage / JFJochDiffractionImage mouse handling
{"Diffraction image", {
{"Wheel", "Zoom in / out, centred on the cursor"},
{"Shift + wheel", "Move the foreground (upper contrast limit) in linear steps"},
{"Ctrl + wheel", "Move the foreground in multiplicative steps (&times;1.15 per notch)"},
{"Alt + wheel", "Step through the dataset, one image per notch (up = next image)"},
{"F held + wheel", "Same as Shift + wheel, for as long as F is held"},
{"A", "Switch on the automatic foreground"},
{"Hover", "Status bar shows the pixel position, its value and the resolution"},
{"Drag", "Pan the image"},
{"Shift + drag", "Draw a rectangular ROI"},
{"Shift + Ctrl + drag", "Draw a circular ROI"},
{"Drag an ROI or its handle", "Move or resize the selected ROI"},
{"Right click", "Copy / save the image, fit to view, clear the ROI"},
}},
// JFJochGridScanImage
{"Grid scan", {
{"Hover", "Status bar shows the image number, the grid position and its value"},
{"Shift + hover", "Load the image under the cursor while moving over the grid"},
{"Double click", "Load the image under the cursor"},
}},
{"Other views", {
{"2D azimuthal image: double click", "Zoom the diffraction image on the corresponding detector position"},
{"Dataset-info plot: hover", "Status bar shows the image number and the plotted value"},
{"Dataset-info plot: Shift + hover", "Load the hovered image"},
{"Spot / reflection list: double click", "Zoom the diffraction image on that spot or prediction"},
{"Image list: double click", "Load that image"},
{"Reciprocal space: drag", "Rotate the view"},
{"Reciprocal space: right drag", "Pan the view"},
{"Reciprocal space: wheel", "Zoom the view"},
{"Reciprocal space: double click", "Zoom the diffraction image on the nearest spot"},
{"Magnifier: wheel", "Zoom the magnifier; it follows the cursor on the main image"},
}},
};
QString html = "<html><body>";
for (const auto &section : sections) {
html += "<h3>" + section.title + "</h3>";
html += "<table cellspacing='0' cellpadding='4' width='100%'>";
for (const auto &row : section.rows)
html += "<tr><td width='38%'><b>" + row.action + "</b></td><td>" + row.effect + "</td></tr>";
html += "</table>";
}
html += "<p><i>Note:</i> some window managers take Alt-modified mouse events for themselves "
"before the application sees them. That is a window-manager setting, not something "
"the viewer can take back.</p>";
html += "</body></html>";
return html;
}
}
JFJochMouseShortcutsWindow::JFJochMouseShortcutsWindow(QWidget *parent) : QDialog(parent) {
setWindowTitle("Mouse Shortcuts");
resize(620, 620);
auto *browser = new QTextBrowser(this);
browser->setHtml(BuildHtml());
auto *layout = new QVBoxLayout(this);
layout->addWidget(browser);
}
@@ -0,0 +1,13 @@
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include <QDialog>
// Modeless cheat sheet of the mouse (and mouse-modifying key) shortcuts of the viewer.
class JFJochMouseShortcutsWindow : public QDialog {
Q_OBJECT
public:
explicit JFJochMouseShortcutsWindow(QWidget *parent = nullptr);
};