// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute // SPDX-License-Identifier: GPL-3.0-only #include "JFJochViewerSettingsDock.h" #include #include #include #include #include #include #include #include #include #include #include #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_); auto *beam = new QHBoxLayout(); beam->addWidget(beamX_); beam->addWidget(beamY_); geom->addRow("Beam center", beam); 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(v); EmitSpotFinding(); }); connect(count, &SliderPlusBox::valueChanged, this, [this](double v) { spot_.photon_count_threshold = static_cast(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(IndexingAlgorithmEnum::Auto)); algo->addItem("FFBIDX (GPU, known cell)", static_cast(IndexingAlgorithmEnum::FFBIDX)); algo->addItem("FFT (GPU, de-novo)", static_cast(IndexingAlgorithmEnum::FFT)); algo->addItem("FFTW (CPU, de-novo)", static_cast(IndexingAlgorithmEnum::FFTW)); algo->addItem("None", static_cast(IndexingAlgorithmEnum::None)); algo->setCurrentIndex(algo->findData(static_cast(indexing_.GetAlgorithm()))); auto *refine = new QComboBox(page); refine->addItem("None", static_cast(GeomRefinementAlgorithmEnum::None)); refine->addItem("Orientation only", static_cast(GeomRefinementAlgorithmEnum::OrientationOnly)); refine->addItem("Beam center + lattice", static_cast(GeomRefinementAlgorithmEnum::BeamCenter)); refine->addItem("Pixel refinement", static_cast(GeomRefinementAlgorithmEnum::PixelRefine)); refine->setCurrentIndex(refine->findData(static_cast(indexing_.GetGeomRefinementAlgorithm()))); idx->addRow("Algorithm", algo); idx->addRow("Refinement", refine); layout->addWidget(idxGroup); connect(algo, &QComboBox::currentIndexChanged, this, [this, algo] { indexing_.Algorithm(static_cast(algo->currentData().toInt())); EmitSpotFinding(); }); connect(refine, &QComboBox::currentIndexChanged, this, [this, refine] { indexing_.GeomRefinementAlgorithm(static_cast(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(lowQ->value()), static_cast(highQ->value())); azint_.QSpacing_recipA(static_cast(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(energy_->value())); experiment_.DetectorDistance_mm(static_cast(distance_->value())); experiment_.BeamX_pxl(static_cast(beamX_->value())); experiment_.BeamY_pxl(static_cast(beamY_->value())); if (cellKnown_->isChecked()) { experiment_.SetUnitCell(UnitCell{ static_cast(cellA_->value()), static_cast(cellB_->value()), static_cast(cellC_->value()), static_cast(cellAlpha_->value()), static_cast(cellBeta_->value()), static_cast(cellGamma_->value())}); const int sg = static_cast(std::lround(spaceGroup_->value())); experiment_.SpaceGroupNumber(sg > 0 ? std::optional(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 dataset) { if (!dataset) return; experiment_ = dataset->experiment; have_experiment_ = true; RefreshGeometryFields(); } void JFJochViewerSettingsDock::loadImage(std::shared_ptr image) { if (powder_) powder_->loadImage(image); }