diff --git a/bec_widgets/examples/device_manager_view/device_manager_view.py b/bec_widgets/examples/device_manager_view/device_manager_view.py index 625af75a..db037ec6 100644 --- a/bec_widgets/examples/device_manager_view/device_manager_view.py +++ b/bec_widgets/examples/device_manager_view/device_manager_view.py @@ -91,8 +91,8 @@ class DeviceManagerView(BECWidget, QWidget): self.ophyd_test_dock.setWidget(self.ophyd_test) # Create the dock widgets - self.explorer_dock = QtAds.CDockWidget("Explorer", self) - self.explorer_dock.setWidget(self.available_devices) + self.available_devices_dock = QtAds.CDockWidget("Explorer", self) + self.available_devices_dock.setWidget(self.available_devices) self.device_table_view_dock = QtAds.CDockWidget("Device Table", self) self.device_table_view_dock.setWidget(self.device_table_view) @@ -104,7 +104,9 @@ class DeviceManagerView(BECWidget, QWidget): self.dm_config_view_dock.setWidget(self.dm_config_view) # Add the dock widgets to the dock manager - self.dock_manager.addDockWidget(QtAds.DockWidgetArea.LeftDockWidgetArea, self.explorer_dock) + self.dock_manager.addDockWidget( + QtAds.DockWidgetArea.LeftDockWidgetArea, self.available_devices_dock + ) monaco_yaml_area = self.dock_manager.addDockWidget( QtAds.DockWidgetArea.RightDockWidgetArea, self.dm_config_view_dock ) @@ -129,6 +131,9 @@ class DeviceManagerView(BECWidget, QWidget): # Connect slots self.device_table_view.selected_device.connect(self.dm_config_view.on_select_config) + self.device_table_view.model.devices_reset.connect( + self.available_devices.update_devices_state + ) ####### Default view has to be done with setting up splitters ######## def set_default_view(self, horizontal_weights: list, vertical_weights: list): diff --git a/bec_widgets/widgets/control/device_manager/components/available_device_resources/available_device_resources.py b/bec_widgets/widgets/control/device_manager/components/available_device_resources/available_device_resources.py index 721599af..55d927ed 100644 --- a/bec_widgets/widgets/control/device_manager/components/available_device_resources/available_device_resources.py +++ b/bec_widgets/widgets/control/device_manager/components/available_device_resources/available_device_resources.py @@ -41,16 +41,24 @@ class AvailableDeviceResources(BECWidget, QWidget, Ui_availableDeviceResources): self.tag_groups_list.clear() self._items = {} for tag_group, devices in self._backend.tag_groups.items(): - item = QListWidgetItem(self.tag_groups_list) - tag_group_widget = DeviceTagGroup(self.tag_groups_list, tag_group, devices) - self.tag_groups_list.setItemWidget(item, tag_group_widget) - self.tag_groups_list.addItem(item) - self._items[tag_group] = (item, tag_group_widget) - item.setSizeHint(QSize(tag_group_widget.width(), tag_group_widget.height())) + self._add_tag_group(tag_group, devices) + self._add_tag_group("Untagged devices", self._backend.untagged_devices) + + def _add_tag_group(self, tag_group: str, devices: set[HashableDevice]): + item = QListWidgetItem(self.tag_groups_list) + tag_group_widget = DeviceTagGroup(self.tag_groups_list, tag_group, devices) + self.tag_groups_list.setItemWidget(item, tag_group_widget) + self.tag_groups_list.addItem(item) + self._items[tag_group] = (item, tag_group_widget) + item.setSizeHint(QSize(tag_group_widget.width(), tag_group_widget.height())) + + def _reset_devices_state(self): + for _, tag_group in self._items.values(): + tag_group.reset_devices_state() def set_devices_state(self, devices: Iterable[HashableDevice], included: bool): - for _, tag_group in self._items.values(): - for device in devices: + for device in devices: + for _, tag_group in self._items.values(): tag_group.set_item_state(hash(device), included) def resizeEvent(self, event): diff --git a/bec_widgets/widgets/control/device_manager/components/available_device_resources/device_resource_backend.py b/bec_widgets/widgets/control/device_manager/components/available_device_resources/device_resource_backend.py index 1e8854c8..66e69c15 100644 --- a/bec_widgets/widgets/control/device_manager/components/available_device_resources/device_resource_backend.py +++ b/bec_widgets/widgets/control/device_manager/components/available_device_resources/device_resource_backend.py @@ -88,6 +88,11 @@ class DeviceResourceBackend(Protocol): """A set of all availble devices. The same device may not appear more than once.""" ... + @property + def untagged_devices(self) -> set[HashableDevice]: + """A set of all untagged devices. The same device may not appear more than once.""" + ... + def tags(self) -> set[str]: """Returns a set of all the tags in all available devices.""" ... @@ -97,26 +102,33 @@ class DeviceResourceBackend(Protocol): ... +def _devices_from_file(file: str, include_source: bool = True): + data = yaml_load(file, process_includes=False) + return _HashableDeviceSet( + HashableDevice.model_validate( + dev | {"name": name, "source_files": {file} if include_source else set()} + ) + for name, dev in data.items() + ) + + class _ConfigFileBackend(DeviceResourceBackend): def __init__(self) -> None: - self._raw_device_set: set[HashableDevice] = self._get_config_from_files( + self._raw_device_set: set[ + HashableDevice + ] = self._get_config_from_backup_file() | self._get_configs_from_plugin_files( Path(plugin_repo_path()) / plugin_package_name() / "device_configs/" ) self._tag_groups = self._get_tag_groups() - def _get_config_from_files(self, dir: Path): + def _get_config_from_backup_file(self): + return _devices_from_file( + "/home/perl_d/Development/bec/bec/logs/device_configs/recovery_configs/recovery_config_2025-08-22_14-02-29.yaml" + ) + + def _get_configs_from_plugin_files(self, dir: Path): files = glob("*.yaml", root_dir=dir, recursive=True) - - def devices_from_file(file: str): - data = yaml_load(str(dir / file)) - return set( - HashableDevice.model_validate( - dev | {"name": name, "source_files": {str(dir / file)}} - ) - for name, dev in data.items() - ) - - return reduce(operator.or_, map(devices_from_file, files)) + return reduce(operator.or_, map(_devices_from_file, (str(dir / f) for f in files))) def _get_tag_groups(self) -> dict[str, set[HashableDevice]]: return { @@ -132,6 +144,10 @@ class _ConfigFileBackend(DeviceResourceBackend): def all_devices(self): return self._raw_device_set + @property + def untagged_devices(self): + return {d for d in self._raw_device_set if d.deviceTags == set()} + def tags(self) -> set[str]: return reduce(operator.or_, (dev.deviceTags for dev in self._raw_device_set)) diff --git a/bec_widgets/widgets/control/device_manager/components/available_device_resources/device_tag_group.py b/bec_widgets/widgets/control/device_manager/components/available_device_resources/device_tag_group.py index 8dcfc5ee..d19b8709 100644 --- a/bec_widgets/widgets/control/device_manager/components/available_device_resources/device_tag_group.py +++ b/bec_widgets/widgets/control/device_manager/components/available_device_resources/device_tag_group.py @@ -1,5 +1,4 @@ -from textwrap import dedent -from typing import Callable, NamedTuple +from typing import NamedTuple from bec_qthemes import material_icon from qtpy.QtCore import QSize @@ -15,6 +14,20 @@ from bec_widgets.widgets.control.device_manager.components.available_device_reso DEVICE_HASH_ROLE = 101 +def _warning_string(spec: HashableDevice): + name_warning = ( + f"Device defined with multiple names! Please check:\n {'\n '.join(spec.names)}\n" + if len(spec.names) > 1 + else "" + ) + source_warning = ( + f"Device found in multiple source files! Please check:\n {'\n '.join(spec.source_files)}" + if len(spec.source_files) > 1 + else "" + ) + return f"{name_warning}{source_warning}" + + class _DeviceEntryWidget(QFrame): _grid_size = QSize(120, 80) @@ -32,6 +45,7 @@ class _DeviceEntryWidget(QFrame): self.setMinimumSize(self._grid_size) self.setup_title_layout(device_spec) + self.check_and_display_warning() self.setToolTip(device_spec.rich_text()) @@ -42,19 +56,18 @@ class _DeviceEntryWidget(QFrame): def setup_title_layout(self, device_spec: HashableDevice): self._title_layout = QHBoxLayout() + self._title_layout.setContentsMargins(0, 0, 0, 0) self._title_container = QWidget(parent=self) self._title_container.setLayout(self._title_layout) + self._warning_label = QLabel() + self._title_layout.addWidget(self._warning_label) + self.title = QLabel(device_spec.name) self.title.setToolTip(device_spec.name) self.title.setStyleSheet(self.title_style("#FF0000")) self._title_layout.addWidget(self.title) - self._title_layout.addStretch(1) - - self._warning_label = QLabel() - self._title_layout.addWidget(self._warning_label) - self._layout.addWidget(self._title_container) def check_and_display_warning(self): @@ -62,15 +75,8 @@ class _DeviceEntryWidget(QFrame): self._warning_label.setText("") self._warning_label.setToolTip("") else: - self._warning_label.setPixmap(material_icon("warning", color="#FFAA00")) - self._warning_label.setToolTip( - dedent( - f""" - {f"Device has multiple names! Please check! \n names: {self._device_spec.names}" if len(self._device_spec.names)>1 else ""} - {f"Device found in multiple source files! Please check! \n files: {self._device_spec.names}" if len(self._device_spec.names)>1 else ""} - """ - ) - ) + self._warning_label.setPixmap(material_icon("warning", size=(12, 12), color="#FFAA00")) + self._warning_label.setToolTip(_warning_string(self._device_spec)) @property def device_hash(self): @@ -104,6 +110,7 @@ class DeviceTagGroup(QWidget, Ui_DeviceTagGroup): for device in data: self._add_item(device) self.device_list.sortItems() + self._update_num_included() self.add_to_composition_button.clicked.connect(self.test) @@ -115,6 +122,11 @@ class DeviceTagGroup(QWidget, Ui_DeviceTagGroup): self.device_list.addItem(item) self._devices[device.name] = _DeviceEntry(item, widget) + def reset_devices_state(self): + for dev in self._devices.values(): + dev.widget.set_included(False) + self._update_num_included() + def set_item_state(self, /, device_hash: int, included: bool): for dev in self._devices.values(): if dev.widget.device_hash == device_hash: diff --git a/bec_widgets/widgets/control/device_manager/components/device_table_view.py b/bec_widgets/widgets/control/device_manager/components/device_table_view.py index 40f31cec..cfa1c082 100644 --- a/bec_widgets/widgets/control/device_manager/components/device_table_view.py +++ b/bec_widgets/widgets/control/device_manager/components/device_table_view.py @@ -115,6 +115,9 @@ class DeviceTableModel(QtCore.QAbstractTableModel): Sort logic is implemented directly on the data of the table view. """ + device_added = QtCore.Signal(dict) + devices_reset = QtCore.Signal(list) + def __init__(self, device_config: list[dict] | None = None, parent=None): super().__init__(parent) self._device_config = device_config or [] @@ -250,6 +253,7 @@ class DeviceTableModel(QtCore.QAbstractTableModel): self.beginResetModel() self._device_config = list(device_config) self.endResetModel() + self.devices_reset.emit(self._device_config) @SafeSlot(dict) def add_device(self, device: dict):