diff --git a/docs/ACKNOWLEDGEMENT.md b/docs/ACKNOWLEDGEMENT.md index f06c7ad7a..40bb25756 100644 --- a/docs/ACKNOWLEDGEMENT.md +++ b/docs/ACKNOWLEDGEMENT.md @@ -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. diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index f54afe26e..919ede852 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -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 diff --git a/docs/JFJOCH_VIEWER.md b/docs/JFJOCH_VIEWER.md index 0d96cbc34..8edccd920 100644 --- a/docs/JFJOCH_VIEWER.md +++ b/docs/JFJOCH_VIEWER.md @@ -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. diff --git a/viewer/CMakeLists.txt b/viewer/CMakeLists.txt index a56b719f6..482cb72c2 100644 --- a/viewer/CMakeLists.txt +++ b/viewer/CMakeLists.txt @@ -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 diff --git a/viewer/JFJochViewerMenu.cpp b/viewer/JFJochViewerMenu.cpp index e66c3bd0a..0025bc7a7 100644 --- a/viewer/JFJochViewerMenu.cpp +++ b/viewer/JFJochViewerMenu.cpp @@ -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, diff --git a/viewer/JFJochViewerMenu.h b/viewer/JFJochViewerMenu.h index 50f49ef0f..82b926716 100644 --- a/viewer/JFJochViewerMenu.h +++ b/viewer/JFJochViewerMenu.h @@ -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(); diff --git a/viewer/JFJochViewerSidePanel.cpp b/viewer/JFJochViewerSidePanel.cpp index 63cca2551..a0b3a12af 100644 --- a/viewer/JFJochViewerSidePanel.cpp +++ b/viewer/JFJochViewerSidePanel.cpp @@ -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); } diff --git a/viewer/JFJochViewerSidePanel.h b/viewer/JFJochViewerSidePanel.h index 938c9a9d2..833c6ff74 100644 --- a/viewer/JFJochViewerSidePanel.h +++ b/viewer/JFJochViewerSidePanel.h @@ -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); }; diff --git a/viewer/JFJochViewerWindow.cpp b/viewer/JFJochViewerWindow.cpp index 215557691..c48730c5b 100644 --- a/viewer/JFJochViewerWindow.cpp +++ b/viewer/JFJochViewerWindow.cpp @@ -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); diff --git a/viewer/image_viewer/JFJochDiffractionImage.cpp b/viewer/image_viewer/JFJochDiffractionImage.cpp index 05b99efbb..dfd2e6e84 100644 --- a/viewer/image_viewer/JFJochDiffractionImage.cpp +++ b/viewer/image_viewer/JFJochDiffractionImage.cpp @@ -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(); diff --git a/viewer/image_viewer/JFJochDiffractionImage.h b/viewer/image_viewer/JFJochDiffractionImage.h index c94a7149c..24e2dd1c1 100644 --- a/viewer/image_viewer/JFJochDiffractionImage.h +++ b/viewer/image_viewer/JFJochDiffractionImage.h @@ -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); }; diff --git a/viewer/resources/resources.qrc b/viewer/resources/resources.qrc index 6ec0fb4b5..a78b97baf 100644 --- a/viewer/resources/resources.qrc +++ b/viewer/resources/resources.qrc @@ -7,6 +7,7 @@ jfjoch.png third_party_licenses.html + ../../docs/ACKNOWLEDGEMENT.md psi_01.png psi_02.png psi_03.png diff --git a/viewer/windows/JFJochAcknowledgementWindow.cpp b/viewer/windows/JFJochAcknowledgementWindow.cpp new file mode 100644 index 000000000..a4c279a44 --- /dev/null +++ b/viewer/windows/JFJochAcknowledgementWindow.cpp @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute +// SPDX-License-Identifier: GPL-3.0-only + +#include "JFJochAcknowledgementWindow.h" + +#include +#include +#include + +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); +} diff --git a/viewer/windows/JFJochAcknowledgementWindow.h b/viewer/windows/JFJochAcknowledgementWindow.h new file mode 100644 index 000000000..7b1bf67f3 --- /dev/null +++ b/viewer/windows/JFJochAcknowledgementWindow.h @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute +// SPDX-License-Identifier: GPL-3.0-only + +#pragma once + +#include + +// 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); +}; diff --git a/viewer/windows/JFJochMouseShortcutsWindow.cpp b/viewer/windows/JFJochMouseShortcutsWindow.cpp new file mode 100644 index 000000000..5793ec891 --- /dev/null +++ b/viewer/windows/JFJochMouseShortcutsWindow.cpp @@ -0,0 +1,75 @@ +// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute +// SPDX-License-Identifier: GPL-3.0-only + +#include "JFJochMouseShortcutsWindow.h" + +#include +#include + +namespace { + struct Shortcut { QString action; QString effect; }; + struct Section { QString title; QVector rows; }; + + QString BuildHtml() { + const QVector
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 (×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 = ""; + for (const auto §ion : sections) { + html += "

" + section.title + "

"; + html += ""; + for (const auto &row : section.rows) + html += ""; + html += "
" + row.action + "" + row.effect + "
"; + } + html += "

Note: 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.

"; + 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); +} diff --git a/viewer/windows/JFJochMouseShortcutsWindow.h b/viewer/windows/JFJochMouseShortcutsWindow.h new file mode 100644 index 000000000..0467a1455 --- /dev/null +++ b/viewer/windows/JFJochMouseShortcutsWindow.h @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute +// SPDX-License-Identifier: GPL-3.0-only + +#pragma once + +#include + +// 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); +};