// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute // SPDX-License-Identifier: GPL-3.0-only #include "JFJochViewerFileBrowser.h" #include #include #include #include #include #include #include #include #include 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(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), 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 among // files. The many _data_NNNNNN.h5 shards of a dataset are not meant to be opened directly. model_->setNameFilters({"*_master.h5", "*_process.h5"}); 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::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); }