Files
Jungfraujoch/viewer/JFJochViewerMenu.cpp
T
leonarski_f 30b6800289
Build Packages / build:windows:nocuda (push) Successful in 17m20s
Build Packages / build:windows:cuda (push) Successful in 19m52s
Build Packages / build:viewer-tgz:cpu (push) Successful in 9m38s
Build Packages / build:viewer-tgz:cuda (push) Successful in 11m18s
Build Packages / build:rugnux-tgz (x86_64) (push) Successful in 9m34s
Build Packages / build:rugnux:aarch64 (cross) (push) Successful in 5m42s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 20m33s
Build Packages / Create release (push) Successful in 33s
Build Packages / build:rugnux:windows (push) Successful in 12m0s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 15m42s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 14m59s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 16m8s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 14m35s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 16m55s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 16m58s
Build Packages / Generate python client (push) Successful in 16s
Build Packages / build:rpm (rocky8) (push) Successful in 15m21s
Build Packages / Build documentation (push) Successful in 54s
Build Packages / build:rpm (rocky9) (push) Successful in 16m23s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 12m2s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 10m6s
Build Packages / Unit tests (push) Successful in 1h10m26s
v1.0.0-rc.171 (#81)
* Rugnux: basic support for CCD images (marCCD, SMV) and for gzipped miniCBF.
* `jfjoch_viewer`: opens the CCD formats, and fixes to the dataset plots.
* Documentation updates.

Reviewed-on: #81
Co-authored-by: Filip Leonarski <filip.leonarski@psi.ch>
2026-09-17 14:42:52 +02:00

351 lines
14 KiB
C++

// SPDX-FileCopyrightText: 2025 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#include "JFJochViewerMenu.h"
#include "RemoteDisplayMode.h"
#include <QMessageBox>
#include <QApplication>
#include <QDir>
#include <QFileDialog>
#include <QString>
#include <QGridLayout>
#include <QLabel>
#include <QLineEdit>
#include <QSpinBox>
#include <QComboBox>
#include <QDialogButtonBox>
#include <QDockWidget>
#include <QSettings>
#include <algorithm>
#include "JFJochViewerWindow.h"
#include "windows/JFJochLicenseWindow.h"
#include "windows/JFJochMouseShortcutsWindow.h"
#include "windows/JFJochAcknowledgementWindow.h"
#include "../common/GitInfo.h"
#include "../common/CUDAWrapper.h"
JFJochViewerMenu::JFJochViewerMenu(QWidget *parent) : QMenuBar(parent) {
// Create "File" menu
QMenu *fileMenu = addMenu("File");
QAction *openAction = fileMenu->addAction("Open");
openAction->setShortcut(QKeySequence::Open);
connect(openAction, &QAction::triggered, this, &JFJochViewerMenu::openSelected);
QAction *openHttpAction = fileMenu->addAction("Open HTTP");
openHttpAction->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_H));
connect(openHttpAction, &QAction::triggered, this, &JFJochViewerMenu::openHttpSelected);
QAction *closeAction = fileMenu->addAction("Close");
closeAction->setShortcut(QKeySequence::Close);
connect(closeAction, &QAction::triggered, this, &JFJochViewerMenu::closeSelected);
// Add seperator
fileMenu->addSeparator();
// Add "Quit" action
QAction *quitAction = fileMenu->addAction("Quit");
quitAction->setShortcut(QKeySequence::Quit);
connect(quitAction, &QAction::triggered, this, &JFJochViewerMenu::quitSelected);
QMenu *maskMenu = addMenu("Mask");
const QAction *saveMaskTiff = maskMenu->addAction("Save User Mask as TIFF...");
connect(saveMaskTiff, &QAction::triggered, this, &JFJochViewerMenu::saveUserMaskAsTiffSelected);
const QAction *loadMaskReplace = maskMenu->addAction("Load User Mask from TIFF (replace)...");
connect(loadMaskReplace, &QAction::triggered, [this] { openLoadUserMaskTiff(true); });
const QAction *loadMaskAdd = maskMenu->addAction("Load User Mask from TIFF (add)...");
connect(loadMaskAdd, &QAction::triggered, [this] { openLoadUserMaskTiff(false); });
const QAction *uploadMask = maskMenu->addAction("&Upload User Mask");
connect(uploadMask, &QAction::triggered, this, &JFJochViewerMenu::uploadUserMaskAction);
const QAction *clearMask = maskMenu->addAction("Clear User Mask");
connect(clearMask, &QAction::triggered, [this] () {emit clearUserMaskSelected();});
windowMenu = addMenu("Window");
QMenu *dockMenu = addMenu("Charts");
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);
const QAction *resetSettings = viewMenu->addAction("Reset all settings to defaults");
connect(resetSettings, &QAction::triggered, this, &JFJochViewerMenu::resetSettingsSelected);
viewMenu->addSeparator();
// Auto-detected from the session (ssh -X and the like); this toggle is the manual override.
QAction *remoteMode = viewMenu->addAction("Remote display mode (limit repaints)");
remoteMode->setCheckable(true);
remoteMode->setChecked(RemoteDisplayMode());
connect(remoteMode, &QAction::toggled, this, [](bool on) { SetRemoteDisplayMode(on); });
viewMenu->addSeparator();
// The desktop's own text scaling stays the primary mechanism, but few users know where it lives,
// so the viewer carries a zoom of its own. It multiplies the font the desktop handed us, so it
// composes with GNOME's "Large Text" or QT_SCALE_FACTOR instead of fighting them.
baseFont = QApplication::font();
QMenu *fontMenu = viewMenu->addMenu("Font size");
QAction *fontLarger = fontMenu->addAction("Larger");
fontLarger->setShortcuts({QKeySequence::ZoomIn,
QKeySequence(Qt::ControlModifier | Qt::Key_Equal),
QKeySequence(Qt::ControlModifier | Qt::KeypadModifier | Qt::Key_Plus)});
connect(fontLarger, &QAction::triggered, this, [this] { ApplyFontZoom(fontZoom + 25); });
QAction *fontSmaller = fontMenu->addAction("Smaller");
fontSmaller->setShortcuts({QKeySequence::ZoomOut,
QKeySequence(Qt::ControlModifier | Qt::KeypadModifier | Qt::Key_Minus)});
connect(fontSmaller, &QAction::triggered, this, [this] { ApplyFontZoom(fontZoom - 25); });
fontMenu->addSeparator();
fontZoomGroup = new QActionGroup(this);
for (int percent: {100, 125, 150}) {
QAction *level = fontMenu->addAction(QString("%1 %").arg(percent));
level->setCheckable(true);
level->setData(percent);
fontZoomGroup->addAction(level);
connect(level, &QAction::triggered, this, [this, percent] { ApplyFontZoom(percent); });
}
ApplyFontZoom(QSettings("PSI", "jfjoch_viewer").value("fontZoom", 100).toInt());
QMenu *helpMenu = addMenu("Help");
// Add "About" action
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);
}
// Three levels, 25 % apart: clamping here is what stops the two stepping actions at the ends.
void JFJochViewerMenu::ApplyFontZoom(int percent) {
fontZoom = std::clamp(percent, 100, 150);
QFont font = baseFont;
font.setPointSizeF(baseFont.pointSizeF() * fontZoom / 100.0);
QApplication::setFont(font);
// setFont on its own only reaches widgets built after it. This is what carries the new size into
// the ones already on screen, and it keeps each widget's own attributes (a bold title stays bold).
QEvent font_change(QEvent::ApplicationFontChange);
for (QWidget *widget: QApplication::allWidgets())
QApplication::sendEvent(widget, &font_change);
QSettings("PSI", "jfjoch_viewer").setValue("fontZoom", fontZoom);
for (QAction *level: fontZoomGroup->actions())
level->setChecked(level->data().toInt() == fontZoom);
}
void JFJochViewerMenu::ResetFontZoom() {
ApplyFontZoom(100);
}
void JFJochViewerMenu::AddWindowEntry(JFJochHelperWindow *window, const QString &name) {
auto action = windowMenu->addAction(name);
action->setCheckable(true);
action->setChecked(false);
connect(action, &QAction::toggled, window, &JFJochHelperWindow::toggle);
connect(window, &JFJochHelperWindow::closing, [action] {action->setChecked(false);});
}
void JFJochViewerMenu::AddDockEntry(QDockWidget *dock, const QString &name) {
auto *action = dock->toggleViewAction(); // checkable, stays in sync with the dock visibility
action->setText(name);
windowMenu->addAction(action);
}
void JFJochViewerMenu::aboutSelected() {
QPixmap pixmap(":/jfjoch.png");
QString version(QString::fromStdString(jfjoch_version()));
// Create QMessageBox
QMessageBox aboutBox(this);
aboutBox.setWindowTitle("About");
QString about_text = "<h3>Jungfraujoch Image Viewer</h3>" // Use HTML for improved style
"<p><b>Version:</b> " + version + "</p>"
"<p><b>Copyright:</b> 2019-2025 Paul Scherrer Institute</p>"
"<p><b>Author:</b> Filip Leonarski &lt;filip.leonarski@psi.ch&gt;</p>"
"<p><b>Links:</b> <a href='https://jungfraujoch.readthedocs.io'>Documentation</a>, "
"<a href='https://gitea.psi.ch/mx/jungfraujoch'>Repository</a></p>";
#ifdef JFJOCH_USE_CUDA
about_text += QString("<p>Compiled with CUDA support. %1 GPU(s) detected</p>")
.arg(QString::number(get_gpu_count()));
#else
about_text += "<p><b>Note:</b> CUDA is not available on this system.</p>";
#endif
// Set detailed text with proper spacing and structure
aboutBox.setText(about_text);
// Optional: Add a larger informative text, if needed
aboutBox.setInformativeText(
"This program comes with ABSOLUTELY NO WARRANTY. "
"This is free software, and you are welcome to redistribute it "
"under certain conditions (GPLv3).\n\n"
"Development supported by Innovation Project (101.535.1 IP-ENG) from Innosuisse."
);
// Set an icon
aboutBox.setIconPixmap(pixmap);
// Execute the dialog
aboutBox.exec();
}
void JFJochViewerMenu::licensesSelected() {
// Lazily create the modeless window and keep it; raise it if already open.
if (licenseWindow == nullptr)
licenseWindow = new JFJochLicenseWindow(window());
licenseWindow->show();
licenseWindow->raise();
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,
"Open File", // Dialog title
"", // Default folder
// A CBF is one frame per file: picking any frame opens the sweep it belongs to.
"HDF5 Master Files (*_master.h5 *_process.h5);; HDF5 Files (*.h5);; CBF Images (*.cbf *.cbf.gz);; CCD Images (*.img *.mccd *.smv);;All Files (*)"
);
if (!fileName.isEmpty())
// Qt returns '/'-separated paths even on Windows, but HDF5's H5Fopen needs native separators
// to recognise a UNC path (\\host\share, e.g. the WSL2 filesystem): '//host/share' is rejected,
// while 'C:/...' still works, which is exactly the reported symptom. No-op on Linux.
emit fileOpenSelected(QDir::toNativeSeparators(fileName), 0, 1, false);
}
void JFJochViewerMenu::closeSelected() {
emit fileCloseSelected();
}
void JFJochViewerMenu::quitSelected() {
// Through close(), not QApplication::quit(): closeEvent is where the persistent settings
// are saved, and quit() would bypass it.
window()->close();
}
void JFJochViewerMenu::openHttpSelected() {
QDialog dialog(this);
dialog.setWindowTitle("Open HTTP Connection");
dialog.setMinimumWidth(400);
QGridLayout *layout = new QGridLayout(&dialog);
// Scheme selector: plain HTTP (broker directly) or HTTPS (e.g. broker behind a reverse proxy).
QComboBox *schemeCombo = new QComboBox(&dialog);
schemeCombo->addItem("http://");
schemeCombo->addItem("https://");
layout->addWidget(schemeCombo, 0, 0);
layout->addWidget(new QLabel(":"), 0, 2);
QLineEdit *hostEdit = new QLineEdit(&dialog);
char *host = std::getenv("JUNGFRAUJOCH_HTTP_HOST");
if (host != nullptr)
hostEdit->setText(host);
else
hostEdit->setText("localhost");
layout->addWidget(hostEdit, 0, 1);
QSpinBox *portSpinBox = new QSpinBox(&dialog);
portSpinBox->setRange(1, 65535);
char *port_std = std::getenv("JUNGFRAUJOCH_HTTP_PORT");
if (port_std != nullptr) {
const int tmp = std::stoi(std::string(port_std));
if (tmp < 65536 && tmp > 0)
portSpinBox->setValue(tmp);
} else
portSpinBox->setValue(8080);
layout->addWidget(portSpinBox, 0, 3);
QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
connect(buttonBox, &QDialogButtonBox::accepted, &dialog, &QDialog::accept);
connect(buttonBox, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
layout->addWidget(buttonBox, 1, 0, 1, 4);
dialog.setLayout(layout);
if (dialog.exec() == QDialog::Accepted) {
QString url = QString("%1%2:%3")
.arg(schemeCombo->currentText())
.arg(hostEdit->text())
.arg(portSpinBox->value());
emit fileOpenSelected(url, -1, 1, false);
}
}
void JFJochViewerMenu::saveUserMaskAsTiffSelected() {
QString filename = QFileDialog::getSaveFileName(
this,
"Save User Mask as TIFF",
"",
"TIFF Images (*.tif *.tiff);;All Files (*)"
);
if (!filename.isEmpty()) {
emit saveUserMaskTiffSelected(filename);
}
}
void JFJochViewerMenu::openLoadUserMaskTiff(bool replace) {
QString filename = QFileDialog::getOpenFileName(
this,
replace ? "Load User Mask from TIFF (replace)" : "Load User Mask from TIFF (add)",
"",
"TIFF Images (*.tif *.tiff);;All Files (*)"
);
if (!filename.isEmpty())
emit loadUserMaskTiffSelected(filename, replace);
}
void JFJochViewerMenu::uploadUserMaskAction() {
auto reply = QMessageBox::question(
this,
"Upload User Mask",
"Upload current user mask to the server?",
QMessageBox::Yes | QMessageBox::No,
QMessageBox::No
);
if (reply == QMessageBox::Yes) {
emit uploadUserMaskSelected();
}
}