From 9aae21808692ac5b9be2eeadeebbc1c68254714e Mon Sep 17 00:00:00 2001 From: Filip Leonarski Date: Mon, 28 Sep 2026 11:52:16 +0200 Subject: [PATCH] Viewer: background slider on demand, Auto zeroes the background, no black plot line - The background (black-point) slider is hidden by default. It is shown from View > Background slider, or as soon as the background becomes non-zero (B + wheel), so an active background is never invisible. The choice is kept in QSettings ("backgroundSlider") and cleared by "Reset all settings". - Auto contrast, continuous or the one-shot A key, sets the background back to zero. - A dataset-info plot without a run colour kept the default QPen: black, and near-invisible on the dark theme. This is every plot of a live HTTP dataset, whose run list is empty. It now takes #1F77B4 on the light theme and #FF7F0E on the dark one (the spots / background colours of the split plot). Checked headlessly: the rendered line pixels are exactly those colours on the #FFFFFF and #12142B plot backgrounds. Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/CHANGELOG.md | 3 +++ docs/JFJOCH_VIEWER.md | 7 ++++--- viewer/JFJochViewerMenu.cpp | 8 ++++++-- viewer/JFJochViewerMenu.h | 4 ++++ viewer/JFJochViewerWindow.cpp | 5 +++++ viewer/charts/JFJochDatasetInfoChartView.cpp | 9 +++++++-- viewer/image_viewer/JFJochDiffractionImage.cpp | 11 +++++++++++ viewer/image_viewer/JFJochDiffractionImage.h | 1 + viewer/toolbar/JFJochViewerToolbarDisplay.cpp | 17 +++++++++++++++-- viewer/toolbar/JFJochViewerToolbarDisplay.h | 8 ++++++++ viewer/windows/JFJochMouseShortcutsWindow.cpp | 4 ++-- 11 files changed, 66 insertions(+), 11 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 87f46fe3e..79d5cb8fc 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -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. diff --git a/docs/JFJOCH_VIEWER.md b/docs/JFJOCH_VIEWER.md index 4cf67404d..2c7b1bc20 100644 --- a/docs/JFJOCH_VIEWER.md +++ b/docs/JFJOCH_VIEWER.md @@ -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 | diff --git a/viewer/JFJochViewerMenu.cpp b/viewer/JFJochViewerMenu.cpp index 56fb8ef62..241d670b8 100644 --- a/viewer/JFJochViewerMenu.cpp +++ b/viewer/JFJochViewerMenu.cpp @@ -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); diff --git a/viewer/JFJochViewerMenu.h b/viewer/JFJochViewerMenu.h index e08e84258..d7fb5f523 100644 --- a/viewer/JFJochViewerMenu.h +++ b/viewer/JFJochViewerMenu.h @@ -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. diff --git a/viewer/JFJochViewerWindow.cpp b/viewer/JFJochViewerWindow.cpp index 816e0157f..7b2cc883a 100644 --- a/viewer/JFJochViewerWindow.cpp +++ b/viewer/JFJochViewerWindow.cpp @@ -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()) 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); } diff --git a/viewer/charts/JFJochDatasetInfoChartView.cpp b/viewer/charts/JFJochDatasetInfoChartView.cpp index 57bd942ba..b19854cec 100644 --- a/viewer/charts/JFJochDatasetInfoChartView.cpp +++ b/viewer/charts/JFJochDatasetInfoChartView.cpp @@ -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 diff --git a/viewer/image_viewer/JFJochDiffractionImage.cpp b/viewer/image_viewer/JFJochDiffractionImage.cpp index 6610cf330..7a00854c1 100644 --- a/viewer/image_viewer/JFJochDiffractionImage.cpp +++ b/viewer/image_viewer/JFJochDiffractionImage.cpp @@ -983,9 +983,19 @@ void JFJochDiffractionImage::loadImage(std::shared_ptr } } +// 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); diff --git a/viewer/image_viewer/JFJochDiffractionImage.h b/viewer/image_viewer/JFJochDiffractionImage.h index ec3459279..4d05280ad 100644 --- a/viewer/image_viewer/JFJochDiffractionImage.h +++ b/viewer/image_viewer/JFJochDiffractionImage.h @@ -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 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; diff --git a/viewer/toolbar/JFJochViewerToolbarDisplay.cpp b/viewer/toolbar/JFJochViewerToolbarDisplay.cpp index 33893b9db..d9c6ffd35 100644 --- a/viewer/toolbar/JFJochViewerToolbarDisplay.cpp +++ b/viewer/toolbar/JFJochViewerToolbarDisplay.cpp @@ -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); diff --git a/viewer/toolbar/JFJochViewerToolbarDisplay.h b/viewer/toolbar/JFJochViewerToolbarDisplay.h index 0606f0ff7..85ee986c4 100644 --- a/viewer/toolbar/JFJochViewerToolbarDisplay.h +++ b/viewer/toolbar/JFJochViewerToolbarDisplay.h @@ -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 image); private slots: diff --git a/viewer/windows/JFJochMouseShortcutsWindow.cpp b/viewer/windows/JFJochMouseShortcutsWindow.cpp index 68b1a058d..4e8386099 100644 --- a/viewer/windows/JFJochMouseShortcutsWindow.cpp +++ b/viewer/windows/JFJochMouseShortcutsWindow.cpp @@ -30,8 +30,8 @@ namespace { {"Shift + wheel", "Move the foreground (upper contrast limit) in linear steps"}, {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 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"},