During a collection every dataset tick ran JFJochViewerDatasetInfo::UpdateLabels(), which clear()ed the plot-type combo and re-added every item, so an open dropdown lost its hovered row and scroll position on each tick and picking a plot type was a race against the next update. The same clear-and-refill ran per tick or per image in the calibration window's calibration combo, the ROI list combo and the powder calibrant combo; the last two also threw away the user's choice (the calibrant was reset to the first entry on every image). New LiveComboBox::SetItems() takes the full item list and leaves the combo untouched when the list is unchanged. When it does change while the popup is open, the change is held back and applied (queued) once the popup window hides, so a click that closes the popup lands first. A replacement keeps the selection by item data, blocks signals, and a held-back one emits itemsReplaced() so the owner can re-plot. The calibration window now re-shows the selected calibration on a dataset tick instead of always reloading the pixel mask over it. Tested offscreen: a timer fires datasetLoaded() every 25 ms while the dropdown is open; the popup and hovered row survive, newly available indexing metrics are held back until the popup closes, and the picked type sticks and is what the chart plots. The rc175 code fails the hover checks of the same harness. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
147 lines
7.6 KiB
C++
147 lines
7.6 KiB
C++
// SPDX-FileCopyrightText: 2025 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
#include "PowderCalibrationWidget.h"
|
|
#include <QPushButton>
|
|
#include <QGridLayout>
|
|
#include <QLabel>
|
|
#include "../../common/JFJochMath.h"
|
|
|
|
#include "../image_analysis/geom_refinement/AssignSpotsToRings.h"
|
|
#include "../image_analysis/geom_refinement/Calibrants.h"
|
|
|
|
PowderCalibrationWidget::PowderCalibrationWidget(QWidget *parent) : QWidget(parent) {
|
|
auto layout = new QVBoxLayout(this);
|
|
|
|
calibrantCombo = new LiveComboBox(this);
|
|
// Without this a combo asks to be as wide as its longest entry - and it is never allowed to
|
|
// shrink below that, which is what used to pin the whole settings dock wide as soon as the
|
|
// powder section was expanded. The current text elides instead; the tooltip carries the rest.
|
|
calibrantCombo->setSizeAdjustPolicy(QComboBox::AdjustToMinimumContentsLengthWithIcon);
|
|
calibrantCombo->setMinimumContentsLength(12);
|
|
updateCalibrantList();
|
|
|
|
// How a whole-dataset run measures the rings. The two buttons below always work from the spots of
|
|
// the image on screen, so this only takes effect for "Analyze dataset".
|
|
methodCombo = new QComboBox(this);
|
|
methodCombo->setSizeAdjustPolicy(QComboBox::AdjustToMinimumContentsLengthWithIcon);
|
|
methodCombo->setMinimumContentsLength(12);
|
|
methodCombo->addItem("Rings (summed profile)", static_cast<int>(CalibrationMethod::Rings));
|
|
methodCombo->addItem("Spots (pooled spot lists)", static_cast<int>(CalibrationMethod::Spots));
|
|
methodCombo->setToolTip("How \"Analyze dataset\" measures the powder rings: rings sums the "
|
|
"(q x azimuth) azimuthal profile over every image and fits the ring arcs in it; "
|
|
"spots pools the found spots and fits those.");
|
|
|
|
// The detector tilt is a free parameter of every one of these fits. Unticking it holds rot1/rot2
|
|
// where the geometry already has them - what a calibration handed to a program with no way to
|
|
// express a tilted detector (XDS) needs. It applies to the two buttons and to "Analyze dataset".
|
|
refineTiltCheck = new QCheckBox("Refine detector tilt", this);
|
|
refineTiltCheck->setChecked(true);
|
|
refineTiltCheck->setToolTip("Fit the detector tilt (PONI rot1/rot2) together with the beam centre "
|
|
"and the distance. Untick to hold the tilt where it is and fit only the "
|
|
"beam centre and the distance - for a calibration that will be given to "
|
|
"a program which cannot express a tilted detector, such as XDS.");
|
|
|
|
auto findBeamCenterButton = new QPushButton("Guess detector calibration", this);
|
|
connect(findBeamCenterButton, &QPushButton::clicked,this, &PowderCalibrationWidget::findBeamCenterClicked);
|
|
|
|
auto optimizeBeamCenterButton = new QPushButton("Refine detector calibration", this);
|
|
connect(optimizeBeamCenterButton, &QPushButton::clicked,this, &PowderCalibrationWidget::optimizeBeamCenterClicked);
|
|
|
|
auto calibrantRingsButton = new QPushButton("Display calibrant rings", this);
|
|
connect(calibrantRingsButton, &QPushButton::clicked, this, [this]() {
|
|
QVector<float> d_rings;
|
|
for (float q : GetCalibrantRings())
|
|
d_rings.append(2 * PI / q);
|
|
emit ringsFromCalibration(d_rings);
|
|
});
|
|
|
|
// One button per row rather than two side by side: the panel lives in a narrow left-hand dock,
|
|
// and two buttons abreast cannot be narrower than the sum of their two labels.
|
|
auto refine_row = new QGridLayout();
|
|
refine_row->setSpacing(12);
|
|
refine_row->addWidget(new QLabel("Calibrant:"),0,0);
|
|
refine_row->addWidget(calibrantCombo,0,1);
|
|
|
|
refine_row->addWidget(new QLabel("Dataset method:"),1,0);
|
|
refine_row->addWidget(methodCombo,1,1);
|
|
|
|
refine_row->addWidget(findBeamCenterButton,2, 0, 1, 2);
|
|
refine_row->addWidget(optimizeBeamCenterButton,3, 0, 1, 2);
|
|
refine_row->addWidget(calibrantRingsButton,4, 0, 1, 2);
|
|
refine_row->addWidget(refineTiltCheck,5, 0, 1, 2);
|
|
|
|
auto hint = new QLabel("The two buttons fit the image on screen; \"Analyze dataset\" fits the whole "
|
|
"run and writes a .poni.", this);
|
|
hint->setWordWrap(true);
|
|
hint->setStyleSheet("color: gray;");
|
|
|
|
layout->addLayout(refine_row);
|
|
layout->addWidget(hint);
|
|
setLayout(layout);
|
|
}
|
|
|
|
std::vector<float> PowderCalibrationWidget::GetCalibrantRings() const {
|
|
// The combo lists the shared calibrant table (in its order), then the current sample's own cell,
|
|
// which is not a powder standard but is what a single-crystal image's spots actually sit on.
|
|
const int idx = calibrantCombo->currentIndex();
|
|
const auto &table = Calibrants();
|
|
if (idx >= 0 && idx < static_cast<int>(table.size()))
|
|
return CalibrantRings(table[idx].name);
|
|
if (sample_cell) {
|
|
// With the group where the dataset has one: the absences then come from the symmetry rather
|
|
// than from a bare P-lattice assumption, and a centred cell's list no longer opens with a
|
|
// reflection that is not there - which would scale the whole calibration by the ratio between
|
|
// that ring and the first real one. Same call the CLI makes.
|
|
if (sample_sg)
|
|
return CalculateXtalRings(sample_cell.value(), *sample_sg);
|
|
return CalculateXtalRings(sample_cell.value());
|
|
}
|
|
return CalibrantRings(table.front().name);
|
|
}
|
|
|
|
CalibrationSelection PowderCalibrationWidget::Selection() const {
|
|
return {calibrantCombo->currentText(), GetCalibrantRings(),
|
|
static_cast<CalibrationMethod>(methodCombo->currentData().toInt()),
|
|
refineTiltCheck->isChecked()};
|
|
}
|
|
|
|
void PowderCalibrationWidget::updateCalibrantList() {
|
|
// Called for every image, live ones included: the combo only changes when the list does, and
|
|
// keeps the user's calibrant by name.
|
|
QVector<LiveComboBox::Item> items;
|
|
for (const auto &c : Calibrants())
|
|
items.push_back({QString::fromStdString(c.name), QString::fromStdString(c.name)});
|
|
if (sample_cell) {
|
|
// The cell goes in the tooltip, not in the entry: a 40-character item would set the combo's
|
|
// minimum width and, through it, the minimum width of the dock this widget sits in.
|
|
items.push_back({"Current sample cell", "Current sample cell",
|
|
QString("%1 %2 %3 %4 %5 %6")
|
|
.arg(QString::number(sample_cell->a, 'f', 1))
|
|
.arg(QString::number(sample_cell->b, 'f', 1))
|
|
.arg(QString::number(sample_cell->c, 'f', 1))
|
|
.arg(QString::number(sample_cell->alpha, 'f', 1))
|
|
.arg(QString::number(sample_cell->beta, 'f', 1))
|
|
.arg(QString::number(sample_cell->gamma, 'f', 1))});
|
|
}
|
|
calibrantCombo->SetItems(items);
|
|
}
|
|
|
|
void PowderCalibrationWidget::loadImage(std::shared_ptr<const JFJochReaderImage> image) {
|
|
if (image) {
|
|
sample_cell = image->Dataset().experiment.GetUnitCell();
|
|
sample_sg = image->Dataset().experiment.GetGemmiSpaceGroup();
|
|
}
|
|
updateCalibrantList();
|
|
}
|
|
|
|
void PowderCalibrationWidget::findBeamCenterClicked() {
|
|
const std::vector<float> rings = GetCalibrantRings();
|
|
emit findBeamCenter(QVector<float>(rings.begin(), rings.end()), true, refineTiltCheck->isChecked());
|
|
}
|
|
|
|
void PowderCalibrationWidget::optimizeBeamCenterClicked() {
|
|
const std::vector<float> rings = GetCalibrantRings();
|
|
emit findBeamCenter(QVector<float>(rings.begin(), rings.end()), false, refineTiltCheck->isChecked());
|
|
}
|