Files
Jungfraujoch/viewer/widgets/JFJochViewerSettingsDock.cpp
T
leonarski_f 9f9e13a4e4 viewer: PSI logo + tidy beam-center row
- 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>
2026-06-23 16:26:15 +02:00

271 lines
12 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 <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);
}