From 9488923381eb1ea1109751e68f0d00a1b0f584e2 Mon Sep 17 00:00:00 2001 From: wyzula-jan Date: Thu, 7 Aug 2025 16:05:43 +0200 Subject: [PATCH] feat(bec_widget): attach/detach method for all widgets + client regenerated --- bec_widgets/cli/client.py | 359 +++++++++++++++++- bec_widgets/utils/bec_widget.py | 24 +- .../positioner_box/positioner_box.py | 2 +- .../positioner_box_2d/positioner_box_2d.py | 2 +- .../positioner_group/positioner_group.py | 2 +- .../control/scan_control/scan_control.py | 2 +- .../widgets/editors/monaco/monaco_widget.py | 3 + .../widgets/editors/website/website.py | 11 +- bec_widgets/widgets/plots/heatmap/heatmap.py | 2 + bec_widgets/widgets/plots/image/image.py | 2 + .../widgets/plots/motor_map/motor_map.py | 2 + .../plots/multi_waveform/multi_waveform.py | 2 + .../scatter_waveform/scatter_waveform.py | 2 + .../widgets/plots/waveform/waveform.py | 5 +- .../ring_progress_bar/ring_progress_bar.py | 3 + .../services/bec_status_box/bec_status_box.py | 2 +- 16 files changed, 408 insertions(+), 17 deletions(-) diff --git a/bec_widgets/cli/client.py b/bec_widgets/cli/client.py index 09195255..2b05c71f 100644 --- a/bec_widgets/cli/client.py +++ b/bec_widgets/cli/client.py @@ -106,6 +106,18 @@ class AbortButton(RPCBase): Cleanup the BECConnector """ + @rpc_call + def attach(self): + """ + None + """ + + @rpc_call + def detach(self): + """ + Detach the widget from its parent dock widget (if widget is in the dock), making it a floating widget. + """ + class AutoUpdates(RPCBase): @property @@ -442,6 +454,18 @@ class BECMainWindow(RPCBase): Cleanup the BECConnector """ + @rpc_call + def attach(self): + """ + None + """ + + @rpc_call + def detach(self): + """ + Detach the widget from its parent dock widget (if widget is in the dock), making it a floating widget. + """ + class BECProgressBar(RPCBase): """A custom progress bar with smooth transitions. The displayed text can be customized using a template.""" @@ -525,6 +549,18 @@ class BECQueue(RPCBase): Cleanup the BECConnector """ + @rpc_call + def attach(self): + """ + None + """ + + @rpc_call + def detach(self): + """ + Detach the widget from its parent dock widget (if widget is in the dock), making it a floating widget. + """ + class BECStatusBox(RPCBase): """An autonomous widget to display the status of BEC services.""" @@ -541,6 +577,25 @@ class BECStatusBox(RPCBase): Cleanup the BECConnector """ + @rpc_call + def attach(self): + """ + None + """ + + @rpc_call + def detach(self): + """ + Detach the widget from its parent dock widget (if widget is in the dock), making it a floating widget. + """ + + @rpc_timeout(None) + @rpc_call + def screenshot(self, file_name: "str | None" = None): + """ + Take a screenshot of the dock area and save it to a file. + """ + class BaseROI(RPCBase): """Base class for all Region of Interest (ROI) implementations.""" @@ -1002,6 +1057,18 @@ class DarkModeButton(RPCBase): Cleanup the BECConnector """ + @rpc_call + def attach(self): + """ + None + """ + + @rpc_call + def detach(self): + """ + Detach the widget from its parent dock widget (if widget is in the dock), making it a floating widget. + """ + class DeviceBrowser(RPCBase): """DeviceBrowser is a widget that displays all available devices in the current BEC session.""" @@ -1012,6 +1079,18 @@ class DeviceBrowser(RPCBase): Cleanup the BECConnector """ + @rpc_call + def attach(self): + """ + None + """ + + @rpc_call + def detach(self): + """ + Detach the widget from its parent dock widget (if widget is in the dock), making it a floating widget. + """ + class DeviceComboBox(RPCBase): """Combobox widget for device input with autocomplete for device names.""" @@ -1045,6 +1124,18 @@ class DeviceInputBase(RPCBase): Cleanup the BECConnector """ + @rpc_call + def attach(self): + """ + None + """ + + @rpc_call + def detach(self): + """ + Detach the widget from its parent dock widget (if widget is in the dock), making it a floating widget. + """ + class DeviceLineEdit(RPCBase): """Line edit widget for device input with autocomplete for device names.""" @@ -1433,6 +1524,18 @@ class Heatmap(RPCBase): Minimum decimal places for crosshair when dynamic precision is enabled. """ + @rpc_call + def attach(self): + """ + None + """ + + @rpc_call + def detach(self): + """ + Detach the widget from its parent dock widget (if widget is in the dock), making it a floating widget. + """ + @rpc_timeout(None) @rpc_call def screenshot(self, file_name: "str | None" = None): @@ -1978,6 +2081,18 @@ class Image(RPCBase): Minimum decimal places for crosshair when dynamic precision is enabled. """ + @rpc_call + def attach(self): + """ + None + """ + + @rpc_call + def detach(self): + """ + Detach the widget from its parent dock widget (if widget is in the dock), making it a floating widget. + """ + @rpc_timeout(None) @rpc_call def screenshot(self, file_name: "str | None" = None): @@ -2590,6 +2705,25 @@ class MonacoWidget(RPCBase): str: The LSP header. """ + @rpc_call + def attach(self): + """ + None + """ + + @rpc_call + def detach(self): + """ + Detach the widget from its parent dock widget (if widget is in the dock), making it a floating widget. + """ + + @rpc_timeout(None) + @rpc_call + def screenshot(self, file_name: "str | None" = None): + """ + Take a screenshot of the dock area and save it to a file. + """ + class MotorMap(RPCBase): """Motor map widget for plotting motor positions in 2D including a trace of the last points.""" @@ -2865,6 +2999,18 @@ class MotorMap(RPCBase): The font size of the legend font. """ + @rpc_call + def attach(self): + """ + None + """ + + @rpc_call + def detach(self): + """ + Detach the widget from its parent dock widget (if widget is in the dock), making it a floating widget. + """ + @rpc_timeout(None) @rpc_call def screenshot(self, file_name: "str | None" = None): @@ -3277,6 +3423,18 @@ class MultiWaveform(RPCBase): Minimum decimal places for crosshair when dynamic precision is enabled. """ + @rpc_call + def attach(self): + """ + None + """ + + @rpc_call + def detach(self): + """ + Detach the widget from its parent dock widget (if widget is in the dock), making it a floating widget. + """ + @rpc_timeout(None) @rpc_call def screenshot(self, file_name: "str | None" = None): @@ -3498,6 +3656,18 @@ class PositionerBox(RPCBase): positioner (Positioner | str) : Positioner to set, accepts str or the device """ + @rpc_call + def attach(self): + """ + None + """ + + @rpc_call + def detach(self): + """ + Detach the widget from its parent dock widget (if widget is in the dock), making it a floating widget. + """ + @rpc_timeout(None) @rpc_call def screenshot(self, file_name: "str | None" = None): @@ -3527,6 +3697,18 @@ class PositionerBox2D(RPCBase): positioner (Positioner | str) : Positioner to set, accepts str or the device """ + @rpc_call + def attach(self): + """ + None + """ + + @rpc_call + def detach(self): + """ + Detach the widget from its parent dock widget (if widget is in the dock), making it a floating widget. + """ + @rpc_timeout(None) @rpc_call def screenshot(self, file_name: "str | None" = None): @@ -3547,6 +3729,18 @@ class PositionerControlLine(RPCBase): positioner (Positioner | str) : Positioner to set, accepts str or the device """ + @rpc_call + def attach(self): + """ + None + """ + + @rpc_call + def detach(self): + """ + Detach the widget from its parent dock widget (if widget is in the dock), making it a floating widget. + """ + @rpc_timeout(None) @rpc_call def screenshot(self, file_name: "str | None" = None): @@ -3566,6 +3760,25 @@ class PositionerGroup(RPCBase): Device names must be separated by space """ + @rpc_call + def attach(self): + """ + None + """ + + @rpc_call + def detach(self): + """ + Detach the widget from its parent dock widget (if widget is in the dock), making it a floating widget. + """ + + @rpc_timeout(None) + @rpc_call + def screenshot(self, file_name: "str | None" = None): + """ + Take a screenshot of the dock area and save it to a file. + """ + class RectangularROI(RPCBase): """Defines a rectangular Region of Interest (ROI) with additional functionality.""" @@ -3705,6 +3918,18 @@ class ResetButton(RPCBase): Cleanup the BECConnector """ + @rpc_call + def attach(self): + """ + None + """ + + @rpc_call + def detach(self): + """ + Detach the widget from its parent dock widget (if widget is in the dock), making it a floating widget. + """ + class ResumeButton(RPCBase): """A button that continue scan queue.""" @@ -3715,6 +3940,18 @@ class ResumeButton(RPCBase): Cleanup the BECConnector """ + @rpc_call + def attach(self): + """ + None + """ + + @rpc_call + def detach(self): + """ + Detach the widget from its parent dock widget (if widget is in the dock), making it a floating widget. + """ + class Ring(RPCBase): @rpc_call @@ -3996,6 +4233,25 @@ class RingProgressBar(RPCBase): bool: True if scan segment updates are enabled. """ + @rpc_call + def attach(self): + """ + None + """ + + @rpc_call + def detach(self): + """ + Detach the widget from its parent dock widget (if widget is in the dock), making it a floating widget. + """ + + @rpc_timeout(None) + @rpc_call + def screenshot(self, file_name: "str | None" = None): + """ + Take a screenshot of the dock area and save it to a file. + """ + class SBBMonitor(RPCBase): """A widget to display the SBB monitor website.""" @@ -4007,9 +4263,15 @@ class ScanControl(RPCBase): """Widget to submit new scans to the queue.""" @rpc_call - def remove(self): + def attach(self): """ - Cleanup the BECConnector + None + """ + + @rpc_call + def detach(self): + """ + Detach the widget from its parent dock widget (if widget is in the dock), making it a floating widget. """ @rpc_timeout(None) @@ -4029,6 +4291,18 @@ class ScanProgressBar(RPCBase): Cleanup the BECConnector """ + @rpc_call + def attach(self): + """ + None + """ + + @rpc_call + def detach(self): + """ + Detach the widget from its parent dock widget (if widget is in the dock), making it a floating widget. + """ + class ScatterCurve(RPCBase): """Scatter curve item for the scatter waveform widget.""" @@ -4327,6 +4601,18 @@ class ScatterWaveform(RPCBase): Minimum decimal places for crosshair when dynamic precision is enabled. """ + @rpc_call + def attach(self): + """ + None + """ + + @rpc_call + def detach(self): + """ + Detach the widget from its parent dock widget (if widget is in the dock), making it a floating widget. + """ + @rpc_timeout(None) @rpc_call def screenshot(self, file_name: "str | None" = None): @@ -4629,6 +4915,18 @@ class StopButton(RPCBase): Cleanup the BECConnector """ + @rpc_call + def attach(self): + """ + None + """ + + @rpc_call + def detach(self): + """ + Detach the widget from its parent dock widget (if widget is in the dock), making it a floating widget. + """ + class TextBox(RPCBase): """A widget that displays text in plain and HTML format""" @@ -4661,6 +4959,25 @@ class VSCodeEditor(RPCBase): class Waveform(RPCBase): """Widget for plotting waveforms.""" + @rpc_call + def attach(self): + """ + None + """ + + @rpc_call + def detach(self): + """ + Detach the widget from its parent dock widget (if widget is in the dock), making it a floating widget. + """ + + @rpc_timeout(None) + @rpc_call + def screenshot(self, file_name: "str | None" = None): + """ + Take a screenshot of the dock area and save it to a file. + """ + @property @rpc_call def _config_dict(self) -> "dict": @@ -4965,13 +5282,6 @@ class Waveform(RPCBase): Minimum decimal places for crosshair when dynamic precision is enabled. """ - @rpc_timeout(None) - @rpc_call - def screenshot(self, file_name: "str | None" = None): - """ - Take a screenshot of the dock area and save it to a file. - """ - @property @rpc_call def curves(self) -> "list[Curve]": @@ -5213,6 +5523,18 @@ class WebConsole(RPCBase): Cleanup the BECConnector """ + @rpc_call + def attach(self): + """ + None + """ + + @rpc_call + def detach(self): + """ + Detach the widget from its parent dock widget (if widget is in the dock), making it a floating widget. + """ + class WebsiteWidget(RPCBase): """A simple widget to display a website""" @@ -5252,3 +5574,22 @@ class WebsiteWidget(RPCBase): """ Go forward in the history """ + + @rpc_call + def attach(self): + """ + None + """ + + @rpc_call + def detach(self): + """ + Detach the widget from its parent dock widget (if widget is in the dock), making it a floating widget. + """ + + @rpc_timeout(None) + @rpc_call + def screenshot(self, file_name: "str | None" = None): + """ + Take a screenshot of the dock area and save it to a file. + """ diff --git a/bec_widgets/utils/bec_widget.py b/bec_widgets/utils/bec_widget.py index ed58aeb2..dccd82ff 100644 --- a/bec_widgets/utils/bec_widget.py +++ b/bec_widgets/utils/bec_widget.py @@ -4,6 +4,7 @@ from datetime import datetime from typing import TYPE_CHECKING import darkdetect +import PySide6QtAds as QtAds import shiboken6 from bec_lib.logger import bec_logger from qtpy.QtCore import QObject @@ -14,6 +15,7 @@ from bec_widgets.utils.bec_connector import BECConnector, ConnectionConfig from bec_widgets.utils.colors import set_theme from bec_widgets.utils.error_popups import SafeSlot from bec_widgets.utils.rpc_decorator import rpc_timeout +from bec_widgets.utils.widget_io import WidgetHierarchy if TYPE_CHECKING: # pragma: no cover from bec_widgets.widgets.containers.dock import BECDock @@ -27,7 +29,7 @@ class BECWidget(BECConnector): # The icon name is the name of the icon in the icon theme, typically a name taken # from fonts.google.com/icons. Override this in subclasses to set the icon name. ICON_NAME = "widgets" - USER_ACCESS = ["remove"] + USER_ACCESS = ["remove", "attach", "detach"] # pylint: disable=too-many-arguments def __init__( @@ -124,6 +126,26 @@ class BECWidget(BECConnector): screenshot.save(file_name) logger.info(f"Screenshot saved to {file_name}") + def attach(self): + dock = WidgetHierarchy.find_ancestor(self, QtAds.CDockWidget) + if dock is None: + return + + if not dock.isFloating(): + return + dock.dockManager().addDockWidget(QtAds.DockWidgetArea.RightDockWidgetArea, dock) + + def detach(self): + """ + Detach the widget from its parent dock widget (if widget is in the dock), making it a floating widget. + """ + dock = WidgetHierarchy.find_ancestor(self, QtAds.CDockWidget) + if dock is None: + return + if dock.isFloating(): + return + dock.setFloating() + def cleanup(self): """Cleanup the widget.""" with RPCRegister.delayed_broadcast(): diff --git a/bec_widgets/widgets/control/device_control/positioner_box/positioner_box/positioner_box.py b/bec_widgets/widgets/control/device_control/positioner_box/positioner_box/positioner_box.py index 78cd5fa2..4a686d8c 100644 --- a/bec_widgets/widgets/control/device_control/positioner_box/positioner_box/positioner_box.py +++ b/bec_widgets/widgets/control/device_control/positioner_box/positioner_box/positioner_box.py @@ -33,7 +33,7 @@ class PositionerBox(PositionerBoxBase): PLUGIN = True RPC = True - USER_ACCESS = ["set_positioner", "screenshot"] + USER_ACCESS = ["set_positioner", "attach", "detach", "screenshot"] device_changed = Signal(str, str) # Signal emitted to inform listeners about a position update position_update = Signal(float) diff --git a/bec_widgets/widgets/control/device_control/positioner_box/positioner_box_2d/positioner_box_2d.py b/bec_widgets/widgets/control/device_control/positioner_box/positioner_box_2d/positioner_box_2d.py index 37bfc90b..7a8edd00 100644 --- a/bec_widgets/widgets/control/device_control/positioner_box/positioner_box_2d/positioner_box_2d.py +++ b/bec_widgets/widgets/control/device_control/positioner_box/positioner_box_2d/positioner_box_2d.py @@ -34,7 +34,7 @@ class PositionerBox2D(PositionerBoxBase): PLUGIN = True RPC = True - USER_ACCESS = ["set_positioner_hor", "set_positioner_ver", "screenshot"] + USER_ACCESS = ["set_positioner_hor", "set_positioner_ver", "attach", "detach", "screenshot"] device_changed_hor = Signal(str, str) device_changed_ver = Signal(str, str) diff --git a/bec_widgets/widgets/control/device_control/positioner_group/positioner_group.py b/bec_widgets/widgets/control/device_control/positioner_group/positioner_group.py index f3ac8892..e16c8371 100644 --- a/bec_widgets/widgets/control/device_control/positioner_group/positioner_group.py +++ b/bec_widgets/widgets/control/device_control/positioner_group/positioner_group.py @@ -62,7 +62,7 @@ class PositionerGroup(BECWidget, QWidget): PLUGIN = True ICON_NAME = "grid_view" - USER_ACCESS = ["set_positioners"] + USER_ACCESS = ["set_positioners", "attach", "detach", "screenshot"] # Signal emitted to inform listeners about a position update of the first positioner position_update = Signal(float) diff --git a/bec_widgets/widgets/control/scan_control/scan_control.py b/bec_widgets/widgets/control/scan_control/scan_control.py index 043250e3..27bad023 100644 --- a/bec_widgets/widgets/control/scan_control/scan_control.py +++ b/bec_widgets/widgets/control/scan_control/scan_control.py @@ -45,7 +45,7 @@ class ScanControl(BECWidget, QWidget): Widget to submit new scans to the queue. """ - USER_ACCESS = ["remove", "screenshot"] + USER_ACCESS = ["attach", "detach", "screenshot"] PLUGIN = True ICON_NAME = "tune" ARG_BOX_POSITION: int = 2 diff --git a/bec_widgets/widgets/editors/monaco/monaco_widget.py b/bec_widgets/widgets/editors/monaco/monaco_widget.py index 07600530..eb05cec7 100644 --- a/bec_widgets/widgets/editors/monaco/monaco_widget.py +++ b/bec_widgets/widgets/editors/monaco/monaco_widget.py @@ -32,6 +32,9 @@ class MonacoWidget(BECWidget, QWidget): "set_vim_mode_enabled", "set_lsp_header", "get_lsp_header", + "attach", + "detach", + "screenshot", ] def __init__(self, parent=None, config=None, client=None, gui_id=None, **kwargs): diff --git a/bec_widgets/widgets/editors/website/website.py b/bec_widgets/widgets/editors/website/website.py index 7839b789..fa9c8815 100644 --- a/bec_widgets/widgets/editors/website/website.py +++ b/bec_widgets/widgets/editors/website/website.py @@ -21,7 +21,16 @@ class WebsiteWidget(BECWidget, QWidget): PLUGIN = True ICON_NAME = "travel_explore" - USER_ACCESS = ["set_url", "get_url", "reload", "back", "forward"] + USER_ACCESS = [ + "set_url", + "get_url", + "reload", + "back", + "forward", + "attach", + "detach", + "screenshot", + ] def __init__( self, parent=None, url: str = None, config=None, client=None, gui_id=None, **kwargs diff --git a/bec_widgets/widgets/plots/heatmap/heatmap.py b/bec_widgets/widgets/plots/heatmap/heatmap.py index 3173806b..70312e1f 100644 --- a/bec_widgets/widgets/plots/heatmap/heatmap.py +++ b/bec_widgets/widgets/plots/heatmap/heatmap.py @@ -115,6 +115,8 @@ class Heatmap(ImageBase): "auto_range_y.setter", "minimal_crosshair_precision", "minimal_crosshair_precision.setter", + "attach", + "detach", "screenshot", # ImageView Specific Settings "color_map", diff --git a/bec_widgets/widgets/plots/image/image.py b/bec_widgets/widgets/plots/image/image.py index 241eec4f..2d4ce927 100644 --- a/bec_widgets/widgets/plots/image/image.py +++ b/bec_widgets/widgets/plots/image/image.py @@ -91,6 +91,8 @@ class Image(ImageBase): "auto_range_y.setter", "minimal_crosshair_precision", "minimal_crosshair_precision.setter", + "attach", + "detach", "screenshot", # ImageView Specific Settings "color_map", diff --git a/bec_widgets/widgets/plots/motor_map/motor_map.py b/bec_widgets/widgets/plots/motor_map/motor_map.py index d01af730..e9e5f979 100644 --- a/bec_widgets/widgets/plots/motor_map/motor_map.py +++ b/bec_widgets/widgets/plots/motor_map/motor_map.py @@ -128,6 +128,8 @@ class MotorMap(PlotBase): "y_log.setter", "legend_label_size", "legend_label_size.setter", + "attach", + "detach", "screenshot", # motor_map specific "color", diff --git a/bec_widgets/widgets/plots/multi_waveform/multi_waveform.py b/bec_widgets/widgets/plots/multi_waveform/multi_waveform.py index 4a891e80..ee7bdc78 100644 --- a/bec_widgets/widgets/plots/multi_waveform/multi_waveform.py +++ b/bec_widgets/widgets/plots/multi_waveform/multi_waveform.py @@ -96,6 +96,8 @@ class MultiWaveform(PlotBase): "legend_label_size.setter", "minimal_crosshair_precision", "minimal_crosshair_precision.setter", + "attach", + "detach", "screenshot", # MultiWaveform Specific RPC Access "highlighted_index", diff --git a/bec_widgets/widgets/plots/scatter_waveform/scatter_waveform.py b/bec_widgets/widgets/plots/scatter_waveform/scatter_waveform.py index 9f4adc6f..a7b81c89 100644 --- a/bec_widgets/widgets/plots/scatter_waveform/scatter_waveform.py +++ b/bec_widgets/widgets/plots/scatter_waveform/scatter_waveform.py @@ -84,6 +84,8 @@ class ScatterWaveform(PlotBase): "legend_label_size.setter", "minimal_crosshair_precision", "minimal_crosshair_precision.setter", + "attach", + "detach", "screenshot", # Scatter Waveform Specific RPC Access "main_curve", diff --git a/bec_widgets/widgets/plots/waveform/waveform.py b/bec_widgets/widgets/plots/waveform/waveform.py index 1a43713a..117fb139 100644 --- a/bec_widgets/widgets/plots/waveform/waveform.py +++ b/bec_widgets/widgets/plots/waveform/waveform.py @@ -63,6 +63,10 @@ class Waveform(PlotBase): RPC = True ICON_NAME = "show_chart" USER_ACCESS = [ + # BECWidget Base Class + "attach", + "detach", + "screenshot", # General PlotBase Settings "_config_dict", "enable_toolbar", @@ -105,7 +109,6 @@ class Waveform(PlotBase): "legend_label_size.setter", "minimal_crosshair_precision", "minimal_crosshair_precision.setter", - "screenshot", # Waveform Specific RPC Access "curves", "x_mode", diff --git a/bec_widgets/widgets/progress/ring_progress_bar/ring_progress_bar.py b/bec_widgets/widgets/progress/ring_progress_bar/ring_progress_bar.py index eeb41307..6385e387 100644 --- a/bec_widgets/widgets/progress/ring_progress_bar/ring_progress_bar.py +++ b/bec_widgets/widgets/progress/ring_progress_bar/ring_progress_bar.py @@ -92,6 +92,9 @@ class RingProgressBar(BECWidget, QWidget): "set_diameter", "reset_diameter", "enable_auto_updates", + "attach", + "detach", + "screenshot", ] def __init__( diff --git a/bec_widgets/widgets/services/bec_status_box/bec_status_box.py b/bec_widgets/widgets/services/bec_status_box/bec_status_box.py index cd21e9b6..ca22a2f6 100644 --- a/bec_widgets/widgets/services/bec_status_box/bec_status_box.py +++ b/bec_widgets/widgets/services/bec_status_box/bec_status_box.py @@ -76,7 +76,7 @@ class BECStatusBox(BECWidget, CompactPopupWidget): PLUGIN = True CORE_SERVICES = ["DeviceServer", "ScanServer", "SciHub", "ScanBundler", "FileWriterManager"] - USER_ACCESS = ["get_server_state", "remove"] + USER_ACCESS = ["get_server_state", "remove", "attach", "detach", "screenshot"] service_update = Signal(BECServiceInfoContainer) bec_core_state = Signal(str)