mirror of
https://github.com/bec-project/bec_widgets.git
synced 2026-05-13 01:55:46 +02:00
wip line edit like optional completer for Editable Comboboxes
This commit is contained in:
@@ -7,8 +7,8 @@ from bec_lib.device import ComputedSignal, Device, Positioner, ReadoutPriority
|
||||
from bec_lib.device import Signal as BECSignal
|
||||
from bec_lib.logger import bec_logger
|
||||
from pydantic import Field, field_validator
|
||||
from qtpy.QtCore import QSize, Signal, Slot
|
||||
from qtpy.QtWidgets import QComboBox, QSizePolicy
|
||||
from qtpy.QtCore import QSize, QStringListModel, Signal, Slot
|
||||
from qtpy.QtWidgets import QComboBox, QCompleter, QSizePolicy
|
||||
|
||||
from bec_widgets.utils.bec_connector import ConnectionConfig
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
@@ -36,6 +36,7 @@ class DeviceInputConfig(ConnectionConfig):
|
||||
arg_name: str | None = None
|
||||
apply_filter: bool = True
|
||||
signal_class_filter: list[str] = Field(default_factory=list)
|
||||
autocomplete: bool = False
|
||||
|
||||
@field_validator("device_filter")
|
||||
@classmethod
|
||||
@@ -104,6 +105,7 @@ class DeviceComboBox(BECWidget, QComboBox):
|
||||
default: str | None = None,
|
||||
arg_name: str | None = None,
|
||||
signal_class_filter: list[str] | None = None,
|
||||
autocomplete: bool | None = None,
|
||||
**kwargs,
|
||||
):
|
||||
self.config = self._process_config(config)
|
||||
@@ -124,6 +126,7 @@ class DeviceComboBox(BECWidget, QComboBox):
|
||||
self._is_valid_input = False
|
||||
self._accent_colors = get_accent_colors()
|
||||
self._set_first_element_as_empty = False
|
||||
self._completer_model = QStringListModel(self)
|
||||
|
||||
self.setEditable(True)
|
||||
self.setInsertPolicy(QComboBox.NoInsert)
|
||||
@@ -144,6 +147,10 @@ class DeviceComboBox(BECWidget, QComboBox):
|
||||
signal_class_filter = self.config.signal_class_filter
|
||||
if default is None and self.config.default:
|
||||
default = self.config.default
|
||||
if autocomplete is not None:
|
||||
self.config.autocomplete = autocomplete
|
||||
if self.config.autocomplete:
|
||||
self.autocomplete = True
|
||||
|
||||
if available_devices is not None:
|
||||
self.set_available_devices(available_devices)
|
||||
@@ -358,6 +365,20 @@ class DeviceComboBox(BECWidget, QComboBox):
|
||||
if not current_text:
|
||||
self.setCurrentText("")
|
||||
|
||||
@SafeProperty(bool)
|
||||
def autocomplete(self) -> bool:
|
||||
"""Whether autocomplete suggestions are enabled while editing."""
|
||||
return self.config.autocomplete
|
||||
|
||||
@autocomplete.setter
|
||||
def autocomplete(self, value: bool) -> None:
|
||||
self.config.autocomplete = value
|
||||
if value:
|
||||
completer = QCompleter(self._completer_model, self)
|
||||
self.setCompleter(completer)
|
||||
else:
|
||||
self._restore_default_completer()
|
||||
|
||||
@property
|
||||
def device_filter(self) -> list[BECDeviceFilter]:
|
||||
"""Device class filters."""
|
||||
@@ -508,10 +529,22 @@ class DeviceComboBox(BECWidget, QComboBox):
|
||||
def _replace_items(self, devices: list[str]):
|
||||
current_text = self.currentText()
|
||||
replace_combobox_items(self, devices)
|
||||
self._update_completer_model(devices)
|
||||
if self._set_first_element_as_empty:
|
||||
self.insertItem(0, "")
|
||||
self.setCurrentText(current_text)
|
||||
|
||||
def _update_completer_model(self, items: list[str]) -> None:
|
||||
self._completer_model.setStringList(items)
|
||||
|
||||
def _restore_default_completer(self) -> None:
|
||||
if self.completer() is not None and self.completer().model() == self.model():
|
||||
return
|
||||
current_text = self.currentText()
|
||||
self.setEditable(False)
|
||||
self.setEditable(True)
|
||||
self.setCurrentText(current_text)
|
||||
|
||||
def _device_name_from_text(self, text: str) -> str:
|
||||
index = self.findText(text)
|
||||
if index >= 0 and isinstance(self.itemData(index), tuple):
|
||||
|
||||
@@ -3,8 +3,8 @@ from __future__ import annotations
|
||||
from bec_lib.callback_handler import EventType
|
||||
from bec_lib.device import Signal as BECSignal
|
||||
from bec_lib.logger import bec_logger
|
||||
from qtpy.QtCore import Property, QSize, Qt, Signal, Slot
|
||||
from qtpy.QtWidgets import QComboBox, QSizePolicy
|
||||
from qtpy.QtCore import Property, QSize, QStringListModel, Qt, Signal, Slot
|
||||
from qtpy.QtWidgets import QComboBox, QCompleter, QSizePolicy
|
||||
|
||||
from bec_widgets.utils.bec_connector import ConnectionConfig
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
@@ -29,6 +29,7 @@ class SignalComboBoxConfig(ConnectionConfig):
|
||||
arg_name: str | None = None
|
||||
device: str | None = None
|
||||
signals: list[str] | None = None
|
||||
autocomplete: bool = False
|
||||
|
||||
|
||||
class SignalComboBox(BECWidget, QComboBox):
|
||||
@@ -73,6 +74,7 @@ class SignalComboBox(BECWidget, QComboBox):
|
||||
arg_name: str | None = None,
|
||||
store_signal_config: bool = True,
|
||||
require_device: bool = False,
|
||||
autocomplete: bool | None = None,
|
||||
**kwargs,
|
||||
):
|
||||
self.config = self._process_config(config)
|
||||
@@ -90,6 +92,7 @@ class SignalComboBox(BECWidget, QComboBox):
|
||||
self._store_signal_config = store_signal_config
|
||||
self._require_device = require_device
|
||||
self._is_valid_input = False
|
||||
self._completer_model = QStringListModel(self)
|
||||
|
||||
if arg_name is not None:
|
||||
self.config.arg_name = arg_name
|
||||
@@ -105,12 +108,16 @@ class SignalComboBox(BECWidget, QComboBox):
|
||||
device = self.config.device
|
||||
if default is None and self.config.default:
|
||||
default = self.config.default
|
||||
if autocomplete is not None:
|
||||
self.config.autocomplete = autocomplete
|
||||
self.config.ndim_filter = ndim_filter
|
||||
|
||||
self.setEditable(True)
|
||||
self.setInsertPolicy(QComboBox.NoInsert)
|
||||
self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
|
||||
self.setMinimumSize(QSize(100, 0))
|
||||
if self.config.autocomplete:
|
||||
self.autocomplete = True
|
||||
|
||||
self._device_update_register = self.bec_dispatcher.client.callbacks.register(
|
||||
EventType.DEVICE_UPDATE, self.update_signals_from_filters
|
||||
@@ -281,6 +288,20 @@ class SignalComboBox(BECWidget, QComboBox):
|
||||
self._require_device = value
|
||||
self.update_signals_from_filters()
|
||||
|
||||
@SafeProperty(bool)
|
||||
def autocomplete(self) -> bool:
|
||||
"""Whether autocomplete suggestions are enabled while editing."""
|
||||
return self.config.autocomplete
|
||||
|
||||
@autocomplete.setter
|
||||
def autocomplete(self, value: bool) -> None:
|
||||
self.config.autocomplete = value
|
||||
if value:
|
||||
completer = QCompleter(self._completer_model, self)
|
||||
self.setCompleter(completer)
|
||||
else:
|
||||
self._restore_default_completer()
|
||||
|
||||
@property
|
||||
def signals(self) -> list[str | tuple[str, dict]]:
|
||||
"""Available signals after filtering."""
|
||||
@@ -427,6 +448,7 @@ class SignalComboBox(BECWidget, QComboBox):
|
||||
self.config.signals = [
|
||||
entry if isinstance(entry, str) else entry[0] for entry in self._signals
|
||||
]
|
||||
self._update_completer_model(self.config.signals)
|
||||
if self._set_first_element_as_empty and self.count() > 0 and self.itemText(0) != "":
|
||||
self.insertItem(0, "")
|
||||
|
||||
@@ -494,6 +516,7 @@ class SignalComboBox(BECWidget, QComboBox):
|
||||
|
||||
def _replace_signal_items(self):
|
||||
replace_combobox_items(self, self._signals)
|
||||
self._update_completer_model(self._signal_display_texts(self._signals))
|
||||
if self._set_first_element_as_empty and self.count() > 0 and self.itemText(0) != "":
|
||||
self.insertItem(0, "")
|
||||
|
||||
@@ -543,6 +566,21 @@ class SignalComboBox(BECWidget, QComboBox):
|
||||
return item_index
|
||||
return -1
|
||||
|
||||
@staticmethod
|
||||
def _signal_display_texts(signals: list[str | tuple[str, dict]]) -> list[str]:
|
||||
return [entry[0] if isinstance(entry, tuple) else entry for entry in signals]
|
||||
|
||||
def _update_completer_model(self, items: list[str]) -> None:
|
||||
self._completer_model.setStringList(items)
|
||||
|
||||
def _restore_default_completer(self) -> None:
|
||||
if self.completer() is not None and self.completer().model() == self.model():
|
||||
return
|
||||
current_text = self.currentText()
|
||||
self.setEditable(False)
|
||||
self.setEditable(True)
|
||||
self.setCurrentText(current_text)
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
from qtpy.QtWidgets import QApplication, QVBoxLayout, QWidget
|
||||
|
||||
@@ -277,6 +277,7 @@ class ScanGroupBox(QGroupBox):
|
||||
arg_name=arg_name,
|
||||
default=default,
|
||||
device_filter=BECDeviceFilter.DEVICE,
|
||||
autocomplete=True,
|
||||
)
|
||||
else:
|
||||
widget = widget_class(parent=self.parent(), arg_name=arg_name, default=default)
|
||||
|
||||
@@ -48,6 +48,9 @@ def test_device_input_combobox_init(device_input_combobox):
|
||||
ReadoutPriority.ON_REQUEST.value,
|
||||
]
|
||||
assert device_input_combobox.config.default is None
|
||||
assert device_input_combobox.autocomplete is False
|
||||
assert device_input_combobox.completer() is not None
|
||||
assert device_input_combobox.completer().model() == device_input_combobox.model()
|
||||
assert device_input_combobox.devices == [
|
||||
"samx",
|
||||
"samy",
|
||||
@@ -76,6 +79,22 @@ def test_device_input_combobox_init_with_kwargs(device_input_combobox_with_kwarg
|
||||
assert device_input_combobox_with_kwargs.config.arg_name == "test_arg_name"
|
||||
|
||||
|
||||
def test_device_input_combobox_autocomplete(qtbot, mocked_client):
|
||||
widget = DeviceComboBox(client=mocked_client, autocomplete=True)
|
||||
qtbot.addWidget(widget)
|
||||
qtbot.waitExposed(widget)
|
||||
|
||||
assert widget.autocomplete is True
|
||||
assert widget.completer() is not None
|
||||
assert widget.completer().model().stringList() == widget.devices
|
||||
assert widget.completer().model() != widget.model()
|
||||
|
||||
widget.autocomplete = False
|
||||
|
||||
assert widget.completer() is not None
|
||||
assert widget.completer().model() == widget.model()
|
||||
|
||||
|
||||
def test_get_device_from_input_combobox_init(device_input_combobox):
|
||||
device_input_combobox.setCurrentIndex(0)
|
||||
device_text = device_input_combobox.currentText()
|
||||
|
||||
@@ -44,6 +44,34 @@ def test_signal_combobox_init(device_signal_combobox):
|
||||
assert device_signal_combobox._hinted_signals == []
|
||||
assert device_signal_combobox._normal_signals == []
|
||||
assert device_signal_combobox._config_signals == []
|
||||
assert device_signal_combobox.autocomplete is False
|
||||
assert device_signal_combobox.completer() is not None
|
||||
assert device_signal_combobox.completer().model() == device_signal_combobox.model()
|
||||
|
||||
|
||||
def test_signal_combobox_autocomplete(qtbot, mocked_client):
|
||||
widget = create_widget(
|
||||
qtbot=qtbot,
|
||||
widget=SignalComboBox,
|
||||
client=mocked_client,
|
||||
autocomplete=True,
|
||||
)
|
||||
|
||||
widget.set_device("samx")
|
||||
|
||||
assert widget.autocomplete is True
|
||||
assert widget.completer() is not None
|
||||
assert widget.completer().model().stringList() == [
|
||||
"samx (readback)",
|
||||
"setpoint",
|
||||
"velocity",
|
||||
]
|
||||
assert widget.completer().model() != widget.model()
|
||||
|
||||
widget.autocomplete = False
|
||||
|
||||
assert widget.completer() is not None
|
||||
assert widget.completer().model() == widget.model()
|
||||
|
||||
|
||||
def test_signal_combobox_qproperties(device_signal_combobox):
|
||||
|
||||
@@ -307,6 +307,7 @@ def test_on_scan_selected(scan_control, scan_name):
|
||||
assert isinstance(widget, expected_widget_type) # Confirm the widget type matches
|
||||
if isinstance(widget, DeviceComboBox):
|
||||
assert widget.currentText() == ""
|
||||
assert widget.autocomplete is True
|
||||
assert "samx" in widget.devices
|
||||
assert (
|
||||
"async_device" in widget.devices
|
||||
|
||||
Reference in New Issue
Block a user