Merge branch 'viewer-bkg-slider' into rc173 (viewer: background slider on demand, Auto zeroes the background, no black plot line)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-28 11:52:28 +02:00
co-authored by Claude Opus 5.5
11 changed files with 66 additions and 11 deletions
+3
View File
@@ -7,6 +7,9 @@
* jfjoch_viewer's reference dataset accepts a structure-factor mmCIF as well as an MTZ, and a processing job can name an atomic model to validate the merged data against, as `rugnux --model` does.
* jfjoch_viewer keeps a separate preferred dataset-info plot for grid scans, where "Spots + background" means the spot count.
* jfjoch_viewer's dark theme no longer leaves navy buttons, red warnings and chart guide lines at their light-theme colours.
* jfjoch_viewer hides the background slider unless it is switched on in the View menu or the background is raised with `B` + wheel, and remembers the choice.
* jfjoch_viewer's Auto contrast, continuous or one-shot, sets the background back to zero.
* jfjoch_viewer draws a dataset-info plot without a run colour, such as a live broker dataset, in blue on the light theme and orange on the dark theme instead of black.
* `rugnux --model` draws its null's random placements away from every orientation equivalent to the model's under the space group or a twin law, so a random placement can no longer be refined onto the model's own solution and make the crystal's own model read as not fitting.
* Rugnux adds beam-stop holder arms that let part of the beam through to the beam-stop mask, without changing the mask of a sweep that has none.
* Rugnux's `--model` rigid-body refinement computes the model's density and solvent mask on all threads, with results unchanged.
+4 -3
View File
@@ -98,8 +98,9 @@ open button do the same, and **File ▸ Open HTTP** connects to a running `jfjoc
![The general view: diffraction image, inspector and dataset-info plot](images/viewer_general.png)
The top toolbars step through the images (slider, first/previous/next/last, `Jump` and `Sum` for
summing consecutive frames) and set the display (foreground and background limits, `Auto`
contrast, HDR, colour map, font size, theme). The image in the middle zooms with the wheel and
summing consecutive frames) and set the display (foreground limit, `Auto` contrast, HDR, colour
map, font size, theme). The background limit has a slider of its own, shown from View >
Background slider or as soon as `B` + wheel raises it; `Auto` sets it back to zero. The image in the middle zooms with the wheel and
pans by dragging; hovering shows the pixel position, its value and the resolution on the status
bar. The **Inspector** on the right lists the dataset's metadata and, once an image has been
analysed, its spot count, background, indexing result and resolution estimate; the **Magnifier**
@@ -187,7 +188,7 @@ keyboard without them `Home` / `End` are `Fn`+`←` / `Fn`+`→` and `Page Up` /
| `Ctrl` + wheel | Move the foreground in multiplicative steps (×1.15 per notch) |
| `F` held + wheel | Same as `Shift` + wheel, for as long as `F` is held |
| `B` held + wheel | Move the background (lower contrast limit) in linear steps, for as long as `B` is held |
| `A` | Apply auto-contrast once; press it again to switch on continuous Auto |
| `A` | Apply auto-contrast once (background back to zero); press it again to switch on continuous Auto |
| `Home` / `End` | Jump to the first / last image in the dataset |
| `Page Up` / `Page Down` | Step one image forward / back |
| Hover | Status bar shows the pixel position, its value and the resolution |
+6 -2
View File
@@ -74,7 +74,7 @@ JFJochViewerMenu::JFJochViewerMenu(QWidget *parent) : QMenuBar(parent) {
const QAction *dockCalibration = dockMenu->addAction("New dataset info plot");
connect(dockCalibration, &QAction::triggered, [this] { emit openDatasetInfo();});
QMenu *viewMenu = addMenu("View");
viewMenu = addMenu("View");
const QAction *imageLayout = viewMenu->addAction("Image layout");
connect(imageLayout, &QAction::triggered, this, &JFJochViewerMenu::imageLayoutSelected);
const QAction *processingLayout = viewMenu->addAction("Processing layout");
@@ -91,7 +91,7 @@ JFJochViewerMenu::JFJochViewerMenu(QWidget *parent) : QMenuBar(parent) {
remoteMode->setChecked(RemoteDisplayMode());
connect(remoteMode, &QAction::toggled, this, [](bool on) { SetRemoteDisplayMode(on); });
viewMenu->addSeparator();
viewTogglesAnchor = viewMenu->addSeparator();
QMenu *themeMenu = viewMenu->addMenu("Theme");
auto *themeGroup = new QActionGroup(this);
const ThemeChoice currentTheme = LoadThemeChoice();
@@ -180,6 +180,10 @@ void JFJochViewerMenu::AddWindowEntry(JFJochHelperWindow *window, const QString
connect(window, &JFJochHelperWindow::closing, [action] {action->setChecked(false);});
}
void JFJochViewerMenu::AddViewToggle(QAction *action) {
viewMenu->insertAction(viewTogglesAnchor, action);
}
void JFJochViewerMenu::AddDockEntry(QDockWidget *dock, const QString &name) {
auto *action = dock->toggleViewAction(); // checkable, stays in sync with the dock visibility
action->setText(name);
+4
View File
@@ -24,6 +24,8 @@ class JFJochViewerMenu : public QMenuBar {
QAction *toggleCalibrationWindowAction = nullptr;
QMenu *windowMenu = nullptr;
QMenu *viewMenu = nullptr;
QAction *viewTogglesAnchor = nullptr; // View-menu toggles owned elsewhere go before this
QActionGroup *fontZoomGroup = nullptr;
QFont baseFont; // the font the desktop handed us; the zoom is a multiple of it
int fontZoom = 100; // per cent: 100, 125 or 150
@@ -37,6 +39,8 @@ public:
void AddWindowEntry(JFJochHelperWindow *window, const QString &name);
// Adds a dock's show/hide toggle to the Window menu (for docked panels).
void AddDockEntry(QDockWidget *dock, const QString &name);
// Adds a checkable action owned by another widget (e.g. a toolbar) to the View menu.
void AddViewToggle(QAction *action);
// Back to 100 %, for "Reset all settings to defaults".
void ResetFontZoom();
// 100, 125 or 150 per cent; also reachable from the display toolbar's size buttons.
+5
View File
@@ -680,6 +680,9 @@ JFJochViewerWindow::JFJochViewerWindow(QWidget *parent, bool dbus, const QString
viewer->setHDRMode(hdrMode);
toolBarDisplay->updateHDRMode(hdrMode);
menuBar->AddViewToggle(toolBarDisplay->backgroundSliderAction());
toolBarDisplay->showBackgroundSlider(settings.value("backgroundSlider", false).toBool());
if (!file.isEmpty())
LoadFile(file, 0, 1, false);
}
@@ -731,6 +734,7 @@ void JFJochViewerWindow::ResetAllSettings() {
viewer->setAutoForeground(false); // emits autoForegroundChanged -> toolbar button state
viewer->setHDRMode(false);
toolBarDisplay->updateHDRMode(false);
toolBarDisplay->showBackgroundSlider(false);
for (auto *info : findChildren<JFJochViewerDatasetInfo *>())
info->resetPlotDefaults();
@@ -800,6 +804,7 @@ void JFJochViewerWindow::closeEvent(QCloseEvent *event) {
settings.setValue("colorMap", toolBarDisplay->colorMap());
settings.setValue("autoForeground", toolBarDisplay->autoForeground());
settings.setValue("hdrMode", toolBarDisplay->hdrMode());
settings.setValue("backgroundSlider", toolBarDisplay->backgroundSliderShown());
QMainWindow::closeEvent(event);
}
+7 -2
View File
@@ -23,6 +23,10 @@ namespace {
pen.setWidthF(font_height / 5.0);
line->setPen(pen);
}
QColor DefaultLineColor() {
return DarkTheme() ? QColor(0xff, 0x7f, 0x0e) : QColor(0x1f, 0x77, 0xb4);
}
}
JFJochDatasetInfoChartView::JFJochDatasetInfoChartView(QWidget *parent)
@@ -403,8 +407,9 @@ void JFJochDatasetInfoChartView::buildTimeDomainChart() {
series = new QLineSeries(this);
if (!primaryName_.isEmpty())
series->setName(primaryName_);
if (primary_color_.isValid())
series->setColor(primary_color_);
// Without a run colour (a live HTTP dataset has no run list) the line would keep the
// default black pen; it takes the spot-count blue on light, the background orange on dark.
series->setColor(primary_color_.isValid() ? primary_color_ : DefaultLineColor());
setSeriesPenWidth(series, fontMetrics().height());
currentSeries = new QScatterSeries(this);
currentSeries->setColor(palette().color(QPalette::WindowText)); // "current image" marker: not a run colour
@@ -983,9 +983,19 @@ void JFJochDiffractionImage::loadImage(std::shared_ptr<const JFJochReaderImage>
}
}
// Auto contrast sets the whole display range, so it takes the black point back to zero.
void JFJochDiffractionImage::ResetBackground() {
if (background == 0.0f)
return;
background = 0.0f;
emit backgroundChanged(background);
}
void JFJochDiffractionImage::setAutoForeground(bool input) {
auto_fg = input;
one_shot_auto_ = false; // whatever `A` did before, the next press starts as a one-shot again
if (auto_fg)
ResetBackground();
// If auto_foreground is not set, then view stays with the current settings till these are explicitly changed
UpdateForeground();
RenderImage();
@@ -1007,6 +1017,7 @@ void JFJochDiffractionImage::oneShotAutoForeground() {
return;
// Unlike a manual foreground change this leaves auto_fg alone (it is off here either way).
foreground = *val;
ResetBackground();
one_shot_auto_ = true;
ScheduleRenderImage();
emit foregroundChanged(foreground);
@@ -103,6 +103,7 @@ private:
// The foreground the current image suggests: the top of the valid range in HDR mode, the
// auto-contrast value otherwise. Empty when there is no image to take it from.
[[nodiscard]] std::optional<float> AutoForegroundValue() const;
void ResetBackground();
bool one_shot_auto_ = false; // `A` applied the auto value; pressing it again makes Auto permanent
void leaveEvent(QEvent *event) override;
+15 -2
View File
@@ -38,7 +38,7 @@ JFJochViewerToolbarDisplay::JFJochViewerToolbarDisplay(QWidget *parent)
foreground_slider->setMaximumWidth(slider_cap);
addWidget(foreground_slider);
addWidget(new QLabel(" Background ", this));
background_label_action = addWidget(new QLabel(" Background ", this));
background_slider = new SliderPlusBox(0, MAX_SLIDER_NO_IMAGE, 1.0, 1, this,
SliderPlusBox::ScaleType::Logarithmic);
@@ -47,7 +47,12 @@ JFJochViewerToolbarDisplay::JFJochViewerToolbarDisplay(QWidget *parent)
" the mouse wheel");
SetThemedStyleSheet(background_slider, SliderStyle);
background_slider->setMaximumWidth(slider_cap);
addWidget(background_slider);
background_slider_action = addWidget(background_slider);
show_background_action = new QAction("Background slider", this);
show_background_action->setCheckable(true);
connect(show_background_action, &QAction::toggled, this, &JFJochViewerToolbarDisplay::showBackgroundSlider);
showBackgroundSlider(false);
auto makeToggle = [this](const QString &text, const QString &tip) {
auto *b = new QToolButton(this);
@@ -137,6 +142,8 @@ void JFJochViewerToolbarDisplay::backgroundSet(double val) {
}
void JFJochViewerToolbarDisplay::updateBackground(float val) {
if (val > 0.0f)
showBackgroundSlider(true);
pending_background_ = val;
if (RemoteDisplayMode() && background_rate_.isValid()
&& background_rate_.elapsed() < kRemoteRepaintIntervalMs) {
@@ -147,6 +154,12 @@ void JFJochViewerToolbarDisplay::updateBackground(float val) {
ApplyPendingBackground();
}
void JFJochViewerToolbarDisplay::showBackgroundSlider(bool val) {
show_background_action->setChecked(val);
background_label_action->setVisible(val);
background_slider_action->setVisible(val);
}
void JFJochViewerToolbarDisplay::ApplyPendingBackground() {
background_rate_.restart();
QSignalBlocker blocker(background_slider);
@@ -22,6 +22,11 @@ class JFJochViewerToolbarDisplay : public QToolBar {
SliderPlusBox *foreground_slider;
SliderPlusBox *background_slider;
// The background slider is rarely needed, so it is hidden unless this toggle (View menu) is on.
// A non-zero black point switches it on, so an active background is never invisible.
QAction *background_label_action;
QAction *background_slider_action;
QAction *show_background_action;
QToolButton *auto_foreground_button;
QToolButton *hdr_mode_button;
QComboBox *color_map_select;
@@ -48,6 +53,8 @@ public:
[[nodiscard]] int colorMap() const { return color_map_select->currentIndex(); }
[[nodiscard]] bool autoForeground() const { return auto_foreground_button->isChecked(); }
[[nodiscard]] bool hdrMode() const { return hdr_mode_button->isChecked(); }
[[nodiscard]] bool backgroundSliderShown() const { return show_background_action->isChecked(); }
[[nodiscard]] QAction *backgroundSliderAction() const { return show_background_action; }
signals:
void setForeground(float val);
@@ -63,6 +70,7 @@ public slots:
void updateAutoForeground(bool val);
void updateColorMap(int val);
void updateHDRMode(bool val);
void showBackgroundSlider(bool val);
void updateFontZoom(int percent);
void imageLoaded(std::shared_ptr<const JFJochReaderImage> image);
private slots:
@@ -30,8 +30,8 @@ namespace {
{"Shift + wheel", "Move the foreground (upper contrast limit) in linear steps"},
{ctrl + " + wheel", "Move the foreground in multiplicative steps (&times;1.15 per notch)"},
{"F held + wheel", "Same as Shift + wheel, for as long as F is held"},
{"B held + wheel", "Move the background (lower contrast limit) in linear steps, for as long as B is held"},
{"A", "Apply auto-contrast once; press again to switch on continuous Auto"},
{"B held + wheel", "Move the background (lower contrast limit) in linear steps, for as long as B is held; shows the background slider"},
{"A", "Apply auto-contrast once (background back to zero); press again to switch on continuous Auto"},
{home_end, "Jump to the first / last image in the dataset"},
{page_up_down, "Step one image forward / back"},
{"Hover", "Status bar shows the pixel position, its value and the resolution"},