Files
Jungfraujoch/viewer/widgets/JFJochViewerFileBrowser.cpp
T
leonarski_fandClaude Sonnet 5 d24cdfcff4 jfjoch_viewer: navigation keys, one-shot auto-contrast, file manager panel, cleanup
* Dataset-info plot hover shows a horizontal crosshair alongside the existing
  vertical one, so a run of hovers reads as a level or a drift at a glance.
* `A` applies auto-contrast once instead of switching on persistent Auto mode
  (a new `oneShotAutoForeground()`, distinct from the toolbar's Auto toggle);
  pressing it a second time on the same image switches Auto on for good, and
  pressing it while Auto is already on leaves it on. The value itself comes
  from one `AutoForegroundValue()` shared with the continuous Auto path.
* `Home`/`End`/`Page Up`/`Page Down` navigate the dataset (first/last image,
  one image forward/back); claimed in the diffraction view's keyPressEvent
  before QGraphicsView's default handling, which otherwise eats them to
  scroll the viewport.
* Alt+wheel dataset navigation is removed - some window managers already
  took it before the app ever saw it, per the caveat that used to sit next
  to its entry in the shortcuts list.
* The image strip dock and its dedicated thumbnail-rendering machinery in
  the reading worker (SetThumbnail*, RenderThumbnail_i) are removed; it cost
  a lot and nothing else used any of it.
* Toolbar text: "Colour" -> "Color".
* Color map, Auto and HDR mode now persist across sessions, the same way the
  window layout already does.
* The Inspector's "Dataset:" path wraps at '/' (a zero-width space after
  each one) instead of being cut off when it doesn't fit one line.
* LoadFile no longer reopens an already-open file - it forwards straight to
  LoadImage instead - which was the likely cause of a D-Bus client's per-image
  navigation lagging behind the same navigation done via the grid-scan hover.
* New file-manager dock (left, tabbed with Settings via tabifyDockWidget):
  a directory tree filtered to *_master.h5/*_process.h5, rooted at
  JUNGFRAUJOCH_DATA_ROOT if set, else the last-browsed root, else the home
  directory. Most beamline users care about opening images, not
  reprocessing settings, so it's the tab raised by default. Its filter box
  narrows files only - a directory always passes - because filtering the
  directories too hid the matching files under any directory whose own name
  did not match, and hid the root's ancestors, which left the tree with no
  valid root index until the root was set again by hand.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PogHY7bWXV4bpctDPdyuDN
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-11 18:45:20 +02:00

131 lines
5.3 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 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);
}