// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute // SPDX-License-Identifier: GPL-3.0-only #include "JFJochViewerROIList.h" #include #include #include "../../common/JFJochMath.h" #include #include #include #include #include JFJochViewerROIList::JFJochViewerROIList(QWidget *parent) : QWidget(parent) { auto *layout = new QVBoxLayout(this); combo_ = new QComboBox(this); layout->addWidget(combo_); connect(combo_, &QComboBox::currentIndexChanged, this, &JFJochViewerROIList::OnSelectionChanged); auto *buttons = new QHBoxLayout(); type_combo_ = new QComboBox(this); type_combo_->addItems({"Box", "Circle", "Azimuthal"}); buttons->addWidget(type_combo_); auto *add = new QPushButton("+", this); auto *ren = new QPushButton("Rename", this); auto *del = new QPushButton("Delete", this); buttons->addWidget(add); buttons->addWidget(ren); buttons->addWidget(del); layout->addLayout(buttons); connect(add, &QPushButton::clicked, this, &JFJochViewerROIList::OnAdd); connect(ren, &QPushButton::clicked, this, &JFJochViewerROIList::OnRename); connect(del, &QPushButton::clicked, this, &JFJochViewerROIList::OnDelete); auto *server = new QHBoxLayout(); auto *download = new QPushButton("Download", this); auto *upload = new QPushButton("Upload to server", this); server->addWidget(download); server->addWidget(upload); layout->addLayout(server); connect(download, &QPushButton::clicked, this, &JFJochViewerROIList::downloadROIs); connect(upload, &QPushButton::clicked, this, &JFJochViewerROIList::uploadROIs); auto *grid = new QGridLayout(); for (int i = 0; i < 4; i++) { flabel_[i] = new QLabel("", this); fedit_[i] = new QLineEdit(this); fedit_[i]->setAlignment(Qt::AlignRight); fedit_[i]->setMaximumWidth(90); fedit_[i]->setStyleSheet("background-color: white; color: black;"); grid->addWidget(flabel_[i], i, 0); grid->addWidget(fedit_[i], i, 1, Qt::AlignRight); connect(fedit_[i], &QLineEdit::editingFinished, this, &JFJochViewerROIList::ApplyEditor); } layout->addLayout(grid); auto *mask = new QHBoxLayout(); auto *mask_add = new QPushButton("Add to mask", this); auto *mask_sub = new QPushButton("Subtract from mask", this); mask->addWidget(mask_add); mask->addWidget(mask_sub); layout->addLayout(mask); connect(mask_add, &QPushButton::clicked, [this] { if (!SelectedName().isEmpty()) emit maskFromROI(SelectedName(), true); }); connect(mask_sub, &QPushButton::clicked, [this] { if (!SelectedName().isEmpty()) emit maskFromROI(SelectedName(), false); }); result_ = new JFJochViewerROIResult(this); layout->addWidget(result_); } void JFJochViewerROIList::UpdateEditor() { const std::string sel = SelectedName().toStdString(); auto set = [&](int i, const QString &label, double val, bool integer) { flabel_[i]->setText(label); flabel_[i]->setVisible(!label.isEmpty()); fedit_[i]->setVisible(!label.isEmpty()); if (!label.isEmpty()) fedit_[i]->setText(integer ? QString::number(std::lround(val)) : QString::number(val, 'g', 6)); }; for (int i = 0; i < 4; i++) set(i, "", 0, false); for (const auto &b : rois_.boxes) if (b.GetName() == sel) { set(0, "Min X", b.GetXMin(), true); set(1, "Max X", b.GetXMax(), true); set(2, "Min Y", b.GetYMin(), true); set(3, "Max Y", b.GetYMax(), true); return; } for (const auto &c : rois_.circles) if (c.GetName() == sel) { set(0, "Center X", c.GetX(), false); set(1, "Center Y", c.GetY(), false); set(2, "Radius", c.GetRadius_pxl(), false); set(3, "", 0, false); return; } for (const auto &a : rois_.azimuthal) if (a.GetName() == sel) { set(0, "Q min [1/A]", a.GetQMin_recipA(), false); set(1, "Q max [1/A]", a.GetQMax_recipA(), false); set(2, "phi min [deg]", a.GetPhiMin_deg(), false); set(3, "phi max [deg]", a.GetPhiMax_deg(), false); return; } } void JFJochViewerROIList::ApplyEditor() { if (rebuilding_) return; const std::string sel = SelectedName().toStdString(); bool changed = false; for (auto &b : rois_.boxes) if (b.GetName() == sel) { b = ROIBox(sel, fedit_[0]->text().toLongLong(), fedit_[1]->text().toLongLong(), fedit_[2]->text().toLongLong(), fedit_[3]->text().toLongLong()); changed = true; break; } for (auto &c : rois_.circles) if (c.GetName() == sel) { c = ROICircle(sel, fedit_[0]->text().toDouble(), fedit_[1]->text().toDouble(), std::max(0.1, fedit_[2]->text().toDouble())); changed = true; break; } for (auto &a : rois_.azimuthal) if (a.GetName() == sel) { const float qmin = fedit_[0]->text().toFloat(); const float qmax = fedit_[1]->text().toFloat(); const float phimin = fedit_[2]->text().toFloat(); const float phimax = fedit_[3]->text().toFloat(); const float d_min = (qmax > 0) ? 2.0f * static_cast(PI) / qmax : a.GetDMin_A(); const float d_max = (qmin > 0) ? 2.0f * static_cast(PI) / qmin : a.GetDMax_A(); a = ROIAzimuthal(sel, d_min, d_max, phimin, phimax); changed = true; break; } if (changed) emit roisChanged(rois_); } void JFJochViewerROIList::loadImage(std::shared_ptr image) { image_ = image; const QString keep = SelectedName(); rois_ = image_ ? image_->Dataset().experiment.ROI().GetROIDefinition() : ROIDefinition{}; RebuildCombo(keep); ShowSelectedResult(); UpdateEditor(); } void JFJochViewerROIList::RebuildCombo(const QString &keep_selected) { rebuilding_ = true; combo_->clear(); auto add_item = [&](const std::string &name, const char *type) { // display "name · type", with the bare name kept as item data for lookups combo_->addItem(QString("%1 · %2").arg(QString::fromStdString(name), type), QString::fromStdString(name)); }; for (const auto &b : rois_.boxes) add_item(b.GetName(), "box"); for (const auto &c : rois_.circles) add_item(c.GetName(), "circle"); for (const auto &a : rois_.azimuthal) add_item(a.GetName(), "azim"); if (!keep_selected.isEmpty()) { const int idx = combo_->findData(keep_selected); if (idx >= 0) combo_->setCurrentIndex(idx); } rebuilding_ = false; } QString JFJochViewerROIList::SelectedName() const { return combo_->currentData().toString(); } void JFJochViewerROIList::setSelected(QString name) { const int idx = combo_->findData(name); if (idx >= 0 && idx != combo_->currentIndex()) combo_->setCurrentIndex(idx); // updates the stats and selection highlight } void JFJochViewerROIList::ShowSelectedResult() { const QString name = SelectedName(); if (image_ && !name.isEmpty()) { const auto &roi = image_->ImageData().roi; auto it = roi.find(name.toStdString()); if (it != roi.end()) { result_->SetROIResult(it->second); return; } } result_->SetROIResult(ROIMessage{}); // clears the display } std::string JFJochViewerROIList::UniqueName() const { std::set used; for (const auto &b : rois_.boxes) used.insert(b.GetName()); for (const auto &c : rois_.circles) used.insert(c.GetName()); for (const auto &a : rois_.azimuthal) used.insert(a.GetName()); for (int i = 1; ; i++) { std::string n = "roi" + std::to_string(i); if (!used.count(n)) return n; } } void JFJochViewerROIList::OnSelectionChanged() { if (rebuilding_) return; ShowSelectedResult(); UpdateEditor(); emit selectedROIChanged(SelectedName()); } void JFJochViewerROIList::OnAdd() { if (rois_.boxes.size() + rois_.circles.size() + rois_.azimuthal.size() >= 16) return; // bitmap allows at most 16 ROIs const std::string name = UniqueName(); int64_t cx = 100, cy = 100; if (image_) { cx = image_->Dataset().experiment.GetXPixelsNum() / 2; cy = image_->Dataset().experiment.GetYPixelsNum() / 2; } switch (type_combo_->currentIndex()) { case 0: rois_.boxes.emplace_back(name, cx - 100, cx + 100, cy - 100, cy + 100); break; case 1: rois_.circles.emplace_back(name, cx, cy, 100); break; default: rois_.azimuthal.emplace_back(name, 2.0f, 4.0f); break; } emit roisChanged(rois_); RebuildCombo(QString::fromStdString(name)); ShowSelectedResult(); } void JFJochViewerROIList::OnDelete() { const std::string name = SelectedName().toStdString(); if (name.empty()) return; auto erase_by_name = [&name](auto &vec) { for (auto it = vec.begin(); it != vec.end(); ++it) if (it->GetName() == name) { vec.erase(it); return true; } return false; }; if (erase_by_name(rois_.boxes) || erase_by_name(rois_.circles) || erase_by_name(rois_.azimuthal)) { emit roisChanged(rois_); RebuildCombo(QString()); ShowSelectedResult(); } } void JFJochViewerROIList::OnRename() { const QString old_name = SelectedName(); if (old_name.isEmpty()) return; bool ok = false; const QString new_name = QInputDialog::getText(this, "Rename ROI", "New name:", QLineEdit::Normal, old_name, &ok); if (!ok || new_name.isEmpty() || new_name == old_name) return; const std::string oldn = old_name.toStdString(); const std::string newn = new_name.toStdString(); for (auto &b : rois_.boxes) if (b.GetName() == oldn) { b = ROIBox(newn, b.GetXMin(), b.GetXMax(), b.GetYMin(), b.GetYMax()); break; } for (auto &c : rois_.circles) if (c.GetName() == oldn) { c = ROICircle(newn, c.GetX(), c.GetY(), c.GetRadius_pxl()); break; } for (auto &a : rois_.azimuthal) if (a.GetName() == oldn) { a = a.HasPhi() ? ROIAzimuthal(newn, a.GetDMin_A(), a.GetDMax_A(), a.GetPhiMin_deg(), a.GetPhiMax_deg()) : ROIAzimuthal(newn, a.GetDMin_A(), a.GetDMax_A()); break; } emit roisChanged(rois_); RebuildCombo(new_name); ShowSelectedResult(); }