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
136 lines
5.5 KiB
C++
136 lines
5.5 KiB
C++
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
#include "JFJochViewerFileBrowser.h"
|
|
|
|
#include <QVBoxLayout>
|
|
#include <QHBoxLayout>
|
|
#include <QLineEdit>
|
|
#include <QToolButton>
|
|
#include <QTreeView>
|
|
#include <QHeaderView>
|
|
#include <QFileDialog>
|
|
#include <QSettings>
|
|
#include <QDir>
|
|
|
|
namespace {
|
|
constexpr auto kDataRootEnvVar = "JUNGFRAUJOCH_DATA_ROOT";
|
|
constexpr auto kSettingsKey = "fileBrowserRoot";
|
|
|
|
// The filter box narrows files only; a directory always passes. Filtering directories too would
|
|
// hide everything below one whose own name does not match - including the files that do match -
|
|
// and would hide the root's ancestors, which leaves the tree with no valid root index at all.
|
|
class FileOnlyFilterProxy : public QSortFilterProxyModel {
|
|
public:
|
|
using QSortFilterProxyModel::QSortFilterProxyModel;
|
|
|
|
protected:
|
|
bool filterAcceptsRow(int row, const QModelIndex &parent) const override {
|
|
const auto *fs = qobject_cast<const QFileSystemModel *>(sourceModel());
|
|
if (fs && fs->isDir(fs->index(row, 0, parent)))
|
|
return true;
|
|
return QSortFilterProxyModel::filterAcceptsRow(row, parent);
|
|
}
|
|
};
|
|
}
|
|
|
|
QString JFJochViewerFileBrowser::DefaultRoot() {
|
|
// Site default (e.g. a beamline launcher setting it to /sls/mx/data/p<group>), then whatever
|
|
// root was last browsed to (so re-launching without the variable set doesn't start over from
|
|
// scratch), then the user's home - never the current directory, which for an installed binary
|
|
// is typically the install location rather than anywhere the user would keep data.
|
|
const QString env_root = qEnvironmentVariable(kDataRootEnvVar);
|
|
if (!env_root.isEmpty() && QDir(env_root).exists())
|
|
return env_root;
|
|
|
|
const QString last_root = QSettings("PSI", "jfjoch_viewer").value(kSettingsKey).toString();
|
|
if (!last_root.isEmpty() && QDir(last_root).exists())
|
|
return last_root;
|
|
|
|
return QDir::homePath();
|
|
}
|
|
|
|
JFJochViewerFileBrowser::JFJochViewerFileBrowser(QWidget *parent) : QWidget(parent) {
|
|
auto *layout = new QVBoxLayout(this);
|
|
layout->setContentsMargins(4, 4, 4, 4);
|
|
|
|
auto *root_row = new QHBoxLayout();
|
|
root_edit_ = new QLineEdit(this);
|
|
root_edit_->setToolTip("Data root - type a path or use Browse…");
|
|
auto *browse_button = new QToolButton(this);
|
|
browse_button->setText("…");
|
|
browse_button->setToolTip("Browse for a data root…");
|
|
root_row->addWidget(root_edit_);
|
|
root_row->addWidget(browse_button);
|
|
layout->addLayout(root_row);
|
|
|
|
filter_edit_ = new QLineEdit(this);
|
|
filter_edit_->setPlaceholderText("Filter…");
|
|
filter_edit_->setClearButtonEnabled(true);
|
|
layout->addWidget(filter_edit_);
|
|
|
|
model_ = new QFileSystemModel(this);
|
|
model_->setFilter(QDir::AllDirs | QDir::Files | QDir::NoDotAndDotDot);
|
|
// Directories are always listed regardless of these; only *_master.h5 / *_process.h5 and
|
|
// miniCBF frames among files. The many _data_NNNNNN.h5 shards of a dataset are not meant to be
|
|
// opened directly; a .cbf is one frame per file, and opening any of them opens its whole sweep.
|
|
model_->setNameFilters({"*_master.h5", "*_process.h5", "*.cbf"});
|
|
model_->setNameFilterDisables(false); // hide non-matching files rather than just greying them out
|
|
|
|
proxy_ = new FileOnlyFilterProxy(this);
|
|
proxy_->setSourceModel(model_);
|
|
proxy_->setFilterKeyColumn(0);
|
|
proxy_->setFilterCaseSensitivity(Qt::CaseInsensitive);
|
|
// Deliberately not recursive: it only ever narrows names already listed at an expanded level,
|
|
// never forces fetching a collapsed subtree just to search inside it.
|
|
|
|
tree_ = new QTreeView(this);
|
|
tree_->setModel(proxy_);
|
|
tree_->setHeaderHidden(true);
|
|
for (int col = 1; col < model_->columnCount(); ++col)
|
|
tree_->hideColumn(col);
|
|
tree_->setSortingEnabled(false);
|
|
layout->addWidget(tree_);
|
|
|
|
connect(browse_button, &QToolButton::clicked, this, &JFJochViewerFileBrowser::onBrowseClicked);
|
|
connect(root_edit_, &QLineEdit::editingFinished, this, &JFJochViewerFileBrowser::onRootEditingFinished);
|
|
connect(filter_edit_, &QLineEdit::textChanged, proxy_, &QSortFilterProxyModel::setFilterFixedString);
|
|
connect(tree_, &QTreeView::doubleClicked, this, &JFJochViewerFileBrowser::onDoubleClicked);
|
|
|
|
SetRoot(DefaultRoot());
|
|
}
|
|
|
|
void JFJochViewerFileBrowser::SetRoot(const QString &path) {
|
|
QDir dir(path);
|
|
if (!dir.exists())
|
|
return;
|
|
|
|
const QString canonical = QDir::toNativeSeparators(dir.absolutePath());
|
|
root_edit_->setText(canonical);
|
|
model_->setRootPath(canonical);
|
|
tree_->setRootIndex(proxy_->mapFromSource(model_->index(canonical)));
|
|
|
|
QSettings("PSI", "jfjoch_viewer").setValue(kSettingsKey, canonical);
|
|
}
|
|
|
|
void JFJochViewerFileBrowser::ResetToDefaultRoot() {
|
|
SetRoot(DefaultRoot());
|
|
}
|
|
|
|
void JFJochViewerFileBrowser::onBrowseClicked() {
|
|
const QString dir = QFileDialog::getExistingDirectory(this, "Select data root", root_edit_->text());
|
|
if (!dir.isEmpty())
|
|
SetRoot(dir);
|
|
}
|
|
|
|
void JFJochViewerFileBrowser::onRootEditingFinished() {
|
|
SetRoot(root_edit_->text());
|
|
}
|
|
|
|
void JFJochViewerFileBrowser::onDoubleClicked(const QModelIndex &index) {
|
|
const QModelIndex source = proxy_->mapToSource(index);
|
|
if (!source.isValid() || model_->isDir(source))
|
|
return; // directories: let the tree's own expand/collapse handle the double click
|
|
emit openDataset(QDir::toNativeSeparators(model_->filePath(source)), 0, 1, false);
|
|
}
|