9f9e13a4e4
- Show the PSI logo in the menu-bar corner, picking one of four interchangeable dot designs at random each launch. Embedded as PNGs (rasterised from the SVG sources, kept alongside) so no Qt SVG module dependency is needed. - Settings dock: put beam center X and Y on a single "Beam center" row to save vertical space on laptop screens. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
271 lines
12 KiB
C++
271 lines
12 KiB
C++
// 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_);
|
||
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<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);
|
||
}
|