The native miniCBF reader added for rugnux is a plain JFJochReader, so the viewer only needed to be told which one to open a file with. JFJochImageReadingWorker held a concrete JFJochHDF5Reader; it now holds both readers and a JFJochReader* pointing at whichever the open file needs, chosen by JFJochCBFReader::CanRead. Naming any frame opens the whole sweep - the reader's own template matcher decides which frames belong to it, so a directory holding two sweeps or XDS auxiliary files is not spliced together. The previous file is closed before the new one is opened, so switching format between HDF5 and CBF in one session leaves nothing behind. Everything the viewer draws already goes through JFJochReader and JFJochReaderDataset, so nothing else had to move. The two HDF5-only features stay on the HDF5 reader: calibration images, and the reprocessing snapshots, which are metadata read back over the same images the reader is serving and have nothing to attach to for a directory of CBFs. A raw sweep therefore shows geometry and pictures with an empty run list, which is the state a live HTTP stream is already in. Reprocessing jobs run on a CBF sweep too - JFJochProcessController opens its own reader the same way, and Rugnux has handled a CBF source since the reader landed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lc5JG6kJqZoCWaoZ43JGTW
265 lines
9.8 KiB
C++
265 lines
9.8 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 <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 "../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);
|
|
|
|
QMenu *helpMenu = addMenu("Help");
|
|
// Add "About" action
|
|
const QAction *aboutAction = helpMenu->addAction("About");
|
|
connect(aboutAction, &QAction::triggered, this, &JFJochViewerMenu::aboutSelected);
|
|
|
|
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::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() {
|
|
QApplication::quit();
|
|
}
|
|
|
|
|
|
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();
|
|
}
|
|
}
|