Build Packages / Unit tests (push) Successful in 1h11m40s
Build Packages / build:windows:nocuda (push) Successful in 17m39s
Build Packages / build:windows:cuda (push) Successful in 20m10s
Build Packages / build:viewer-tgz:cpu (push) Successful in 10m2s
Build Packages / build:viewer-tgz:cuda (push) Successful in 11m28s
Build Packages / build:rugnux-tgz (x86_64) (push) Successful in 9m4s
Build Packages / build:rugnux:windows (push) Successful in 11m20s
Build Packages / build:rugnux:aarch64 (cross) (push) Successful in 7m46s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 16m15s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 16m21s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 18m1s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 19m19s
Build Packages / build:rpm (rocky8) (push) Successful in 18m17s
Build Packages / build:rpm (rocky9) (push) Successful in 18m48s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 15m59s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 16m19s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 25m2s
Build Packages / Generate python client (push) Successful in 1m3s
Build Packages / Build documentation (push) Successful in 1m28s
Build Packages / Create release (push) Successful in 18s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 10m50s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 11m15s
The magnifier moves from a helper window into its own dock below the inspector. It has three fixed zoom levels (x64 and x32 with the pixel values written on the pixels, x10 without) instead of wheel zoom, and follows the cursor only while Shift is held over the diffraction image. Meanwhile the diffraction view draws a frame around the area it covers, just outside that area so it stays visible at low zoom without hiding the pixels; releasing Shift (seen by the application-wide key filter, so wherever the focus is), moving without it or leaving the image hides it. A "Pop out" button moves the magnifier into a separate window and back. Qt's own dock floating stays disabled: a floated dock is placed off-screen on WSLg. Whether it was popped out, and the window geometry, persist across sessions. The inspector's image statistics become a collapsible section so a small screen can give the space to the magnifier. kLayoutVersion is bumped for the new dock. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
249 lines
11 KiB
C++
249 lines
11 KiB
C++
// SPDX-FileCopyrightText: 2025 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
#include <QHBoxLayout>
|
|
#include <QPushButton>
|
|
#include <QColorDialog>
|
|
#include <QGridLayout>
|
|
|
|
#include "JFJochViewerSidePanel.h"
|
|
|
|
#include "widgets/CollapsibleSection.h"
|
|
#include "widgets/JFJochViewerImageStatistics.h"
|
|
|
|
namespace {
|
|
// A small square filled with the given colour, used as a legend/key swatch.
|
|
QLabel *MakeColorSwatch(QWidget *parent, const QColor &color) {
|
|
auto *swatch = new QLabel(parent);
|
|
swatch->setFixedSize(13, 13);
|
|
swatch->setStyleSheet(QString("background-color: %1; border: 1px solid #666;").arg(color.name()));
|
|
return swatch;
|
|
}
|
|
}
|
|
|
|
JFJochViewerSidePanel::JFJochViewerSidePanel(QWidget *parent) : QWidget(parent) {
|
|
|
|
auto layout = new QVBoxLayout(this);
|
|
|
|
// Open at start, but foldable, so a small screen can give its space to the magnifier below
|
|
auto *statsSection = new CollapsibleSection("Image statistics", this);
|
|
auto *statsLayout = new QVBoxLayout();
|
|
statsLayout->setContentsMargins(0, 0, 0, 0);
|
|
auto stats = new JFJochViewerImageStatistics(this);
|
|
statsLayout->addWidget(stats);
|
|
statsSection->setContentLayout(statsLayout);
|
|
statsSection->setExpanded(true);
|
|
layout->addWidget(statsSection);
|
|
connect(this, &JFJochViewerSidePanel::imageLoaded, stats, &JFJochViewerImageStatistics::loadImage);
|
|
|
|
auto *featuresSection = new CollapsibleSection("Image features", this);
|
|
|
|
auto spotToggleCheckBox = new QCheckBox("Show spots", this);
|
|
spotToggleCheckBox->setCheckState(Qt::CheckState::Unchecked);
|
|
|
|
connect(spotToggleCheckBox, &QCheckBox::toggled, this, &JFJochViewerSidePanel::spotsToggled);
|
|
|
|
auto highlightIceRingToggleCheckBox = new QCheckBox("Highlight spots on ice rings", this);
|
|
highlightIceRingToggleCheckBox->setCheckState(Qt::CheckState::Checked);
|
|
connect(highlightIceRingToggleCheckBox, &QCheckBox::toggled, this,&JFJochViewerSidePanel::highlightIceRingsToggled);
|
|
|
|
auto hideUnindexedCheckBox = new QCheckBox("Hide non-indexed spots", this);
|
|
hideUnindexedCheckBox->setToolTip("Draw only the spots that were assigned to a lattice.");
|
|
connect(hideUnindexedCheckBox, &QCheckBox::toggled, this, &JFJochViewerSidePanel::hideUnindexedSpotsToggled);
|
|
|
|
auto hideIceRingSpotsCheckBox = new QCheckBox("Hide spots on ice rings", this);
|
|
hideIceRingSpotsCheckBox->setToolTip("Leave the spots that fall on an ice ring out of the overlay.");
|
|
connect(hideIceRingSpotsCheckBox, &QCheckBox::toggled, this, &JFJochViewerSidePanel::hideIceRingSpotsToggled);
|
|
|
|
auto outlineSpotsCheckBox = new QCheckBox("Outline spots in black", this);
|
|
outlineSpotsCheckBox->setCheckState(Qt::CheckState::Unchecked);
|
|
outlineSpotsCheckBox->setToolTip("Draw a black casing under each spot marker, so it stays visible "
|
|
"where the colour map is light.");
|
|
connect(outlineSpotsCheckBox, &QCheckBox::toggled, this, &JFJochViewerSidePanel::outlineSpots);
|
|
|
|
auto predictionsToggleCheckBox = new QCheckBox("Show predictions", this);
|
|
predictionsToggleCheckBox->setCheckState(Qt::CheckState::Unchecked);
|
|
|
|
connect(predictionsToggleCheckBox, &QCheckBox::toggled, this,
|
|
&JFJochViewerSidePanel::predictionsToggled);
|
|
|
|
auto highestPixelsComboBox = new QComboBox(this);
|
|
highestPixelsComboBox->addItem("Show 0 highest pixels", 0);
|
|
highestPixelsComboBox->addItem("Show 1 highest pixel", 1);
|
|
highestPixelsComboBox->addItem("Show 2 highest pixels", 2);
|
|
highestPixelsComboBox->addItem("Show 3 highest pixels", 3);
|
|
highestPixelsComboBox->addItem("Show 5 highest pixels", 5);
|
|
highestPixelsComboBox->addItem("Show 10 highest pixels", 10);
|
|
highestPixelsComboBox->setCurrentIndex(0);
|
|
|
|
connect(highestPixelsComboBox, &QComboBox::currentIndexChanged, this, [this, highestPixelsComboBox](int index) {
|
|
int value = highestPixelsComboBox->itemData(index).toInt();
|
|
emit showHighestPixels(value);
|
|
});
|
|
|
|
auto saturatedPixelsCheckBox = new QCheckBox("Show saturated pixels", this);
|
|
|
|
connect(saturatedPixelsCheckBox, &QCheckBox::toggled, this, &JFJochViewerSidePanel::saturatedPixelsToggled);
|
|
|
|
auto beamStopCheckBox = new QCheckBox("Show beam stop", this);
|
|
beamStopCheckBox->setChecked(true);
|
|
beamStopCheckBox->setToolTip("Draw the detected beam-stop shadow (pixel mask bit 9) in coral. "
|
|
"Unchecked, it is drawn like any other masked pixel.");
|
|
connect(beamStopCheckBox, &QCheckBox::toggled, this, &JFJochViewerSidePanel::showBeamStop);
|
|
|
|
auto colorSelectButton = new QPushButton("Select feature color", this);
|
|
|
|
connect(colorSelectButton, &QPushButton::clicked, this, [this]() {
|
|
QColor color = QColorDialog::getColor(Qt::magenta, this, "Select Feature Color");
|
|
if (color.isValid()) {
|
|
emit setFeatureColor(color);
|
|
if (legend_indexed_swatch_)
|
|
legend_indexed_swatch_->setStyleSheet(
|
|
QString("background-color: %1; border: 1px solid #666;").arg(color.name()));
|
|
}
|
|
});
|
|
|
|
auto spotColorSelectButton = new QPushButton("Select spot color", this);
|
|
|
|
connect(spotColorSelectButton, &QPushButton::clicked, this, [this]() {
|
|
QColor color = QColorDialog::getColor(Qt::green, this, "Select Spot Color");
|
|
if (color.isValid()) {
|
|
emit setSpotColor(color);
|
|
if (legend_spot_swatch_)
|
|
legend_spot_swatch_->setStyleSheet(
|
|
QString("background-color: %1; border: 1px solid #666;").arg(color.name()));
|
|
}
|
|
});
|
|
|
|
auto image_feature_grid = new QGridLayout();
|
|
image_feature_grid->addWidget(spotToggleCheckBox, 0, 0);
|
|
image_feature_grid->addWidget(highlightIceRingToggleCheckBox, 0, 1);
|
|
image_feature_grid->addWidget(hideUnindexedCheckBox, 1, 0);
|
|
image_feature_grid->addWidget(hideIceRingSpotsCheckBox, 1, 1);
|
|
image_feature_grid->addWidget(predictionsToggleCheckBox, 2, 0);
|
|
image_feature_grid->addWidget(beamStopCheckBox, 2, 1);
|
|
|
|
image_feature_grid->addWidget(saturatedPixelsCheckBox, 3, 0);
|
|
image_feature_grid->addWidget(outlineSpotsCheckBox, 3, 1);
|
|
image_feature_grid->addWidget(highestPixelsComboBox, 4, 0);
|
|
image_feature_grid->addWidget(colorSelectButton, 5, 0);
|
|
image_feature_grid->addWidget(spotColorSelectButton, 5, 1);
|
|
|
|
// Legend: what each overlay colour means (matches JFJochDiffractionImage draw colours).
|
|
legend_spot_swatch_ = MakeColorSwatch(this, Qt::green);
|
|
legend_indexed_swatch_ = MakeColorSwatch(this, Qt::magenta);
|
|
auto *legendLayout = new QGridLayout();
|
|
legendLayout->setContentsMargins(0, 4, 0, 0);
|
|
legendLayout->setHorizontalSpacing(6);
|
|
legendLayout->setVerticalSpacing(2);
|
|
struct LegendRow { QLabel *swatch; const char *text; };
|
|
const LegendRow legend_rows[] = {
|
|
{legend_spot_swatch_, "Spots (not indexed)"},
|
|
{legend_indexed_swatch_, "Indexed spots"},
|
|
{MakeColorSwatch(this, Qt::cyan), "Spots on ice ring"},
|
|
{MakeColorSwatch(this, QColor(0xFA, 0x72, 0x68)), "Secondary lattice"},
|
|
{MakeColorSwatch(this, Qt::darkRed), "Predictions"},
|
|
};
|
|
int legend_row = 0;
|
|
for (const auto &r : legend_rows) {
|
|
legendLayout->addWidget(r.swatch, legend_row, 0, Qt::AlignVCenter);
|
|
legendLayout->addWidget(new QLabel(tr(r.text), this), legend_row, 1);
|
|
legend_row++;
|
|
}
|
|
legendLayout->setColumnStretch(1, 1);
|
|
image_feature_grid->addWidget(new QLabel(tr("Legend"), this), 6, 0, 1, 2);
|
|
image_feature_grid->addLayout(legendLayout, 7, 0, 1, 2);
|
|
|
|
featuresSection->setContentLayout(image_feature_grid);
|
|
layout->addWidget(featuresSection);
|
|
|
|
auto *ringsSection = new CollapsibleSection("Resolution rings", this);
|
|
auto *ringsLayout = new QVBoxLayout();
|
|
ringsLayout->setContentsMargins(0, 0, 0, 0);
|
|
res_rings = new ResolutionRingWidget(this);
|
|
|
|
connect(res_rings, &ResolutionRingWidget::resRingsSet, [this] (const QVector<float> &v) {
|
|
emit resRingsSet(v);
|
|
});
|
|
|
|
connect(res_rings, &ResolutionRingWidget::ringModeSet, [this] (JFJochDiffractionImage::RingMode mode) {
|
|
emit ringModeSet(mode);
|
|
});
|
|
|
|
ringsLayout->addWidget(res_rings);
|
|
ringsSection->setContentLayout(ringsLayout);
|
|
layout->addWidget(ringsSection);
|
|
|
|
// The per-image statistics plot now lives in the dataset-info dock (its "Per-image" toggle),
|
|
// and powder calibration / full analysis live in the settings dock and the hero buttons,
|
|
// so they are no longer duplicated here.
|
|
|
|
roiSection = new CollapsibleSection("ROI", this);
|
|
auto *roiLayout = new QVBoxLayout();
|
|
roiLayout->setContentsMargins(0, 0, 0, 0);
|
|
|
|
auto roiLabelsCheckBox = new QCheckBox("Show labels", this);
|
|
roiLabelsCheckBox->setCheckState(Qt::CheckState::Unchecked);
|
|
connect(roiLabelsCheckBox, &QCheckBox::toggled, this, &JFJochViewerSidePanel::showROILabels);
|
|
|
|
auto roiFillCheckBox = new QCheckBox("Translucent fill", this);
|
|
roiFillCheckBox->setCheckState(Qt::CheckState::Unchecked);
|
|
connect(roiFillCheckBox, &QCheckBox::toggled, this, &JFJochViewerSidePanel::showROIFill);
|
|
|
|
auto roiToggleRow = new QHBoxLayout();
|
|
roiToggleRow->addWidget(roiLabelsCheckBox);
|
|
roiToggleRow->addWidget(roiFillCheckBox);
|
|
roiLayout->addLayout(roiToggleRow);
|
|
|
|
roi_list = new JFJochViewerROIList(this);
|
|
roiLayout->addWidget(roi_list);
|
|
connect(this, &JFJochViewerSidePanel::imageLoaded, roi_list, &JFJochViewerROIList::loadImage);
|
|
connect(roi_list, &JFJochViewerROIList::roisChanged, this, &JFJochViewerSidePanel::roisChanged);
|
|
connect(roi_list, &JFJochViewerROIList::selectedROIChanged, this, &JFJochViewerSidePanel::selectedROIChanged);
|
|
connect(roi_list, &JFJochViewerROIList::downloadROIs, this, &JFJochViewerSidePanel::downloadROIs);
|
|
connect(roi_list, &JFJochViewerROIList::uploadROIs, this, &JFJochViewerSidePanel::uploadROIs);
|
|
connect(roi_list, &JFJochViewerROIList::maskFromROI, this, &JFJochViewerSidePanel::maskFromROI);
|
|
connect(roi_list, &JFJochViewerROIList::roisChanged, this, [this] { roiSection->setExpanded(true); });
|
|
roiSection->setContentLayout(roiLayout);
|
|
layout->addWidget(roiSection);
|
|
|
|
layout->addStretch();
|
|
setLayout(layout); // Set the layout to the widget
|
|
}
|
|
|
|
void JFJochViewerSidePanel::spotsToggled(bool input) {
|
|
emit showSpots(input);
|
|
}
|
|
|
|
void JFJochViewerSidePanel::predictionsToggled(bool input) {
|
|
emit showPredictions(input);
|
|
}
|
|
|
|
void JFJochViewerSidePanel::loadImage(std::shared_ptr<const JFJochReaderImage> image) {
|
|
emit imageLoaded(image);
|
|
}
|
|
|
|
void JFJochViewerSidePanel::saturatedPixelsToggled(bool input) {
|
|
emit showSaturatedPixels(input);
|
|
}
|
|
|
|
void JFJochViewerSidePanel::highlightIceRingsToggled(bool input) {
|
|
emit highlightIceRings(input);
|
|
}
|
|
|
|
void JFJochViewerSidePanel::hideUnindexedSpotsToggled(bool input) {
|
|
emit hideUnindexedSpots(input);
|
|
}
|
|
|
|
void JFJochViewerSidePanel::hideIceRingSpotsToggled(bool input) {
|
|
emit hideIceRingSpots(input);
|
|
}
|
|
|
|
void JFJochViewerSidePanel::selectROIInList(QString name) {
|
|
roi_list->setSelected(name);
|
|
}
|
|
|
|
void JFJochViewerSidePanel::SetRings(QVector<float> v) {
|
|
res_rings->setRings(v);
|
|
}
|