refactor(curve_tree): use shared scan index combobox

This commit is contained in:
2026-07-10 15:56:56 +02:00
committed by Jan Wyzula
parent 567b10c137
commit d2b5c74204
2 changed files with 18 additions and 110 deletions
@@ -5,7 +5,6 @@ from typing import TYPE_CHECKING
from bec_lib.logger import bec_logger
from bec_qthemes._icon.material_icons import material_icon
from qtpy.QtGui import QValidator
from qtpy.QtWidgets import (
QApplication,
QCheckBox,
@@ -39,6 +38,7 @@ from bec_widgets.widgets.control.device_input.device_combobox.device_combobox im
from bec_widgets.widgets.control.device_input.signal_combobox.signal_combobox import SignalComboBox
from bec_widgets.widgets.dap.dap_combo_box.dap_combo_box import DapComboBox
from bec_widgets.widgets.plots.waveform.curve import CurveConfig, DeviceSignal
from bec_widgets.widgets.utility.scan_index_combobox.scan_index_combobox import ScanIndexComboBox
from bec_widgets.widgets.utility.visual.color_button_native.color_button_native import (
ColorButtonNative,
)
@@ -51,31 +51,6 @@ if TYPE_CHECKING: # pragma: no cover
logger = bec_logger.logger
class ScanIndexValidator(QValidator):
"""Validator to allow only 'live' or integer scan numbers from an allowed set."""
def __init__(self, allowed_scans: set[int] | None = None, parent=None):
super().__init__(parent)
self.allowed_scans = allowed_scans or set()
def validate(self, input_str: str, pos: int):
# Accept empty or 'live'
if input_str == "" or input_str == "live":
return QValidator.State.Acceptable, input_str, pos
# Allow partial editing of "live"
if "live".startswith(input_str):
return QValidator.State.Intermediate, input_str, pos
# Accept integer only if present in the allowed set
if input_str.isdigit():
try:
num = int(input_str)
except ValueError:
return QValidator.State.Invalid, input_str, pos
if num in self.allowed_scans:
return QValidator.State.Acceptable, input_str, pos
return QValidator.State.Invalid, input_str, pos
def _normalize_dap_selection(
dap_value: str | list[str] | tuple[str, ...] | None,
) -> tuple[str, list[str]]:
@@ -184,6 +159,7 @@ class CurveRow(QTreeWidgetItem):
# BEC user input
self.device_edit = None
self.dap_combo = None
self.scan_index_combo = None
self.composite_models: list[str] = []
self.dev = device_manager
@@ -205,51 +181,9 @@ class CurveRow(QTreeWidgetItem):
"""Create the Scan # editable combobox in column 3."""
if self.source not in ("device", "history"):
return
self.scan_index_combo = QComboBox()
self.scan_index_combo.setEditable(True)
# Populate 'live' and all available history scan indices
self.scan_index_combo.addItem("live", None)
scan_number_list = []
scan_id_list = []
try:
history = getattr(self.curve_tree.client, "history", None)
if history is not None:
scan_number_list = getattr(history, "_scan_numbers", []) or []
scan_id_list = getattr(history, "_scan_ids", []) or []
except Exception as e:
logger.error(f"Cannot fetch scan numbers from BEC client: {e}")
# If scan numbers cannot be fetched, only provide 'live' option
scan_number_list = []
scan_id_list = []
# Restrict input to 'live' or valid scan numbers
allowed = set()
try:
allowed = set(int(n) for n in scan_number_list if isinstance(n, (int, str)))
except Exception:
allowed = set()
validator = ScanIndexValidator(allowed, self.scan_index_combo)
self.scan_index_combo.lineEdit().setValidator(validator)
# Add items: show scan numbers, store scan IDs as item data
if scan_number_list and scan_id_list and len(scan_number_list) == len(scan_id_list):
for num, sid in zip(scan_number_list, scan_id_list):
self.scan_index_combo.addItem(str(num), sid)
else:
logger.error("Scan number and ID lists are mismatched or empty.")
self.scan_index_combo = ScanIndexComboBox(parent=self.tree, client=self.curve_tree.client)
# Select current based on existing config
selected = False
if getattr(self.config, "scan_id", None): # scan_id matching only
for i in range(self.scan_index_combo.count()):
if self.scan_index_combo.itemData(i) == self.config.scan_id:
self.scan_index_combo.setCurrentIndex(i)
selected = True
break
if not selected:
self.scan_index_combo.setCurrentText("live")
self.scan_index_combo.set_scan_id(self.config.scan_id)
self.tree.setItemWidget(self, 3, self.scan_index_combo)
def _init_actions(self):
@@ -461,6 +395,11 @@ class CurveRow(QTreeWidgetItem):
self.entry_edit.deleteLater()
self.entry_edit = None
if self.scan_index_combo is not None:
self.scan_index_combo.close()
self.scan_index_combo.deleteLater()
self.scan_index_combo = None
if getattr(self, "dap_combo", None) is not None:
self.dap_combo.close()
self.dap_combo.deleteLater()
@@ -514,23 +453,18 @@ class CurveRow(QTreeWidgetItem):
)
self.config.signal = DeviceSignal(device=device_name, signal=device_entry)
scan_combo_text = self.scan_index_combo.currentText()
if scan_combo_text == "live" or scan_combo_text == "":
scan_number = self.scan_index_combo.scan_number
if scan_number is None:
self.config.scan_number = None
self.config.scan_id = None
self.config.source = "device"
self.config.label = f"{device_name}-{device_entry}"
if scan_combo_text.isdigit():
try:
scan_num = int(scan_combo_text)
except ValueError:
scan_num = None
self.config.scan_number = scan_num
self.config.scan_id = self.scan_index_combo.currentData()
else:
self.config.scan_number = scan_number
self.config.scan_id = self.scan_index_combo.scan_id
self.config.source = "history"
# Label history curves with scan number suffix
if scan_num is not None:
self.config.label = f"{device_name}-{device_entry}-scan-{scan_num}"
self.config.label = f"{device_name}-{device_entry}-scan-{scan_number}"
else:
# DAP logic
parent_conf_dict = {}
+3 -29
View File
@@ -3,14 +3,10 @@ from unittest.mock import MagicMock, patch
import pytest
from bec_lib.scan_history import ScanHistory
from qtpy.QtGui import QValidator
from qtpy.QtWidgets import QComboBox, QVBoxLayout
from bec_widgets.widgets.plots.waveform.settings.curve_settings.curve_setting import CurveSetting
from bec_widgets.widgets.plots.waveform.settings.curve_settings.curve_tree import (
CurveTree,
ScanIndexValidator,
)
from bec_widgets.widgets.plots.waveform.settings.curve_settings.curve_tree import CurveTree
from bec_widgets.widgets.plots.waveform.waveform import Waveform
from tests.unit_tests.client_mocks import dap_plugin_message, mocked_client, mocked_client_with_dap
from tests.unit_tests.conftest import create_widget
@@ -447,32 +443,10 @@ def test_primary_model_change_removes_duplicate_composite_model(curve_tree_fixtu
assert dap_child.composite_button.text() == "Composite (2)"
def test_scan_index_validator_behavior():
"""
Test ScanIndexValidator allows empty, 'live', partial 'live', valid scan numbers,
and rejects invalid or disallowed inputs under the new allowed-set API.
"""
validator = ScanIndexValidator(allowed_scans={1, 2, 3})
def state(txt):
s, _, _ = validator.validate(txt, 0)
return s
assert state("") == QValidator.State.Acceptable
assert state("live") == QValidator.State.Acceptable
assert state("l") == QValidator.State.Intermediate
assert state("liv") == QValidator.State.Intermediate
assert state("1") == QValidator.State.Acceptable
assert state("3") == QValidator.State.Acceptable
assert state("4") == QValidator.State.Invalid
assert state("0") == QValidator.State.Invalid
assert state("abc") == QValidator.State.Invalid
def test_export_data_history_curve(curve_tree_fixture, scan_history_factory):
"""
Test that export_data for a history curve row correctly serializes scan_number
and resets scan_id when a numeric scan is selected.
and scan_id when a numeric scan is typed into the scan index combobox.
"""
curve_tree, wf = curve_tree_fixture
# Inject two history scans into the waveform client
@@ -494,5 +468,5 @@ def test_export_data_history_curve(curve_tree_fixture, scan_history_factory):
exported = device_row.export_data()
assert exported["source"] == "history"
assert exported["scan_number"] == 2
assert exported["scan_id"] is None
assert exported["scan_id"] == "hid2"
assert exported["label"] == "bpm4i-bpm4i-scan-2"