Files
Jungfraujoch/viewer/widgets/JFJochViewerSettingsDock.cpp
T
leonarski_fandClaude Opus 4.8 e3bd1a3529 viewer: settings fields fill the panel width (form growth policy)
The settings content stopped resizing past ~360 px, leaving dead space when the
dock was widened. Cause: QFormLayout's default field growth policy keeps fields
at their size hint. Set AllNonFixedFieldsGrow on all five forms (geometry, unit
cell, spot finding, indexing, azint) so the fields grow to fill the dock width.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 16:26:15 +02:00

342 lines
16 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 <QPushButton>
#include <QButtonGroup>
#include <QStackedWidget>
#include <QCheckBox>
#include <QComboBox>
#include <QLabel>
#include <cmath>
#include "SliderPlusBox.h"
#include "NumberLineEdit.h"
#include "PowderCalibrationWidget.h"
#include "CollapsibleSection.h"
#include "../../common/JFJochMath.h"
#include "../../gemmi_gph/gemmi/symmetry.hpp"
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);
// Geometry is common to both communities, so it lives above the toggle rather than per page.
auto *layout = new QVBoxLayout(this);
layout->addLayout(toggleRow);
layout->addWidget(BuildGeometrySection());
layout->addWidget(stack);
layout->addStretch();
}
QWidget *JFJochViewerSettingsDock::BuildGeometrySection() {
auto *section = new CollapsibleSection("Geometry", this);
auto *geom = new QFormLayout();
geom->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow); // fields fill the panel width
energy_ = new NumberLineEdit(1.0, 200.0, 12.4, 4, "keV", this);
distance_ = new NumberLineEdit(10.0, 5000.0, 100.0, 2, "mm", this);
beamX_ = new NumberLineEdit(-20000.0, 20000.0, 0.0, 1, "px", this);
beamY_ = new NumberLineEdit(-20000.0, 20000.0, 0.0, 1, "px", this);
rot1_ = new NumberLineEdit(-180.0, 180.0, 0.0, 3, "°", this);
rot2_ = new NumberLineEdit(-180.0, 180.0, 0.0, 3, "°", this);
// The detector origin is the PONI (PyFAI) point; in MX (XDS-style) terms it is the beam origin,
// which coincides with the beam center only when the detector is untilted.
const QString beamTip =
"Beam origin (XDS convention): the PONI point where the un-tilted beam meets the detector. "
"Equals the beam center only when the detector tilt is zero.";
beamX_->setToolTip(beamTip);
beamY_->setToolTip(beamTip);
const QString tiltTip = "Detector tilt about the two in-plane axes (PyFAI PONI rot1 / rot2), in degrees.";
rot1_->setToolTip(tiltTip);
rot2_->setToolTip(tiltTip);
auto *beam = new QHBoxLayout();
beam->addWidget(beamX_);
beam->addWidget(beamY_);
auto *tilt = new QHBoxLayout();
tilt->addWidget(rot1_);
tilt->addWidget(rot2_);
geom->addRow("Photon energy", energy_);
geom->addRow("Detector distance", distance_);
geom->addRow("Beam origin", beam);
geom->addRow("Detector tilt", tilt);
for (auto *f : {energy_, distance_, beamX_, beamY_, rot1_, rot2_})
connect(f, &NumberLineEdit::newValue, this, [this] { EmitExperiment(); });
section->setContentLayout(geom);
return section;
}
QWidget *JFJochViewerSettingsDock::BuildMXPage() {
auto *page = new QWidget(this);
auto *layout = new QVBoxLayout(page);
layout->setContentsMargins(0, 0, 0, 0);
// --- Unit cell + space group (new: no input existed before) ---
auto *cellSection = new CollapsibleSection("Unit cell", page);
auto *cell = new QFormLayout();
cell->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow);
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);
spaceGroup_->setToolTip("Space group number (1230); 0 = unset.");
spaceGroupName_ = new QLabel("—", 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_);
// Number and symbol split the line evenly, like the beam-origin row.
auto *sgRow = new QHBoxLayout();
sgRow->addWidget(spaceGroup_, 1);
sgRow->addWidget(spaceGroupName_, 1);
cell->addRow("a, b, c", abc);
cell->addRow("α, β, γ", angles);
cell->addRow("Space group", sgRow);
cellSection->setContentLayout(cell);
layout->addWidget(cellSection);
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);
UpdateSpaceGroupName();
EmitExperiment();
});
for (auto *f : {cellA_, cellB_, cellC_, cellAlpha_, cellBeta_, cellGamma_})
connect(f, &NumberLineEdit::newValue, this, [this] { EmitExperiment(); });
connect(spaceGroup_, &NumberLineEdit::newValue, this, [this] {
UpdateSpaceGroupName();
EmitExperiment();
});
// --- Spot finding ---
auto *spotSection = new CollapsibleSection("Spot finding", page);
auto *spot = new QFormLayout();
spot->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow);
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);
spotSection->setContentLayout(spot);
layout->addWidget(spotSection);
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 ---
auto *idxSection = new CollapsibleSection("Indexing", page);
auto *idx = new QFormLayout();
idx->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow);
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);
idxSection->setContentLayout(idx);
layout->addWidget(idxSection);
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();
});
layout->addStretch(); // anchor sections to the top so expanding an accordion grows downward
return page;
}
QWidget *JFJochViewerSettingsDock::BuildAzIntPage() {
auto *page = new QWidget(this);
auto *layout = new QVBoxLayout(page);
layout->setContentsMargins(0, 0, 0, 0);
auto *azSection = new CollapsibleSection("Azimuthal integration", page);
auto *az = new QFormLayout();
az->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow);
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);
azSection->setContentLayout(az);
layout->addWidget(azSection);
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.
auto *powderSection = new CollapsibleSection("Powder calibration", page);
auto *powderLayout = new QVBoxLayout();
powderLayout->setContentsMargins(0, 0, 0, 0);
powder_ = new PowderCalibrationWidget(page);
connect(powder_, &PowderCalibrationWidget::findBeamCenter, this, &JFJochViewerSettingsDock::findBeamCenter);
connect(powder_, &PowderCalibrationWidget::ringsFromCalibration, this, &JFJochViewerSettingsDock::ringsFromCalibration);
powderLayout->addWidget(powder_);
powderSection->setContentLayout(powderLayout);
layout->addWidget(powderSection);
layout->addStretch(); // anchor sections to the top so expanding an accordion grows downward
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()));
experiment_.PoniRot1_rad(static_cast<float>(rot1_->value() * PI / 180.0));
experiment_.PoniRot2_rad(static_cast<float>(rot2_->value() * PI / 180.0));
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());
rot1_->setValue(experiment_.GetPoniRot1_rad() * 180.0 / PI);
rot2_->setValue(experiment_.GetPoniRot2_rad() * 180.0 / PI);
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));
}
UpdateSpaceGroupName();
}
void JFJochViewerSettingsDock::UpdateSpaceGroupName() {
if (!cellKnown_->isChecked()) {
spaceGroupName_->setText("—");
return;
}
const int n = static_cast<int>(std::lround(spaceGroup_->value()));
if (n >= 1 && n <= 230) {
try {
const auto &sg = gemmi::get_spacegroup_by_number(n); // HermannMauguin short symbol
spaceGroupName_->setText(QString::fromStdString(sg.short_name()));
} catch (...) {
spaceGroupName_->setText("invalid");
}
} else {
spaceGroupName_->setText(n == 0 ? "—" : "invalid");
}
}
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);
}