mirror of
https://github.com/bec-project/bec_widgets.git
synced 2025-07-14 03:31:50 +02:00
feat: new PositionerGroup widget
This commit is contained in:
0
bec_widgets/widgets/positioner_group/__init__.py
Normal file
0
bec_widgets/widgets/positioner_group/__init__.py
Normal file
170
bec_widgets/widgets/positioner_group/positioner_group.py
Normal file
170
bec_widgets/widgets/positioner_group/positioner_group.py
Normal 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_())
|
@ -0,0 +1 @@
|
|||||||
|
{'files': ['positioner_group.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 = """
|
||||||
|
<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()
|
@ -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()
|
Reference in New Issue
Block a user