jfjoch_viewer: live updates no longer rebuild open combo boxes

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>
This commit is contained in:
2026-10-12 19:46:48 +02:00
co-authored by Claude Opus 5.5
parent 83b1d85400
commit 9cf987cd4e
11 changed files with 204 additions and 72 deletions
+71
View File
@@ -0,0 +1,71 @@
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#include "LiveComboBox.h"
#include <QAbstractItemView>
#include <QEvent>
#include <QIcon>
#include <QPixmap>
#include <QSignalBlocker>
#include <QTimer>
LiveComboBox::LiveComboBox(QWidget *parent) : QComboBox(parent) {
// The popup is a separate window holding the view. Not every way of closing it goes through
// hidePopup() (a click outside just hides the window), so watch the window itself.
view()->window()->installEventFilter(this);
}
bool LiveComboBox::SetItems(const QVector<Item> &items) {
if (view()->isVisible()) {
pending_ = items;
return false;
}
pending_.reset();
if (items == items_)
return false;
Replace(items);
return true;
}
void LiveComboBox::Replace(const QVector<Item> &items) {
QSignalBlocker blocker(this);
const QVariant selected = currentData();
clear();
for (const auto &item : items) {
if (item.separator) {
insertSeparator(count());
continue;
}
if (item.swatch.isValid()) {
QPixmap pixmap(14, 14);
pixmap.fill(item.swatch);
addItem(QIcon(pixmap), item.text, item.data);
} else
addItem(item.text, item.data);
if (!item.tooltip.isEmpty())
setItemData(count() - 1, item.tooltip, Qt::ToolTipRole);
}
items_ = items;
const int index = selected.isValid() ? findData(selected) : -1;
setCurrentIndex(index >= 0 ? index : (count() > 0 ? 0 : -1));
}
void LiveComboBox::ApplyPending() {
if (!pending_ || view()->isVisible())
return;
const QVector<Item> items = std::move(*pending_);
pending_.reset();
if (items == items_)
return;
Replace(items);
emit itemsReplaced();
}
bool LiveComboBox::eventFilter(QObject *watched, QEvent *event) {
// Queued: a click on an item hides the popup first and selects the item afterwards, so the
// held-back items go in once the pick has landed, and keep it.
if (event->type() == QEvent::Hide && pending_)
QTimer::singleShot(0, this, &LiveComboBox::ApplyPending);
return QComboBox::eventFilter(watched, event);
}