0
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2025-07-13 19:21:50 +02:00

feat: new PositionerGroup widget

This commit is contained in:
2024-10-11 19:23:55 +02:00
parent e4121a01cb
commit af9655de0c
5 changed files with 243 additions and 0 deletions

View File

@ -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_())

View File

@ -0,0 +1 @@
{'files': ['positioner_group.py']}

View File

@ -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 = """
<ui language='c++'>
<widget class='PositionerGroup' name='positioner_group'>
</widget>
</ui>
"""
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()

View File

@ -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()