From 51f54ca22e61ad9adf789a4bdf94078270b263f7 Mon Sep 17 00:00:00 2001 From: wyzula-jan Date: Mon, 1 Jul 2024 16:05:39 +0200 Subject: [PATCH] feat(motor_widget): Motor Widget with toolbar WIP --- .../plots/motor_map/assets/connection.svg | 4 + .../plots/motor_map/assets/settings.svg | 4 + .../plots/motor_map/motor_map_widget.py | 117 ++++++++++++++ bec_widgets/widgets/toolbar/toolbar.py | 148 ++++++------------ 4 files changed, 175 insertions(+), 98 deletions(-) create mode 100644 bec_widgets/widgets/figure/plots/motor_map/assets/connection.svg create mode 100644 bec_widgets/widgets/figure/plots/motor_map/assets/settings.svg create mode 100644 bec_widgets/widgets/figure/plots/motor_map/motor_map_widget.py diff --git a/bec_widgets/widgets/figure/plots/motor_map/assets/connection.svg b/bec_widgets/widgets/figure/plots/motor_map/assets/connection.svg new file mode 100644 index 00000000..3b31c422 --- /dev/null +++ b/bec_widgets/widgets/figure/plots/motor_map/assets/connection.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/bec_widgets/widgets/figure/plots/motor_map/assets/settings.svg b/bec_widgets/widgets/figure/plots/motor_map/assets/settings.svg new file mode 100644 index 00000000..9ce6f411 --- /dev/null +++ b/bec_widgets/widgets/figure/plots/motor_map/assets/settings.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/bec_widgets/widgets/figure/plots/motor_map/motor_map_widget.py b/bec_widgets/widgets/figure/plots/motor_map/motor_map_widget.py new file mode 100644 index 00000000..5e785a78 --- /dev/null +++ b/bec_widgets/widgets/figure/plots/motor_map/motor_map_widget.py @@ -0,0 +1,117 @@ +import os + +from qtpy.QtCore import QSize, Slot +from qtpy.QtGui import QAction, QIcon +from qtpy.QtWidgets import QHBoxLayout, QLabel, QVBoxLayout, QWidget + +from bec_widgets.utils import BECConnector +from bec_widgets.widgets.device_inputs import DeviceComboBox +from bec_widgets.widgets.figure import BECFigure +from bec_widgets.widgets.figure.plots.motor_map.motor_map import MotorMapConfig +from bec_widgets.widgets.toolbar import ModularToolBar +from bec_widgets.widgets.toolbar.toolbar import ToolBarAction + + +class SettingsAction(ToolBarAction): + def add_to_toolbar(self, toolbar, target): + current_path = os.path.dirname(__file__) + icon = QIcon() + icon.addFile(os.path.join(current_path, "assets", "settings.svg"), size=QSize(20, 20)) + action = QAction(icon, "Config", target) + action.triggered.connect(lambda: print(target.config_dict)) + toolbar.addAction(action) + + +class DeviceSelectionAction(ToolBarAction): + def __init__(self, label: str): + self.label = label + self.device_combobox = DeviceComboBox(device_filter="Positioner") + + def add_to_toolbar(self, toolbar, target): + widget = QWidget() + layout = QHBoxLayout(widget) + + label = QLabel(f"{self.label}") + + layout.addWidget(label) + layout.addWidget(self.device_combobox) + toolbar.addWidget(widget) + + +class ConnectAction(ToolBarAction): + def add_to_toolbar(self, toolbar, target): + current_path = os.path.dirname(__file__) + icon = QIcon() + icon.addFile(os.path.join(current_path, "assets", "connection.svg"), size=QSize(20, 20)) + self.action = QAction(icon, "Connect Motors", target) + toolbar.addAction(self.action) + + +class BECMotorMapWidget(BECConnector, QWidget): + USER_ACCESS = [] + + def __init__( + self, + parent: QWidget | None = None, + config: MotorMapConfig | None = None, + client=None, + gui_id: str | None = None, + ) -> None: + if config is None: + config = MotorMapConfig(widget_class=self.__class__.__name__) + else: + if isinstance(config, dict): + config = MotorMapConfig(**config) + super().__init__(client=client, gui_id=gui_id) + QWidget.__init__(self, parent) + + self.layout = QVBoxLayout(self) + self.layout.setSpacing(0) + self.layout.setContentsMargins(0, 0, 0, 0) + + self.fig = BECFigure() + self.toolbar = ModularToolBar( + actions={ + "motor_x": DeviceSelectionAction("Motor X:"), + "motor_y": DeviceSelectionAction("Motor Y:"), + "connect": ConnectAction(), + "config": SettingsAction(), + }, + target_widget=self, + ) + + self.layout.addWidget(self.toolbar) + self.layout.addWidget(self.fig) + + self.map = self.fig.motor_map() + self.map.apply_config(config) + + self.config = config + + self._hook_actions() + + def _hook_actions(self): + self.toolbar.widgets["connect"].action.triggered.connect(self.pass_motors) + + def pass_motors(self): + motor_x = self.toolbar.widgets["motor_x"].device_combobox.currentText() + motor_y = self.toolbar.widgets["motor_y"].device_combobox.currentText() + self.change_motors(motor_x, motor_y) + + @Slot(str, str) + def change_motors(self, motor_x, motor_y): + self.map.change_motors(motor_x, motor_y) + + def set(self, **kwargs): + self.map.set(**kwargs) + + +if __name__ == "__main__": + import sys + + from PySide6.QtWidgets import QApplication + + app = QApplication(sys.argv) + widget = BECMotorMapWidget() + widget.show() + sys.exit(app.exec_()) diff --git a/bec_widgets/widgets/toolbar/toolbar.py b/bec_widgets/widgets/toolbar/toolbar.py index 1c3adc2c..badc01a9 100644 --- a/bec_widgets/widgets/toolbar/toolbar.py +++ b/bec_widgets/widgets/toolbar/toolbar.py @@ -1,4 +1,7 @@ from abc import ABC, abstractmethod +from collections import defaultdict + +from PySide6.QtWidgets import QHBoxLayout, QLabel, QSpinBox # pylint: disable=no-name-in-module from qtpy.QtCore import QSize, QTimer @@ -7,117 +10,63 @@ from qtpy.QtWidgets import QApplication, QStyle, QToolBar, QWidget class ToolBarAction(ABC): - """Abstract base class for action creators for the toolbar.""" - @abstractmethod - def create(self, target: QWidget): - """Creates and returns an action to be added to a toolbar. - - This method must be implemented by subclasses. + def add_to_toolbar(self, toolbar: QToolBar, target: QWidget): + """Adds an action or widget to a toolbar. Args: - target (QWidget): The widget that the action will target. - - Returns: - QAction: The action created for the toolbar. + toolbar (QToolBar): The toolbar to add the action or widget to. + target (QWidget): The target widget for the action. """ -class OpenFileAction: # (ToolBarAction): - """Action creator for the 'Open File' action in the toolbar.""" +class ColumnAdjustAction(ToolBarAction): + """Toolbar spinbox to adjust number of columns in the plot layout""" - def create(self, target: QWidget): - """Creates an 'Open File' action for the toolbar. + def add_to_toolbar(self, toolbar: QToolBar, target: QWidget): + """Creates a access history button for the toolbar. Args: - target (QWidget): The widget that the 'Open File' action will be targeted. + toolbar (QToolBar): The toolbar to add the action to. + target (QWidget): The widget that the 'Access Scan History' action will be targeted. Returns: - QAction: The 'Open File' action created for the toolbar. + QAction: The 'Access Scan History' action created for the toolbar. """ - icon = QApplication.style().standardIcon(QStyle.StandardPixmap.SP_DialogOpenButton) - action = QAction(icon, "Open File", target) - # action = QAction("Open File", target) - action.triggered.connect(target.open_file) - return action + widget = QWidget() + layout = QHBoxLayout(widget) + label = QLabel("Columns:") + spin_box = QSpinBox() + spin_box.setMinimum(1) # Set minimum value + spin_box.setMaximum(10) # Set maximum value + spin_box.setValue(target.get_column_count()) # Initial value + spin_box.valueChanged.connect(lambda value: target.set_column_count(value)) -class SaveFileAction: - """Action creator for the 'Save File' action in the toolbar.""" - - def create(self, target): - """Creates a 'Save File' action for the toolbar. - - Args: - target (QWidget): The widget that the 'Save File' action will be targeted. - - Returns: - QAction: The 'Save File' action created for the toolbar. - """ - icon = QApplication.style().standardIcon(QStyle.StandardPixmap.SP_DialogSaveButton) - action = QAction(icon, "Save File", target) - # action = QAction("Save File", target) - action.triggered.connect(target.save_file) - return action - - -class RunScriptAction: - """Action creator for the 'Run Script' action in the toolbar.""" - - def create(self, target): - """Creates a 'Run Script' action for the toolbar. - - Args: - target (QWidget): The widget that the 'Run Script' action will be targeted. - - Returns: - QAction: The 'Run Script' action created for the toolbar. - """ - icon = QApplication.style().standardIcon(QStyle.StandardPixmap.SP_MediaPlay) - action = QAction(icon, "Run Script", target) - # action = QAction("Run Script", target) - action.triggered.connect(target.run_script) - return action + layout.addWidget(label) + layout.addWidget(spin_box) + toolbar.addWidget(widget) class ModularToolBar(QToolBar): """Modular toolbar with optional automatic initialization. - Args: parent (QWidget, optional): The parent widget of the toolbar. Defaults to None. auto_init (bool, optional): If True, automatically populates the toolbar based on the parent widget. """ - def __init__(self, parent=None, auto_init=True): + def __init__(self, parent=None, actions=None, target_widget=None): super().__init__(parent) - self.auto_init = auto_init - self.handler = { - "BECEditor": [OpenFileAction(), SaveFileAction(), RunScriptAction()], - # BECMonitor: [SomeOtherAction(), AnotherAction()], # Example for another widget - } + self.setStyleSheet("QToolBar { background: transparent; }") - # Set the icon size for the toolbar self.setIconSize(QSize(20, 20)) + self.widgets = defaultdict(dict) - if self.auto_init: - QTimer.singleShot(0, self.auto_detect_and_populate) + if actions is not None and target_widget is not None: + self.populate_toolbar(actions, target_widget) + # QTimer.singleShot(0, lambda :self.set_manual_actions(actions, target_widget)) - def auto_detect_and_populate(self): - """Automatically detects the parent widget and populates the toolbar with relevant actions.""" - if not self.auto_init: - return - - parent_widget = self.parent() - if parent_widget is None: - return - - parent_widget_class_name = type(parent_widget).__name__ - for widget_type_name, actions in self.handler.items(): - if parent_widget_class_name == widget_type_name: - self.populate_toolbar(actions, parent_widget) - return - - def populate_toolbar(self, actions, target_widget): + def populate_toolbar(self, actions: dict, target_widget): """Populates the toolbar with a set of actions. Args: @@ -125,20 +74,23 @@ class ModularToolBar(QToolBar): target_widget (QWidget): The widget that the actions will target. """ self.clear() - for action_creator in actions: - action = action_creator.create(target_widget) - self.addAction(action) + for action_id, action in actions.items(): + action.add_to_toolbar(self, target_widget) + self.widgets[action_id] = action - def set_manual_actions(self, actions, target_widget): - """Manually sets the actions for the toolbar. + # for action in actions: + # action.add_to_toolbar(self, target_widget) - Args: - actions (list[QAction or ToolBarAction]): A list of actions or action creators to populate the toolbar. - target_widget (QWidget): The widget that the actions will target. - """ - self.clear() - for action in actions: - if isinstance(action, QAction): - self.addAction(action) - elif isinstance(action, ToolBarAction): - self.addAction(action.create(target_widget)) + # def set_manual_actions(self, actions, target_widget): + # """Manually sets the actions for the toolbar. + # + # Args: + # actions (list[QAction or ToolBarAction]): A list of actions or action creators to populate the toolbar. + # target_widget (QWidget): The widget that the actions will target. + # """ + # self.clear() + # for action in actions: + # if isinstance(action, QAction): + # self.addAction(action) + # elif isinstance(action, ToolBarAction): + # self.addAction(action.add_to_toolbar(self, target_widget))