The dataset-info plot remembers the selected quantity and the Grid/Per-image toggle states across restarts, with grid scans and standard scans each keeping their own favourite (QSettings groups datasetInfoPlot/gridScan and /standard). Choices are stored as the combo item's data code, not its index, so a saved choice survives the plot list changing and a code no longer offered falls back to the first item. Favourites are saved on every explicit user action and applied whenever a dataset of the other kind loads; loads of the same kind keep the in-session state, and the live-source default still wins until the user picks. The unconditional Grid auto-check on grid scans moved into the favourite's default, so unchecking Grid on a grid dataset now sticks. View > "Reset all settings to defaults" clears the whole settings store after confirmation and brings the running session to a first-start state in place: default layout, window size, display settings (Indigo, Auto off, HDR off), plot choices, and the file manager root. Because the live state then equals the defaults, saving on close stays enabled and later changes persist as usual. File > Quit now closes the main window instead of calling QApplication::quit(), so closeEvent runs and the persistent settings are saved on that path too - previously they were saved only when the window was closed by the window manager. The file manager root persistence was audited and left unchanged: saved on every successful change, restored behind JUNGFRAUJOCH_DATA_ROOT, and a vanished directory falls back to the home directory. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WbjnQhutnboy4qbxwmgDAX
302 lines
12 KiB
C++
302 lines
12 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 "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); });
|
|
|
|
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);
|
|
}
|
|
|
|
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 <filip.leonarski@psi.ch></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);;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();
|
|
}
|
|
}
|