viewer: Phase 1b — inline MX/AzInt settings dock

Surface a surgical subset of processing settings in an always-visible dock
instead of hidden windows:

- New JFJochViewerSettingsDock with an MX / AzInt segmented toggle.
  MX page: geometry (energy, distance, beam X/Y), a new unit-cell +
  space-group editor (no such input existed before; enables known-cell
  ffbidx), spot finding (S/N, photon count, min pixels/spot), indexing
  algorithm and geometry refinement. AzInt page: q range / spacing /
  azimuthal bins plus the existing powder-calibration widget.
- Edits feed straight into the worker (UpdateSpotFindingSettings,
  UpdateAzintSettings, UpdateDataset, FindCenter); fields populate from the
  loaded dataset.
- Add CeO2 and Silicon calibrant presets.
- Dock it left, objectName "settingsDock", show it in the Processing
  perspective (hidden in Image).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-23 16:26:15 +02:00
co-authored by Claude Opus 4.8
parent f5a146d212
commit 58c60e03fe
6 changed files with 378 additions and 1 deletions
+2
View File
@@ -60,6 +60,8 @@ ADD_EXECUTABLE(jfjoch_viewer jfjoch_viewer.cpp JFJochViewerWindow.cpp JFJochView
widgets/SliderPlusBox.h
widgets/JFJochViewerSidePanelChart.cpp
widgets/JFJochViewerSidePanelChart.h
widgets/JFJochViewerSettingsDock.cpp
widgets/JFJochViewerSettingsDock.h
windows/JFJochViewerSpotListWindow.cpp
windows/JFJochViewerSpotListWindow.h
image_viewer/JFJochAzIntImage.cpp
+29 -1
View File
@@ -15,6 +15,7 @@
#include "JFJochImageReadingWorker.h"
#include "image_viewer/JFJochDiffractionImage.h"
#include "JFJochViewerSidePanel.h"
#include "widgets/JFJochViewerSettingsDock.h"
#include "JFJochViewerStatusBar.h"
#include "../common/CUDAWrapper.h"
#include "windows/JFJochViewerImageListWindow.h"
@@ -421,6 +422,32 @@ JFJochViewerWindow::JFJochViewerWindow(QWidget *parent, bool dbus, const QString
lastActiveRunId = std::move(active);
});
// Inline settings dock: the surgical MX/AzInt subset, always visible (left), wired straight
// into the running analysis so edits re-run on the current image / dataset.
auto *settingsPanel = new JFJochViewerSettingsDock(spot_finding_settings, indexing_settings,
experiment.GetAzimuthalIntegrationSettings(), this);
settingsDock = new QDockWidget("Settings", this);
settingsDock->setObjectName("settingsDock");
settingsDock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea);
settingsDock->setWidget(settingsPanel);
addDockWidget(Qt::LeftDockWidgetArea, settingsDock);
menuBar->AddDockEntry(settingsDock, "Settings");
connect(settingsPanel, &JFJochViewerSettingsDock::spotFindingChanged,
reading_worker, &JFJochImageReadingWorker::UpdateSpotFindingSettings);
connect(settingsPanel, &JFJochViewerSettingsDock::azintChanged,
reading_worker, &JFJochImageReadingWorker::UpdateAzintSettings);
connect(settingsPanel, &JFJochViewerSettingsDock::experimentChanged,
reading_worker, &JFJochImageReadingWorker::UpdateDataset);
connect(settingsPanel, &JFJochViewerSettingsDock::findBeamCenter,
reading_worker, &JFJochImageReadingWorker::FindCenter);
connect(reading_worker, &JFJochImageReadingWorker::datasetLoaded,
settingsPanel, &JFJochViewerSettingsDock::datasetLoaded);
connect(reading_worker, &JFJochImageReadingWorker::imageLoaded,
settingsPanel, &JFJochViewerSettingsDock::loadImage);
connect(settingsPanel, &JFJochViewerSettingsDock::ringsFromCalibration,
side_panel, &JFJochViewerSidePanel::SetRings);
// Dock the processing panel in the bottom-right corner, next to the dataset-info plots.
processingDock = new QDockWidget("Processing", this);
processingDock->setObjectName("processingDock");
@@ -464,9 +491,10 @@ JFJochViewerWindow::~JFJochViewerWindow() {
}
void JFJochViewerWindow::ApplyPerspective(Perspective p) {
// Image: just the image + inspector. Processing: also the dataset-info plots and jobs panel.
// Image: just the image + inspector. Processing: also settings, dataset-info plots, jobs panel.
const bool processing = (p == Perspective::Processing);
if (inspectorDock) inspectorDock->setVisible(true);
if (settingsDock) settingsDock->setVisible(processing);
if (processingDock) processingDock->setVisible(processing);
for (auto *d : findChildren<QDockWidget *>())
if (d->objectName().startsWith("datasetInfoDock"))
+1
View File
@@ -40,6 +40,7 @@ private:
QDockWidget *lastDatasetInfoDock = nullptr; // most recent dataset-info dock, for docking layout
QDockWidget *inspectorDock = nullptr; // image inspector (former right-hand side panel)
QDockWidget *settingsDock = nullptr; // inline MX/AzInt settings panel
QDockWidget *processingDock = nullptr; // processing jobs panel
QByteArray defaultLayoutState; // captured after construction, for "Reset layout"
int datasetInfoCounter = 0; // gives each dataset-info dock a unique objectName
+268
View File
@@ -0,0 +1,268 @@
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#include "JFJochViewerSettingsDock.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QFormLayout>
#include <QGroupBox>
#include <QPushButton>
#include <QButtonGroup>
#include <QStackedWidget>
#include <QCheckBox>
#include <QComboBox>
#include <QLabel>
#include <cmath>
#include "SliderPlusBox.h"
#include "NumberLineEdit.h"
#include "PowderCalibrationWidget.h"
#include "TitleLabel.h"
JFJochViewerSettingsDock::JFJochViewerSettingsDock(const SpotFindingSettings &spot,
const IndexingSettings &indexing,
const AzimuthalIntegrationSettings &azint,
QWidget *parent)
: QWidget(parent), spot_(spot), indexing_(indexing), azint_(azint) {
// Segmented MX / AzInt toggle: the two communities pick their page; pages never share a screen.
auto *mxButton = new QPushButton("MX", this);
auto *azButton = new QPushButton("AzInt", this);
for (auto *b : {mxButton, azButton}) {
b->setCheckable(true);
b->setStyleSheet("QPushButton:checked { background-color: #1F3A5F; color: white; }");
}
mxButton->setChecked(true);
auto *group = new QButtonGroup(this);
group->setExclusive(true);
group->addButton(mxButton, 0);
group->addButton(azButton, 1);
auto *toggleRow = new QHBoxLayout();
toggleRow->setSpacing(0);
toggleRow->addWidget(mxButton);
toggleRow->addWidget(azButton);
auto *stack = new QStackedWidget(this);
stack->addWidget(BuildMXPage());
stack->addWidget(BuildAzIntPage());
connect(group, &QButtonGroup::idClicked, stack, &QStackedWidget::setCurrentIndex);
auto *layout = new QVBoxLayout(this);
layout->addLayout(toggleRow);
layout->addWidget(stack);
layout->addStretch();
}
QWidget *JFJochViewerSettingsDock::BuildMXPage() {
auto *page = new QWidget(this);
auto *layout = new QVBoxLayout(page);
layout->setContentsMargins(0, 0, 0, 0);
// --- Geometry ---
layout->addWidget(new TitleLabel("Geometry", page));
auto *geomGroup = new QGroupBox(page);
auto *geom = new QFormLayout(geomGroup);
energy_ = new NumberLineEdit(1.0, 200.0, 12.4, 4, "keV", page);
distance_ = new NumberLineEdit(10.0, 5000.0, 100.0, 2, "mm", page);
beamX_ = new NumberLineEdit(0.0, 20000.0, 0.0, 1, "px", page);
beamY_ = new NumberLineEdit(0.0, 20000.0, 0.0, 1, "px", page);
geom->addRow("Photon energy", energy_);
geom->addRow("Detector distance", distance_);
geom->addRow("Beam center X", beamX_);
geom->addRow("Beam center Y", beamY_);
layout->addWidget(geomGroup);
for (auto *f : {energy_, distance_, beamX_, beamY_})
connect(f, &NumberLineEdit::newValue, this, [this] { EmitExperiment(); });
// --- Unit cell + space group (new: no input existed before) ---
layout->addWidget(new TitleLabel("Unit cell", page));
auto *cellGroup = new QGroupBox(page);
auto *cell = new QFormLayout(cellGroup);
cellKnown_ = new QCheckBox("Known unit cell", page);
cell->addRow("", cellKnown_);
cellA_ = new NumberLineEdit(1.0, 2000.0, 79.0, 3, "Å", page);
cellB_ = new NumberLineEdit(1.0, 2000.0, 79.0, 3, "Å", page);
cellC_ = new NumberLineEdit(1.0, 2000.0, 38.0, 3, "Å", page);
cellAlpha_ = new NumberLineEdit(1.0, 179.0, 90.0, 2, "°", page);
cellBeta_ = new NumberLineEdit(1.0, 179.0, 90.0, 2, "°", page);
cellGamma_ = new NumberLineEdit(1.0, 179.0, 90.0, 2, "°", page);
spaceGroup_ = new NumberLineEdit(0.0, 230.0, 0.0, 0, "", page);
auto *abc = new QHBoxLayout();
abc->addWidget(cellA_); abc->addWidget(cellB_); abc->addWidget(cellC_);
auto *angles = new QHBoxLayout();
angles->addWidget(cellAlpha_); angles->addWidget(cellBeta_); angles->addWidget(cellGamma_);
cell->addRow("a, b, c", abc);
cell->addRow("α, β, γ", angles);
cell->addRow("Space group", spaceGroup_);
layout->addWidget(cellGroup);
auto enableCellFields = [this](bool on) {
for (auto *f : {cellA_, cellB_, cellC_, cellAlpha_, cellBeta_, cellGamma_, spaceGroup_})
f->setEnabled(on);
};
enableCellFields(false);
connect(cellKnown_, &QCheckBox::toggled, this, [this, enableCellFields](bool on) {
enableCellFields(on);
EmitExperiment();
});
for (auto *f : {cellA_, cellB_, cellC_, cellAlpha_, cellBeta_, cellGamma_, spaceGroup_})
connect(f, &NumberLineEdit::newValue, this, [this] { EmitExperiment(); });
// --- Spot finding ---
layout->addWidget(new TitleLabel("Spot finding", page));
auto *spotGroup = new QGroupBox(page);
auto *spot = new QFormLayout(spotGroup);
auto *snr = new SliderPlusBox(1.0, 10.0, 0.1, 1, page);
snr->setValue(spot_.signal_to_noise_threshold);
auto *count = new SliderPlusBox(0.0, 100.0, 1.0, 0, page);
count->setValue(std::lround(spot_.photon_count_threshold));
auto *minPix = new SliderPlusBox(1.0, 20.0, 1.0, 0, page);
minPix->setValue(std::lround(spot_.min_pix_per_spot));
spot->addRow("Signal/noise", snr);
spot->addRow("Photon count", count);
spot->addRow("Min pixels/spot", minPix);
layout->addWidget(spotGroup);
connect(snr, &SliderPlusBox::valueChanged, this, [this](double v) {
spot_.signal_to_noise_threshold = static_cast<float>(v); EmitSpotFinding(); });
connect(count, &SliderPlusBox::valueChanged, this, [this](double v) {
spot_.photon_count_threshold = static_cast<float>(v); EmitSpotFinding(); });
connect(minPix, &SliderPlusBox::valueChanged, this, [this](double v) {
spot_.min_pix_per_spot = std::lround(v); EmitSpotFinding(); });
// --- Indexing ---
layout->addWidget(new TitleLabel("Indexing", page));
auto *idxGroup = new QGroupBox(page);
auto *idx = new QFormLayout(idxGroup);
auto *algo = new QComboBox(page);
algo->addItem("Auto", static_cast<int>(IndexingAlgorithmEnum::Auto));
algo->addItem("FFBIDX (GPU, known cell)", static_cast<int>(IndexingAlgorithmEnum::FFBIDX));
algo->addItem("FFT (GPU, de-novo)", static_cast<int>(IndexingAlgorithmEnum::FFT));
algo->addItem("FFTW (CPU, de-novo)", static_cast<int>(IndexingAlgorithmEnum::FFTW));
algo->addItem("None", static_cast<int>(IndexingAlgorithmEnum::None));
algo->setCurrentIndex(algo->findData(static_cast<int>(indexing_.GetAlgorithm())));
auto *refine = new QComboBox(page);
refine->addItem("None", static_cast<int>(GeomRefinementAlgorithmEnum::None));
refine->addItem("Orientation only", static_cast<int>(GeomRefinementAlgorithmEnum::OrientationOnly));
refine->addItem("Beam center + lattice", static_cast<int>(GeomRefinementAlgorithmEnum::BeamCenter));
refine->addItem("Pixel refinement", static_cast<int>(GeomRefinementAlgorithmEnum::PixelRefine));
refine->setCurrentIndex(refine->findData(static_cast<int>(indexing_.GetGeomRefinementAlgorithm())));
idx->addRow("Algorithm", algo);
idx->addRow("Refinement", refine);
layout->addWidget(idxGroup);
connect(algo, &QComboBox::currentIndexChanged, this, [this, algo] {
indexing_.Algorithm(static_cast<IndexingAlgorithmEnum>(algo->currentData().toInt()));
EmitSpotFinding();
});
connect(refine, &QComboBox::currentIndexChanged, this, [this, refine] {
indexing_.GeomRefinementAlgorithm(static_cast<GeomRefinementAlgorithmEnum>(refine->currentData().toInt()));
EmitSpotFinding();
});
return page;
}
QWidget *JFJochViewerSettingsDock::BuildAzIntPage() {
auto *page = new QWidget(this);
auto *layout = new QVBoxLayout(page);
layout->setContentsMargins(0, 0, 0, 0);
layout->addWidget(new TitleLabel("Azimuthal integration", page));
auto *azGroup = new QGroupBox(page);
auto *az = new QFormLayout(azGroup);
auto *lowQ = new SliderPlusBox(1e-5, 10.0, 0.001, 4, page);
lowQ->setValue(azint_.GetLowQ_recipA());
auto *highQ = new SliderPlusBox(2e-5, 10.0, 0.001, 4, page);
highQ->setValue(azint_.GetHighQ_recipA());
auto *spacing = new SliderPlusBox(1e-5, 1.0, 0.001, 5, page, SliderPlusBox::ScaleType::Logarithmic);
spacing->setValue(azint_.GetQSpacing_recipA());
auto *azimBins = new QComboBox(page);
for (int b : {1, 2, 4, 8, 16, 32, 64, 128})
azimBins->addItem(QString::number(b), b);
azimBins->setCurrentIndex(azimBins->findData(azint_.GetAzimuthalBinCount()));
az->addRow("Low Q [Å⁻¹]", lowQ);
az->addRow("High Q [Å⁻¹]", highQ);
az->addRow("Q spacing [Å⁻¹]", spacing);
az->addRow("Azimuthal bins", azimBins);
layout->addWidget(azGroup);
auto emitAz = [=, this] {
azint_.QRange_recipA(static_cast<float>(lowQ->value()), static_cast<float>(highQ->value()));
azint_.QSpacing_recipA(static_cast<float>(spacing->value()));
azint_.AzimuthalBinCount(azimBins->currentData().toInt());
emit azintChanged(azint_);
};
connect(lowQ, &SliderPlusBox::valueChanged, this, [emitAz] { emitAz(); });
connect(highQ, &SliderPlusBox::valueChanged, this, [emitAz] { emitAz(); });
connect(spacing, &SliderPlusBox::valueChanged, this, [emitAz] { emitAz(); });
connect(azimBins, &QComboBox::currentIndexChanged, this, [emitAz] { emitAz(); });
// Powder calibration (calibrant rings + geometry refinement) - reuse the existing widget.
layout->addWidget(new TitleLabel("Powder calibration", page));
powder_ = new PowderCalibrationWidget(page);
connect(powder_, &PowderCalibrationWidget::findBeamCenter, this, &JFJochViewerSettingsDock::findBeamCenter);
connect(powder_, &PowderCalibrationWidget::ringsFromCalibration, this, &JFJochViewerSettingsDock::ringsFromCalibration);
layout->addWidget(powder_);
return page;
}
void JFJochViewerSettingsDock::EmitSpotFinding() {
emit spotFindingChanged(spot_, indexing_, max_spots_);
}
void JFJochViewerSettingsDock::EmitExperiment() {
if (!have_experiment_)
return;
experiment_.IncidentEnergy_keV(static_cast<float>(energy_->value()));
experiment_.DetectorDistance_mm(static_cast<float>(distance_->value()));
experiment_.BeamX_pxl(static_cast<float>(beamX_->value()));
experiment_.BeamY_pxl(static_cast<float>(beamY_->value()));
if (cellKnown_->isChecked()) {
experiment_.SetUnitCell(UnitCell{
static_cast<float>(cellA_->value()), static_cast<float>(cellB_->value()),
static_cast<float>(cellC_->value()), static_cast<float>(cellAlpha_->value()),
static_cast<float>(cellBeta_->value()), static_cast<float>(cellGamma_->value())});
const int sg = static_cast<int>(std::lround(spaceGroup_->value()));
experiment_.SpaceGroupNumber(sg > 0 ? std::optional<int64_t>(sg) : std::nullopt);
} else {
experiment_.SetUnitCell(std::nullopt);
experiment_.SpaceGroupNumber(std::nullopt);
}
emit experimentChanged(experiment_);
}
void JFJochViewerSettingsDock::RefreshGeometryFields() {
// Populate fields from the loaded experiment. NumberLineEdit::setValue does not emit newValue
// (that fires only on user editing), so this cannot feed back into EmitExperiment.
energy_->setValue(experiment_.GetIncidentEnergy_keV());
distance_->setValue(experiment_.GetDetectorDistance_mm());
beamX_->setValue(experiment_.GetBeamX_pxl());
beamY_->setValue(experiment_.GetBeamY_pxl());
const auto cell = experiment_.GetUnitCell();
QSignalBlocker blockKnown(cellKnown_);
cellKnown_->setChecked(cell.has_value());
for (auto *f : {cellA_, cellB_, cellC_, cellAlpha_, cellBeta_, cellGamma_, spaceGroup_})
f->setEnabled(cell.has_value());
if (cell) {
cellA_->setValue(cell->a); cellB_->setValue(cell->b); cellC_->setValue(cell->c);
cellAlpha_->setValue(cell->alpha); cellBeta_->setValue(cell->beta); cellGamma_->setValue(cell->gamma);
spaceGroup_->setValue(experiment_.GetSpaceGroupNumber().value_or(0));
}
}
void JFJochViewerSettingsDock::datasetLoaded(std::shared_ptr<const JFJochReaderDataset> dataset) {
if (!dataset)
return;
experiment_ = dataset->experiment;
have_experiment_ = true;
RefreshGeometryFields();
}
void JFJochViewerSettingsDock::loadImage(std::shared_ptr<const JFJochReaderImage> image) {
if (powder_)
powder_->loadImage(image);
}
+68
View File
@@ -0,0 +1,68 @@
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include <QWidget>
#include <memory>
#include "../../common/DiffractionExperiment.h"
#include "../../common/IndexingSettings.h"
#include "../../common/AzimuthalIntegrationSettings.h"
#include "../../image_analysis/spot_finding/SpotFindingSettings.h"
#include "../../reader/JFJochReaderDataset.h"
#include "../../reader/JFJochReaderImage.h"
class QStackedWidget;
class QCheckBox;
class QComboBox;
class SliderPlusBox;
class NumberLineEdit;
class PowderCalibrationWidget;
// Compact, always-visible settings panel with an MX / AzInt toggle: the surgical subset of
// processing settings that the two communities reach for most, kept in a dock instead of a
// hidden window. Edits feed straight back into the running analysis via the worker.
class JFJochViewerSettingsDock : public QWidget {
Q_OBJECT
public:
JFJochViewerSettingsDock(const SpotFindingSettings &spot, const IndexingSettings &indexing,
const AzimuthalIntegrationSettings &azint, QWidget *parent = nullptr);
public slots:
void datasetLoaded(std::shared_ptr<const JFJochReaderDataset> dataset);
void loadImage(std::shared_ptr<const JFJochReaderImage> image);
signals:
void spotFindingChanged(const SpotFindingSettings &spot, const IndexingSettings &indexing, int64_t max_spots);
void azintChanged(const AzimuthalIntegrationSettings &settings);
void experimentChanged(const DiffractionExperiment &experiment);
void findBeamCenter(const UnitCell &calibrant, bool guess);
void ringsFromCalibration(QVector<float> rings);
private:
SpotFindingSettings spot_;
IndexingSettings indexing_;
AzimuthalIntegrationSettings azint_;
DiffractionExperiment experiment_;
bool have_experiment_ = false; // geometry edits only take effect once a dataset is loaded
int64_t max_spots_ = 1000;
// MX geometry + crystal fields
NumberLineEdit *energy_ = nullptr;
NumberLineEdit *distance_ = nullptr;
NumberLineEdit *beamX_ = nullptr;
NumberLineEdit *beamY_ = nullptr;
QCheckBox *cellKnown_ = nullptr;
NumberLineEdit *cellA_ = nullptr, *cellB_ = nullptr, *cellC_ = nullptr;
NumberLineEdit *cellAlpha_ = nullptr, *cellBeta_ = nullptr, *cellGamma_ = nullptr;
NumberLineEdit *spaceGroup_ = nullptr;
PowderCalibrationWidget *powder_ = nullptr;
QWidget *BuildMXPage();
QWidget *BuildAzIntPage();
void EmitSpotFinding();
void EmitExperiment();
void RefreshGeometryFields();
};
@@ -52,6 +52,14 @@ UnitCell PowderCalibrationWidget::GetCalibrant() const {
uc = UnitCell(5.1769, 4.7218, 58.380, 89.440, 89.634, 75.854);
break;
case 2:
// CeO2, cubic fluorite (a = 5.4115 Å), NIST SRM 674b.
uc = UnitCell(5.4115, 5.4115, 5.4115, 90, 90, 90);
break;
case 3:
// Silicon, cubic (a = 5.43102 Å), NIST SRM 640.
uc = UnitCell(5.43102, 5.43102, 5.43102, 90, 90, 90);
break;
case 4:
if (sample_cell)
uc = sample_cell.value();
break;
@@ -66,6 +74,8 @@ void PowderCalibrationWidget::updateCalibrantList() {
calibrantCombo->addItem("LaB6");
calibrantCombo->addItem("Silver Behenate");
calibrantCombo->addItem("CeO2");
calibrantCombo->addItem("Silicon");
if (sample_cell)
calibrantCombo->addItem(QString("Current sample (%1 %2 %3 %4 %5 %6)")
.arg(QString::number(sample_cell->a, 'f', 1))