Files
Jungfraujoch/viewer/windows/JFJochCalibrationResultWindow.cpp
Filip LeonarskiandClaude Opus 5.5 0be8eb2bca Viewer: dark theme, switchable live from the View menu
View > Theme offers Follow system / Light / Dark, stored in the settings.
Follow system uses the desktop's colour scheme where Qt reports it (6.5+)
and is light otherwise. The dark theme is a deep indigo (#1A1D3A panels,
#12142B entry fields and charts).

The palettes live in the new ViewerTheme module. A switch applies at once:
the application stylesheet is set again after the palette so every widget
is re-polished (with a stylesheet active, polished widgets otherwise keep
their old palette); widgets that bake colours into stylesheets use
SetThemedStyleSheet, which re-applies them on ThemeNotifier::changed;
toolbar icons draw their glyph at paint time through an icon engine, so the
ink follows the theme (and stays sharp at any size); charts re-theme and
rebuild. Hard-coded white backgrounds on entry fields and tables are
removed in favour of the palette's base colour.

The idle detector-status badge in the status bar is now transparent instead
of an empty default-styled progress bar.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 17:51:50 +02:00

107 lines
5.2 KiB
C++

// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#include "JFJochCalibrationResultWindow.h"
#include "../ViewerTheme.h"
#include <QFrame>
#include <QHBoxLayout>
#include <QHeaderView>
#include <QLabel>
#include <QTableWidget>
#include <QVBoxLayout>
#include "../../common/JFJochMath.h" // PI
namespace {
constexpr double RAD_TO_DEG = 180.0 / PI;
// A big-number "card" for the hero row, as in the merge-statistics window.
QWidget *MakeCard(const QString &value, const QString &caption, QWidget *parent) {
auto *card = new QFrame(parent);
card->setFrameShape(QFrame::StyledPanel);
auto *l = new QVBoxLayout(card);
l->setContentsMargins(12, 8, 12, 8);
l->setSpacing(0);
auto *v = new QLabel(value, card);
QFont f = v->font();
f.setPointSizeF(f.pointSizeF() * 2.2);
f.setBold(true);
v->setFont(f);
SetThemedStyleSheet(v, [] { return "color: " + AccentTextColor().name() + ";"; });
v->setAlignment(Qt::AlignCenter);
auto *c = new QLabel(caption, card);
c->setStyleSheet("color: gray;");
c->setAlignment(Qt::AlignCenter);
l->addWidget(v);
l->addWidget(c);
return card;
}
}
JFJochCalibrationResultWindow::JFJochCalibrationResultWindow(const QString &title,
const CalibrationResult &calibration,
const DiffractionGeometry &header,
const QString &poni_path,
QWidget *parent)
: QWidget(parent, Qt::Window) {
setWindowTitle("Detector calibration — " + title);
setAttribute(Qt::WA_DeleteOnClose);
resize(640, 380);
const DiffractionGeometry &g = calibration.geometry;
const double pxl_mm = g.GetPixelSize_mm();
auto *layout = new QVBoxLayout(this);
auto *hero = new QHBoxLayout();
hero->addWidget(MakeCard(QString::number(calibration.rms_radial_pxl, 'f', 2), "Radial rms [px]", this));
hero->addWidget(MakeCard(QString::number(calibration.beam_sigma_pxl, 'f', 3), "Beam σ [px]", this));
hero->addWidget(MakeCard(QString::number(calibration.ring_points), "Ring points", this));
layout->addLayout(hero);
auto *table = new QTableWidget(7, 4, this);
table->setHorizontalHeaderLabels({"Quantity", "Fitted", "From header", "Change"});
table->setEditTriggers(QAbstractItemView::NoEditTriggers);
table->verticalHeader()->setVisible(false);
table->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
// The PONI is the point of normal incidence, which is what the .poni file stores; the direct beam
// is where the beam actually lands, which is what every other program calls the beam centre. They
// part company by distance*tan(rot) as soon as the detector is tilted, so show both.
const auto [beam_x, beam_y] = g.GetDirectBeam_pxl();
const auto [header_beam_x, header_beam_y] = header.GetDirectBeam_pxl();
const struct { QString name; double fitted; double header; int digits; } rows[] = {
{"PONI x [px]", g.GetBeamX_pxl(), header.GetBeamX_pxl(), 3},
{"PONI y [px]", g.GetBeamY_pxl(), header.GetBeamY_pxl(), 3},
{"Rot1 [°]", g.GetPoniRot1_rad() * RAD_TO_DEG, header.GetPoniRot1_rad() * RAD_TO_DEG, 4},
{"Rot2 [°]", g.GetPoniRot2_rad() * RAD_TO_DEG, header.GetPoniRot2_rad() * RAD_TO_DEG, 4},
{"Distance [mm]", g.GetDetectorDistance_mm(), header.GetDetectorDistance_mm(), 4},
{"Direct beam x [px]", beam_x, header_beam_x, 3},
{"Direct beam y [px]", beam_y, header_beam_y, 3},
};
for (int i = 0; i < 7; i++) {
const double change = rows[i].fitted - rows[i].header;
table->setItem(i, 0, new QTableWidgetItem(rows[i].name));
table->setItem(i, 1, new QTableWidgetItem(QString::number(rows[i].fitted, 'f', rows[i].digits)));
table->setItem(i, 2, new QTableWidgetItem(QString::number(rows[i].header, 'f', rows[i].digits)));
table->setItem(i, 3, new QTableWidgetItem(
QStringLiteral("%1%2").arg(change >= 0 ? "+" : "").arg(change, 0, 'f', rows[i].digits)));
}
layout->addWidget(table);
auto *note = new QLabel(this);
note->setWordWrap(true);
note->setStyleSheet("color: gray;");
// In mm these are what the .poni file carries, so they are measured pyFAI's way - from the edge of
// the sensor, which puts the centre of pixel i at (i + 0.5) * pixel size - not from the centre of
// the first pixel as the [px] rows above (docs/DETECTOR_GEOMETRY.md).
note->setText(QStringLiteral("PONI x = %1 mm, PONI y = %2 mm.%3")
.arg((g.GetBeamX_pxl() + 0.5) * pxl_mm, 0, 'f', 4)
.arg((g.GetBeamY_pxl() + 0.5) * pxl_mm, 0, 'f', 4)
.arg(poni_path.isEmpty() ? QString() : " Written to " + poni_path));
layout->addWidget(note);
}