Files
Jungfraujoch/viewer/widgets/JFJochViewerFileBrowser.cpp
T
leonarski_fandClaude Opus 5 a7a4c28925
Build Packages / Unit tests (push) Successful in 49m15s
Build Packages / build:windows:nocuda (push) Successful in 18m5s
Build Packages / build:windows:cuda (push) Successful in 20m26s
Build Packages / build:viewer-tgz:cpu (push) Successful in 7m17s
Build Packages / build:viewer-tgz:cuda (push) Successful in 8m15s
Build Packages / build:rugnux-tgz (x86_64) (push) Successful in 6m57s
Build Packages / build:rugnux:windows (push) Successful in 10m56s
Build Packages / build:rugnux:aarch64 (cross) (push) Successful in 5m0s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 10m21s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 8m22s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 11m49s
Build Packages / build:rpm (rocky8) (push) Successful in 11m57s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 9m5s
Build Packages / XDS test (neggia plugin) (push) Successful in 5m45s
Build Packages / Generate python client (push) Successful in 10s
Build Packages / Build documentation (push) Successful in 36s
Build Packages / Create release (push) Skipped
Build Packages / XDS test (JFJoch plugin) (push) Failing after 3h12m36s
Build Packages / XDS test (durin plugin) (push) Failing after 3h12m38s
Build Packages / DIALS test (push) Failing after 3h12m40s
Build Packages / build:rpm (ubuntu2204) (push) Failing after 3h12m42s
Build Packages / build:rpm (rocky9) (push) Failing after 3h12m44s
Build Packages / build:rpm (rocky9_sls9) (push) Failing after 3h12m46s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Failing after 3h12m48s
Build Packages / build:rpm (rocky9_nocuda) (push) Failing after 3h12m50s
jfjoch_viewer: the inspector states the numbers it used to hide
Six changes to the desktop viewer, all in what it shows and what it is
called:

- The left-hand reprocessing panel is titled "Processing" (it sits next
  to "File manager"); the jobs panel that held that name is "Jobs".
- The file manager lists *.cbf beside the HDF5 datasets. A CBF is one
  frame per file and opening any of them opens its sweep, which is what
  the File > Open dialog already did.
- Image statistics: collection time, detector distance, beam centre and
  wavelength were in the dataset row's tooltip, where a number nobody
  hovers over is a number nobody reads. They are rows of their own, in
  the order a person asks for them - what the dataset is, how it was
  collected, then what this image contains. Source/instrument and sample
  are bold like every other value. The B-factor row is gone.
- A "Spots + background" plot: spot count on the left axis, background
  per pixel on the right, since a common scale would flatten one of
  them. The axis labels take the colour of their line - a legend costs
  exactly the vertical room those labels need at this dock height. It is
  what a live (HTTP) connection selects by default, until the user picks
  something else.
- Expanding the powder-calibration section no longer widens the settings
  panel past the space it has. Two buttons side by side cannot be
  narrower than both their labels, and a combo is never narrower than
  its longest entry - a 40-character cell in the calibrant list set the
  minimum width of the whole dock. One button per row, the cell in a
  tooltip, and both combos allowed to elide.
- The grid/per-image toggles both rebuild the metric selector now. The
  grid button did not, so leaving per-image mode through it left the
  per-image plot types listed - X-ray fluorescence offered as a metric
  of a grid scan - and their values were then read as per-dataset
  metrics. Each mode also remembers its own selection instead of
  carrying an index across two unrelated lists.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oVDk7aukYqfPZBLHn5nGg
2026-09-11 22:07:54 +02:00

132 lines
5.4 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::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);
}