From af9655de0c541092437accfbaa779628a2f48ccb Mon Sep 17 00:00:00 2001 From: Mathias Guijarro Date: Fri, 11 Oct 2024 19:23:55 +0200 Subject: [PATCH] feat: new PositionerGroup widget --- .../widgets/positioner_group/__init__.py | 0 .../positioner_group/positioner_group.py | 170 ++++++++++++++++++ .../positioner_group.pyproject | 1 + .../positioner_group_plugin.py | 57 ++++++ .../register_positioner_group.py | 15 ++ 5 files changed, 243 insertions(+) create mode 100644 bec_widgets/widgets/positioner_group/__init__.py create mode 100644 bec_widgets/widgets/positioner_group/positioner_group.py create mode 100644 bec_widgets/widgets/positioner_group/positioner_group.pyproject create mode 100644 bec_widgets/widgets/positioner_group/positioner_group_plugin.py create mode 100644 bec_widgets/widgets/positioner_group/register_positioner_group.py diff --git a/bec_widgets/widgets/positioner_group/__init__.py b/bec_widgets/widgets/positioner_group/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/bec_widgets/widgets/positioner_group/positioner_group.py b/bec_widgets/widgets/positioner_group/positioner_group.py new file mode 100644 index 00000000..f287ad30 --- /dev/null +++ b/bec_widgets/widgets/positioner_group/positioner_group.py @@ -0,0 +1,170 @@ +""" Module for a PositionerGroup widget to control a positioner device.""" + +from __future__ import annotations + +from bec_lib.device import Positioner +from bec_lib.logger import bec_logger +from qtpy.QtCore import Property, QSize, Signal, Slot +from qtpy.QtWidgets import QGridLayout, QGroupBox, QSizePolicy, QVBoxLayout, QWidget + +from bec_widgets.utils.bec_widget import BECWidget +from bec_widgets.widgets.positioner_box.positioner_box import PositionerBox + +logger = bec_logger.logger + + +class PositionerGroupBox(QGroupBox): + + position_update = Signal(float) + + def __init__(self, parent, dev_name): + super().__init__(parent) + + self.device_name = dev_name + + QVBoxLayout(self) + self.layout().setContentsMargins(0, 0, 0, 0) + self.layout().setSpacing(0) + self.widget = PositionerBox(self, dev_name) + self.widget.compact_view = True + self.widget.expand_popup = False + self.layout().addWidget(self.widget) + self.widget.position_update.connect(self._on_position_update) + self.widget.expand.connect(self._on_expand) + self.setTitle(self.device_name) + self.widget.init_device() # force readback + + def _on_expand(self, expand): + if expand: + self.setTitle("") + self.setFlat(True) + else: + self.setTitle(self.device_name) + self.setFlat(False) + + def _on_position_update(self, pos: float): + self.position_update.emit(pos) + self.widget.label = f"%.{self.widget.dev[self.widget.device].precision}f" % pos + + def close(self): + self.widget.close() + super().close() + + +class PositionerGroup(BECWidget, QWidget): + """Simple Widget to control a positioner in box form""" + + ICON_NAME = "grid_view" + USER_ACCESS = ["set_positioners"] + + # Signal emitted to inform listeners about a position update of the first positioner + position_update = Signal(float) + # Signal emitted to inform listeners about (positioner, pos) updates + device_position_update = Signal(str, float) + + def __init__(self, parent=None, **kwargs): + """Initialize the widget. + + Args: + parent: The parent widget. + """ + super().__init__(**kwargs) + QWidget.__init__(self, parent) + + self.get_bec_shortcuts() + + QGridLayout(self) + self.layout().setContentsMargins(0, 0, 0, 0) + + self._device_widgets = {} + self._grid_ncols = 2 + + def minimumSizeHint(self): + return QSize(300, 30) + + @Slot(str) + def set_positioners(self, device_names: str): + """Redraw grid with positioners from device_names string + + Device names must be separated by space + """ + devs = device_names.split() + for dev_name in devs: + if not self._check_device_is_valid(dev_name): + raise ValueError(f"{dev_name} is not a valid Positioner") + for i, existing_widget in enumerate(self._device_widgets.values()): + self.layout().removeWidget(existing_widget) + existing_widget.position_update.disconnect(self._on_position_update) + if i == 0: + existing_widget.position_update.disconnect(self.position_update) + for i, dev_name in enumerate(devs): + widget = self._device_widgets.get(dev_name) + if widget is None: + widget = PositionerGroupBox(self, dev_name) + self._device_widgets[dev_name] = widget + widget.position_update.connect(self._on_position_update) + if i == 0: + # only emit 'position_update' for the first positioner in grid + widget.position_update.connect(self.position_update) + self.layout().addWidget(widget, i // self._grid_ncols, i % self._grid_ncols) + to_remove = set(self._device_widgets) - set(devs) + for dev_name in to_remove: + self._device_widgets[dev_name].close() + del self._device_widgets[dev_name] + + def _check_device_is_valid(self, device: str): + """Check if the device is a positioner + + Args: + device (str): The device name + """ + if device not in self.dev: + logger.info(f"Device {device} not found in the device list") + return False + if not isinstance(self.dev[device], Positioner): + logger.info(f"Device {device} is not a positioner") + return False + return True + + def _on_position_update(self, pos: float): + widget = self.sender() + self.device_position_update.emit(widget.title(), pos) + + @Property(str) + def devices_list(self): + """Device names string separated by space""" + return " ".join(self._device_widgets) + + @devices_list.setter + def devices_list(self, device_names: str): + """Set devices list from device names string separated by space""" + devs = device_names.split() + for dev_name in devs: + if not self._check_device_is_valid(dev_name): + return + self.set_positioners(device_names) + + @Property(int) + def grid_max_cols(self): + """Max number of columns for widgets grid""" + return self._grid_ncols + + @grid_max_cols.setter + def grid_max_cols(self, ncols: int): + """Set max number of columns for widgets grid""" + self._grid_ncols = ncols + self.set_positioners(self.devices_list) + + +if __name__ == "__main__": # pragma: no cover + import sys + + from qtpy.QtWidgets import QApplication # pylint: disable=ungrouped-imports + + app = QApplication(sys.argv) + widget = PositionerGroup() + widget.grid_max_cols = 3 + widget.set_positioners("samx samy samz") + + widget.show() + sys.exit(app.exec_()) diff --git a/bec_widgets/widgets/positioner_group/positioner_group.pyproject b/bec_widgets/widgets/positioner_group/positioner_group.pyproject new file mode 100644 index 00000000..58b1a143 --- /dev/null +++ b/bec_widgets/widgets/positioner_group/positioner_group.pyproject @@ -0,0 +1 @@ +{'files': ['positioner_group.py']} diff --git a/bec_widgets/widgets/positioner_group/positioner_group_plugin.py b/bec_widgets/widgets/positioner_group/positioner_group_plugin.py new file mode 100644 index 00000000..635d238a --- /dev/null +++ b/bec_widgets/widgets/positioner_group/positioner_group_plugin.py @@ -0,0 +1,57 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import os + +from qtpy.QtDesigner import QDesignerCustomWidgetInterface + +from bec_widgets.utils.bec_designer import designer_material_icon +from bec_widgets.widgets.positioner_group.positioner_group import PositionerGroup + +DOM_XML = """ + + + + +""" +MODULE_PATH = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) + + +class PositionerGroupPlugin(QDesignerCustomWidgetInterface): # pragma: no cover + def __init__(self): + super().__init__() + self._form_editor = None + + def createWidget(self, parent): + t = PositionerGroup(parent) + return t + + def domXml(self): + return DOM_XML + + def group(self): + return "Device Control" + + def icon(self): + return designer_material_icon(PositionerGroup.ICON_NAME) + + def includeFile(self): + return "positioner_group" + + def initialize(self, form_editor): + self._form_editor = form_editor + + def isContainer(self): + return False + + def isInitialized(self): + return self._form_editor is not None + + def name(self): + return "PositionerGroup" + + def toolTip(self): + return "Container Widget to control positioners in compact form, in a grid" + + def whatsThis(self): + return self.toolTip() diff --git a/bec_widgets/widgets/positioner_group/register_positioner_group.py b/bec_widgets/widgets/positioner_group/register_positioner_group.py new file mode 100644 index 00000000..97e07d5d --- /dev/null +++ b/bec_widgets/widgets/positioner_group/register_positioner_group.py @@ -0,0 +1,15 @@ +def main(): # pragma: no cover + from qtpy import PYSIDE6 + + if not PYSIDE6: + print("PYSIDE6 is not available in the environment. Cannot patch designer.") + return + from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection + + from bec_widgets.widgets.positioner_group.positioner_group_plugin import PositionerGroupPlugin + + QPyDesignerCustomWidgetCollection.addCustomWidget(PositionerGroupPlugin()) + + +if __name__ == "__main__": # pragma: no cover + main()