diff --git a/bec_widgets/examples/bec_main_app/__init__.py b/bec_widgets/examples/bec_main_app/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/bec_widgets/examples/bec_main_app/bec_main_app.py b/bec_widgets/examples/bec_main_app/bec_main_app.py new file mode 100644 index 00000000..4c70c198 --- /dev/null +++ b/bec_widgets/examples/bec_main_app/bec_main_app.py @@ -0,0 +1,67 @@ +from qtpy import QtCore, QtWidgets + +from bec_widgets.examples.device_manager_view.device_manager_view import DeviceManagerView +from bec_widgets.widgets.containers.advanced_dock_area.advanced_dock_area import AdvancedDockArea + + +class BECMainApp(QtWidgets.QWidget): + def __init__(self, parent=None): + super().__init__(parent) + + # Main layout + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(0) + + # Tab widget as central area + self.tabs = QtWidgets.QTabWidget(self) + self.tabs.setContentsMargins(0, 0, 0, 0) + self.tabs.setTabPosition(QtWidgets.QTabWidget.West) # Tabs on the left side + + layout.addWidget(self.tabs) + # Add DM + self._add_device_manager_view() + + # Add Plot area + self._add_ad_dockarea() + + # Adjust size of tab bar + # TODO not yet properly working, tabs a spread across the full length, to be checked! + tab_bar = self.tabs.tabBar() + tab_bar.setFixedWidth(tab_bar.sizeHint().width()) + + def _add_device_manager_view(self) -> None: + self.device_manager_view = DeviceManagerView(parent=self) + self.add_tab(self.device_manager_view, "Device Manager") + + def _add_ad_dockarea(self) -> None: + self.advanced_dock_area = AdvancedDockArea(parent=self) + self.add_tab(self.advanced_dock_area, "Plot Area") + + def add_tab(self, widget: QtWidgets.QWidget, title: str): + """Add a custom QWidget as a tab.""" + tab_container = QtWidgets.QWidget() + tab_layout = QtWidgets.QVBoxLayout(tab_container) + tab_layout.setContentsMargins(0, 0, 0, 0) + tab_layout.setSpacing(0) + + tab_layout.addWidget(widget) + self.tabs.addTab(tab_container, title) + + +if __name__ == "__main__": + import sys + + from bec_lib.bec_yaml_loader import yaml_load + from bec_qthemes import apply_theme + + app = QtWidgets.QApplication(sys.argv) + apply_theme("light") + win = BECMainApp() + config_path = "/Users/appel_c/work_psi_awi/bec_workspace/csaxs_bec/csaxs_bec/device_configs/first_light.yaml" + cfg = yaml_load(config_path) + cfg.update({"device_will_fail": {"name": "device_will_fail", "some_param": 1}}) + win.device_manager_view.device_table_view.set_device_config(cfg) + win.resize(1920, 1080) + win.show() + sys.exit(app.exec_()) diff --git a/bec_widgets/examples/device_manager_view/device_manager_view.py b/bec_widgets/examples/device_manager_view/device_manager_view.py index d3950dca..62ab960a 100644 --- a/bec_widgets/examples/device_manager_view/device_manager_view.py +++ b/bec_widgets/examples/device_manager_view/device_manager_view.py @@ -1,32 +1,36 @@ -from typing import List +from __future__ import annotations + +import os +from typing import TYPE_CHECKING, List import PySide6QtAds as QtAds import yaml -from bec_qthemes import material_icon +from bec_lib.bec_yaml_loader import yaml_load +from bec_lib.file_utils import DeviceConfigWriter +from bec_lib.logger import bec_logger +from bec_lib.plugin_helper import plugin_package_name, plugin_repo_path +from bec_qthemes import apply_theme from PySide6QtAds import CDockManager, CDockWidget from qtpy.QtCore import Qt, QTimer -from qtpy.QtWidgets import ( - QPushButton, - QSizePolicy, - QSplitter, - QStackedLayout, - QTreeWidget, - QVBoxLayout, - QWidget, -) +from qtpy.QtWidgets import QFileDialog, QMessageBox, QSplitter, QVBoxLayout, QWidget from bec_widgets import BECWidget from bec_widgets.utils.error_popups import SafeSlot +from bec_widgets.utils.toolbars.actions import MaterialIconAction +from bec_widgets.utils.toolbars.bundles import ToolbarBundle +from bec_widgets.utils.toolbars.toolbar import ModularToolBar from bec_widgets.widgets.containers.advanced_dock_area.advanced_dock_area import AdvancedDockArea -from bec_widgets.widgets.control.device_manager.components.device_table_view import DeviceTableView -from bec_widgets.widgets.control.device_manager.components.dm_config_view import DMConfigView -from bec_widgets.widgets.control.device_manager.components.dm_ophyd_test import ( - DeviceManagerOphydTest, +from bec_widgets.widgets.control.device_manager.components import ( + DeviceTableView, + DMConfigView, + DMOphydTest, + DocstringView, ) -from bec_widgets.widgets.editors.monaco.monaco_widget import MonacoWidget -from bec_widgets.widgets.editors.web_console.web_console import WebConsole -from bec_widgets.widgets.services.bec_status_box.bec_status_box import BECStatusBox -from bec_widgets.widgets.utility.ide_explorer.ide_explorer import IDEExplorer + +if TYPE_CHECKING: + from bec_lib.client import BECClient + +logger = bec_logger.logger def set_splitter_weights(splitter: QSplitter, weights: List[float]) -> None: @@ -66,7 +70,7 @@ def set_splitter_weights(splitter: QSplitter, weights: List[float]) -> None: class DeviceManagerView(BECWidget, QWidget): def __init__(self, parent=None, *args, **kwargs): - super().__init__(parent, *args, **kwargs) + super().__init__(parent=parent, client=None, *args, **kwargs) # Top-level layout hosting a toolbar and the dock manager self._root_layout = QVBoxLayout(self) @@ -75,38 +79,52 @@ class DeviceManagerView(BECWidget, QWidget): self.dock_manager = CDockManager(self) self._root_layout.addWidget(self.dock_manager) - # Initialize the widgets - self.explorer = IDEExplorer(self) # TODO will be replaced by explorer widget + # Available Resources Widget + self.available_devices = QWidget(self) + self.available_devices_dock = QtAds.CDockWidget("Available Devices", self) + self.available_devices_dock.setWidget(self.available_devices) + + # Device Table View widget self.device_table_view = DeviceTableView(self) - # Placeholder - self.dm_config_view = DMConfigView(self) - - # Placeholder for ophyd test - WebConsole.startup_cmd = "ipython" - self.ophyd_test = DeviceManagerOphydTest(self) - self.ophyd_test_dock = QtAds.CDockWidget("Ophyd Test", self) - self.ophyd_test_dock.setWidget(self.ophyd_test) - - # Create the dock widgets - self.explorer_dock = QtAds.CDockWidget("Explorer", self) - self.explorer_dock.setWidget(self.explorer) - self.device_table_view_dock = QtAds.CDockWidget("Device Table", self) self.device_table_view_dock.setWidget(self.device_table_view) - # Device Table will be central widget - self.dock_manager.setCentralWidget(self.device_table_view_dock) - - self.dm_config_view_dock = QtAds.CDockWidget("YAML Editor", self) + # Device Config View widget + self.dm_config_view = DMConfigView(self) + self.dm_config_view_dock = QtAds.CDockWidget("Device Config View", self) self.dm_config_view_dock.setWidget(self.dm_config_view) - # Add the dock widgets to the dock manager - self.dock_manager.addDockWidget(QtAds.DockWidgetArea.LeftDockWidgetArea, self.explorer_dock) - monaco_yaml_area = self.dock_manager.addDockWidget( - QtAds.DockWidgetArea.RightDockWidgetArea, self.dm_config_view_dock + # Docstring View + self.dm_docs_view = DocstringView(self) + self.dm_docs_view_dock = QtAds.CDockWidget("Docstring View", self) + self.dm_docs_view_dock.setWidget(self.dm_docs_view) + + # Ophyd Test view + self.ophyd_test_view = DMOphydTest(self) + self.ophyd_test_dock_view = QtAds.CDockWidget("Ophyd Test View", self) + self.ophyd_test_dock_view.setWidget(self.ophyd_test_view) + + # Arrange widgets within the QtAds dock manager + + # Central widget area + self.central_dock_area = self.dock_manager.setCentralWidget(self.device_table_view_dock) + self.dock_manager.addDockWidget( + QtAds.DockWidgetArea.BottomDockWidgetArea, + self.dm_docs_view_dock, + self.central_dock_area, + ) + + # Left Area + self.left_dock_area = self.dock_manager.addDockWidget( + QtAds.DockWidgetArea.LeftDockWidgetArea, self.available_devices_dock ) self.dock_manager.addDockWidget( - QtAds.DockWidgetArea.BottomDockWidgetArea, self.ophyd_test_dock, monaco_yaml_area + QtAds.DockWidgetArea.BottomDockWidgetArea, self.dm_config_view_dock, self.left_dock_area + ) + + # Right area + self.dock_manager.addDockWidget( + QtAds.DockWidgetArea.RightDockWidgetArea, self.ophyd_test_dock_view ) for dock in self.dock_manager.dockWidgets(): @@ -122,10 +140,252 @@ class DeviceManagerView(BECWidget, QWidget): area.titleBar().setVisible(False) # Apply stretch after the layout is done - self.set_default_view([2, 5, 3], [5, 5]) + self.set_default_view([2, 8, 2], [3, 1]) + # self.set_default_view([2, 8, 2], [2, 2, 4]) # Connect slots self.device_table_view.selected_device.connect(self.dm_config_view.on_select_config) + self.device_table_view.selected_device.connect(self.dm_docs_view.on_select_config) + self.ophyd_test_view.device_validated.connect( + self.device_table_view.update_device_validation + ) + self.device_table_view.device_configs_added.connect(self.ophyd_test_view.add_device_configs) + + self._add_toolbar() + + def _add_toolbar(self): + self.toolbar = ModularToolBar(self) + + # Add IO actions + self._add_io_actions() + self._add_table_actions() + self.toolbar.show_bundles(["IO", "Table"]) + self._root_layout.insertWidget(0, self.toolbar) + + def _add_io_actions(self): + # Create IO bundle + io_bundle = ToolbarBundle("IO", self.toolbar.components) + + # Add load config from plugin dir + self.toolbar.add_bundle(io_bundle) + + load = MaterialIconAction( + icon_name="file_open", parent=self, tooltip="Load configuration file from disk" + ) + self.toolbar.components.add_safe("load", load) + load.action.triggered.connect(self._load_file_action) + io_bundle.add_action("load") + + # Add safe to disk + safe_to_disk = MaterialIconAction( + icon_name="file_save", parent=self, tooltip="Save config to disk" + ) + self.toolbar.components.add_safe("safe_to_disk", safe_to_disk) + safe_to_disk.action.triggered.connect(self._safe_to_disk_action) + io_bundle.add_action("safe_to_disk") + + # Add load config from redis + load_redis = MaterialIconAction( + icon_name="cached", parent=self, tooltip="Load current config from Redis" + ) + load_redis.action.triggered.connect(self._load_redis_action) + self.toolbar.components.add_safe("load_redis", load_redis) + io_bundle.add_action("load_redis") + + # Update config action + update_config_redis = MaterialIconAction( + icon_name="cloud_upload", parent=self, tooltip="Update current config in Redis" + ) + update_config_redis.action.triggered.connect(self._update_redis_action) + self.toolbar.components.add_safe("update_config_redis", update_config_redis) + io_bundle.add_action("update_config_redis") + + # Table actions + + def _add_table_actions(self) -> None: + table_bundle = ToolbarBundle("Table", self.toolbar.components) + + # Add load config from plugin dir + self.toolbar.add_bundle(table_bundle) + + # Reset composed view + reset_composed = MaterialIconAction( + icon_name="delete_sweep", parent=self, tooltip="Reset current composed config view" + ) + reset_composed.action.triggered.connect(self._reset_composed_view) + self.toolbar.components.add_safe("reset_composed", reset_composed) + table_bundle.add_action("reset_composed") + + # Add device + add_device = MaterialIconAction(icon_name="add", parent=self, tooltip="Add new device") + add_device.action.triggered.connect(self._add_device_action) + self.toolbar.components.add_safe("add_device", add_device) + table_bundle.add_action("add_device") + + # Remove device + remove_device = MaterialIconAction(icon_name="remove", parent=self, tooltip="Remove device") + remove_device.action.triggered.connect(self._remove_device_action) + self.toolbar.components.add_safe("remove_device", remove_device) + table_bundle.add_action("remove_device") + + # Rerun validation + rerun_validation = MaterialIconAction( + icon_name="checklist", parent=self, tooltip="Run device validation on selected devices" + ) + rerun_validation.action.triggered.connect(self._rerun_validation_action) + self.toolbar.components.add_safe("rerun_validation", rerun_validation) + table_bundle.add_action("rerun_validation") + + # Most likly, no actions on available devices + # Actions (vielleicht bundle fuer available devices ) + # - reset composed view + # - add new device (EpicsMotor, EpicsMotorECMC, EpicsSignal, CustomDevice) + # - remove device + # - rerun validation (with/without connect) + + # IO actions + + @SafeSlot() + def _load_file_action(self): + """Action for the 'load' action to load a config from disk for the io_bundle of the toolbar.""" + # Check if plugin repo is installed... + try: + plugin_path = plugin_repo_path() + plugin_name = plugin_package_name() + config_path = os.path.join(plugin_path, plugin_name, "device_configs") + except ValueError: + # Get the recovery config path as fallback + config_path = self._get_recovery_config_path() + logger.warning( + f"No plugin repository installed, fallback to recovery config path: {config_path}" + ) + + # Implement the file loading logic here + start_dir = os.path.abspath(config_path) + file_path, _ = QFileDialog.getOpenFileName( + self, caption="Select Config File", dir=start_dir + ) + if file_path: + try: + config = yaml_load(file_path) + except Exception as e: + logger.error(f"Failed to load config from file {file_path}. Error: {e}") + return + self.device_table_view.set_device_config( + config + ) # TODO ADD QDialog with 'replace', 'add' & 'cancel' + + # TODO would we ever like to add the current config to an existing composition + @SafeSlot() + def _load_redis_action(self): + """Action for the 'load_redis' action to load the current config from Redis for the io_bundle of the toolbar.""" + reply = QMessageBox.question( + self, + "Load currently active config", + "Do you really want to flush the current config and reload?", + QMessageBox.Yes | QMessageBox.No, + QMessageBox.No, + ) + if reply == QMessageBox.Yes: + cfg = {} + config_list = self.client.device_manager._get_redis_device_config() + for item in config_list: + k = item["name"] + item.pop("name") + cfg[k] = item + self.device_table_view.set_device_config(cfg) + else: + return + + @SafeSlot() + def _safe_to_disk_action(self): + """Action for the 'safe_to_disk' action to save the current config to disk.""" + # Check if plugin repo is installed... + try: + config_path = self._get_recovery_config_path() + except ValueError: + # Get the recovery config path as fallback + config_path = os.path.abspath(os.path.expanduser("~")) + logger.warning(f"Failed to find recovery config path, fallback to: {config_path}") + + # Implement the file loading logic here + file_path, _ = QFileDialog.getSaveFileName( + self, caption="Save Config File", dir=config_path + ) + if file_path: + config = self.device_table_view.get_device_config() + with open(file_path, "w") as file: + file.write(yaml.dump(config)) + + # TODO add here logic, should be asyncronous, but probably block UI, and show a loading spinner. If failed, it should report.. + @SafeSlot() + def _update_redis_action(self): + """Action for the 'update_redis' action to update the current config in Redis.""" + config = self.device_table_view.get_device_config() + reply = QMessageBox.question( + self, + "Not implemented yet", + "This feature has not been implemented yet, will be coming soon...!!", + QMessageBox.Cancel, + QMessageBox.Cancel, + ) + + # Table actions + + @SafeSlot() + def _reset_composed_view(self): + """Action for the 'reset_composed_view' action to reset the composed view.""" + reply = QMessageBox.question( + self, + "Clear View", + "You are about to clear the current composed config view, please confirm...", + QMessageBox.Yes | QMessageBox.No, + QMessageBox.No, + ) + if reply == QMessageBox.Yes: + self.device_table_view.clear_device_configs() + + # TODO Here we would like to implement a custom popup view, that allows to add new devices + # We want to have a combobox to choose from EpicsMotor, EpicsMotorECMC, EpicsSignal, EpicsSignalRO, and maybe EpicsSignalWithRBV and custom Device + # For all default Epics devices, we would like to preselect relevant fields, and prompt them with the proper deviceConfig args already, i.e. 'prefix', 'read_pv', 'write_pv' etc.. + # For custom Device, they should receive all options. It might be cool to get a side panel with docstring view of the class upon inspecting it to make it easier in case deviceConfig entries are required.. + @SafeSlot() + def _add_device_action(self): + """Action for the 'add_device' action to add a new device.""" + # Implement the logic to add a new device + reply = QMessageBox.question( + self, + "Not implemented yet", + "This feature has not been implemented yet, will be coming soon...!!", + QMessageBox.Cancel, + QMessageBox.Cancel, + ) + + # TODO fix the device table remove actions. This is currently not working properly... + @SafeSlot() + def _remove_device_action(self): + """Action for the 'remove_device' action to remove a device.""" + reply = QMessageBox.question( + self, + "Not implemented yet", + "This feature has not been implemented yet, will be coming soon...!!", + QMessageBox.Cancel, + QMessageBox.Cancel, + ) + + # TODO implement proper logic for validation. We should also carefully review how these jobs update the table, and how we can cancel pending validations + # in case they are no longer relevant. We might want to 'block' the interactivity on the items for which validation runs with 'connect'! + @SafeSlot() + def _rerun_validation_action(self): + """Action for the 'rerun_validation' action to rerun validation on selected devices.""" + # Implement the logic to rerun validation on selected devices + reply = QMessageBox.question( + self, + "Not implemented yet", + "This feature has not been implemented yet, will be coming soon...!!", + QMessageBox.Cancel, + QMessageBox.Cancel, + ) ####### Default view has to be done with setting up splitters ######## def set_default_view(self, horizontal_weights: list, vertical_weights: list): @@ -189,18 +449,40 @@ class DeviceManagerView(BECWidget, QWidget): v = [1, 1] self.set_default_view(h, v) + def _get_recovery_config_path(self) -> str: + """Get the recovery config path from the log_writer config.""" + # pylint: disable=protected-access + log_writer_config: BECClient = self.client._service_config.config.get("log_writer", {}) + writer = DeviceConfigWriter(service_config=log_writer_config) + return os.path.abspath(os.path.expanduser(writer.get_recovery_directory())) + if __name__ == "__main__": import sys + from copy import deepcopy + from bec_lib.bec_yaml_loader import yaml_load from qtpy.QtWidgets import QApplication + from bec_widgets.widgets.utility.visual.dark_mode_button.dark_mode_button import DarkModeButton + app = QApplication(sys.argv) + w = QWidget() + l = QVBoxLayout() + w.setLayout(l) + apply_theme("dark") + button = DarkModeButton() + l.addWidget(button) device_manager_view = DeviceManagerView() - config = device_manager_view.client.device_manager._get_redis_device_config() - device_manager_view.device_table_view.set_device_config(config) - device_manager_view.show() - device_manager_view.setWindowTitle("Device Manager View") - device_manager_view.resize(1200, 800) + l.addWidget(device_manager_view) + # config_path = "/Users/appel_c/work_psi_awi/bec_workspace/csaxs_bec/csaxs_bec/device_configs/first_light.yaml" + # cfg = yaml_load(config_path) + # cfg.update({"device_will_fail": {"name": "device_will_fail", "some_param": 1}}) + + # # config = device_manager_view.client.device_manager._get_redis_device_config() + # device_manager_view.device_table_view.set_device_config(cfg) + w.show() + w.setWindowTitle("Device Manager View") + w.resize(1920, 1080) # developer_view.set_stretch(horizontal=[1, 3, 2], vertical=[5, 5]) #can be set during runtime sys.exit(app.exec_()) diff --git a/bec_widgets/examples/device_manager_view/device_manager_widget.py b/bec_widgets/examples/device_manager_view/device_manager_widget.py index 98e34fef..9d4c9c80 100644 --- a/bec_widgets/examples/device_manager_view/device_manager_widget.py +++ b/bec_widgets/examples/device_manager_view/device_manager_widget.py @@ -2,6 +2,10 @@ from __future__ import annotations +import os + +from bec_lib.bec_yaml_loader import yaml_load +from bec_lib.logger import bec_logger from bec_qthemes import material_icon from qtpy import QtCore, QtWidgets @@ -9,6 +13,8 @@ from bec_widgets.examples.device_manager_view.device_manager_view import DeviceM from bec_widgets.utils.bec_widget import BECWidget from bec_widgets.utils.error_popups import SafeSlot +logger = bec_logger.logger + class DeviceManagerWidget(BECWidget, QtWidgets.QWidget): @@ -41,19 +47,50 @@ class DeviceManagerWidget(BECWidget, QtWidgets.QWidget): self._overlay_widget.setSizePolicy( QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding ) + # Load current config self.button_load_current_config = QtWidgets.QPushButton("Load Current Config") icon = material_icon(icon_name="database", size=(24, 24), convert_to_pixmap=False) self.button_load_current_config.setIcon(icon) self._overlay_layout.addWidget(self.button_load_current_config) self.button_load_current_config.clicked.connect(self._load_config_clicked) + # Load config from disk + self.button_load_config_from_file = QtWidgets.QPushButton("Load Config From File") + icon = material_icon(icon_name="folder", size=(24, 24), convert_to_pixmap=False) + self.button_load_config_from_file.setIcon(icon) + self._overlay_layout.addWidget(self.button_load_config_from_file) + self.button_load_config_from_file.clicked.connect(self._load_config_from_file_clicked) self._overlay_widget.setVisible(True) + def _load_config_from_file_clicked(self): + """Handle click on 'Load Config From File' button.""" + start_dir = os.path.expanduser("~") + file_path, _ = QtWidgets.QFileDialog.getOpenFileName( + self, caption="Select Config File", dir=start_dir + ) + if file_path: + self._load_config_from_file(file_path) + + def _load_config_from_file(self, file_path: str): + try: + config = yaml_load(file_path) + except Exception as e: + logger.error(f"Failed to load config from file {file_path}. Error: {e}") + return + config_list = [] + for name, cfg in config.items(): + config_list.append(cfg) + config_list[-1]["name"] = name + self.device_manager_view.device_table_view.set_device_config(config_list) + # self.device_manager_view.ophyd_test.on_device_config_update(config) + self.stacked_layout.setCurrentWidget(self.device_manager_view) + @SafeSlot() def _load_config_clicked(self): """Handle click on 'Load Current Config' button.""" config = self.client.device_manager._get_redis_device_config() + config.append({"name": "wrong_device", "some_value": 1}) self.device_manager_view.device_table_view.set_device_config(config) - self.device_manager_view.ophyd_test.on_device_config_update(config) + # self.device_manager_view.ophyd_test.on_device_config_update(config) self.stacked_layout.setCurrentWidget(self.device_manager_view) diff --git a/bec_widgets/widgets/containers/advanced_dock_area/states/user/test.ini b/bec_widgets/widgets/containers/advanced_dock_area/states/user/test.ini new file mode 100644 index 00000000..6188162c --- /dev/null +++ b/bec_widgets/widgets/containers/advanced_dock_area/states/user/test.ini @@ -0,0 +1,234 @@ +[BECMainWindowNoRPC.AdvancedDockArea] +acceptDrops=false +accessibleDescription= +accessibleIdentifier= +accessibleName= +autoFillBackground=false +baseSize=@Size(0 0) +contextMenuPolicy=@Variant(\0\0\0\x7f\0\0\0\x18PySide::PyObjectWrapper\0\0\0\0\x66\x80\x4\x95[\0\0\0\0\0\0\0\x8c\bbuiltins\x94\x8c\agetattr\x94\x93\x94\x8c\xePySide6.QtCore\x94\x8c\x14Qt.ContextMenuPolicy\x94\x93\x94\x8c\x12\x44\x65\x66\x61ultContextMenu\x94\x86\x94R\x94.) +cursor=@Variant(\0\0\0J\0\0) +enabled=true +focusPolicy=@Variant(\0\0\0\x7f\0\0\0\x18PySide::PyObjectWrapper\0\0\0\0U\x80\x4\x95J\0\0\0\0\0\0\0\x8c\bbuiltins\x94\x8c\agetattr\x94\x93\x94\x8c\xePySide6.QtCore\x94\x8c\xeQt.FocusPolicy\x94\x93\x94\x8c\aNoFocus\x94\x86\x94R\x94.) +font=@Variant(\0\0\0@\0\0\0$\0.\0\x41\0p\0p\0l\0\x65\0S\0y\0s\0t\0\x65\0m\0U\0I\0\x46\0o\0n\0t@*\0\0\0\0\0\0\xff\xff\xff\xff\x5\x1\0\x32\x10) +geometry=@Rect(0 29 2075 974) +inputMethodHints=@Variant(\0\0\0\x7f\0\0\0\x18PySide::PyObjectWrapper\0\0\0\0Y\x80\x4\x95N\0\0\0\0\0\0\0\x8c\bbuiltins\x94\x8c\agetattr\x94\x93\x94\x8c\xePySide6.QtCore\x94\x8c\x12Qt.InputMethodHint\x94\x93\x94\x8c\aImhNone\x94\x86\x94R\x94.) +layoutDirection=@Variant(\0\0\0\x7f\0\0\0\x18PySide::PyObjectWrapper\0\0\0\0]\x80\x4\x95R\0\0\0\0\0\0\0\x8c\bbuiltins\x94\x8c\agetattr\x94\x93\x94\x8c\xePySide6.QtCore\x94\x8c\x12Qt.LayoutDirection\x94\x93\x94\x8c\vLeftToRight\x94\x86\x94R\x94.) +locale=@Variant(\0\0\0\x12\0\0\0\n\0\x65\0n\0_\0\x43\0H) +lock_workspace=false +maximumSize=@Size(16777215 16777215) +minimumSize=@Size(0 0) +mode=developer +mouseTracking=false +palette=@Variant(\0\0\0\x44\x1\x1\xff\xffMMQQWW\0\0\x1\x1\xff\xff\xf8\xf8\xf9\xf9\xfa\xfa\0\0\x1\x1\xff\xff\xff\xff\xff\xff\xff\xff\0\0\x1\x1\xff\xff\xf5\xf5\xf5\xf5\xf5\xf5\0\0\x1\x1\xff\xff\xbf\xbf\xbf\xbf\xbf\xbf\0\0\x1\x1\xff\xff\xa9:\xa9:\xa9:\0\0\x1\x1\xff\xffMMQQWW\0\0\x1\x1\xff\xff\xff\xff\xff\xff\xff\xff\0\0\x1\x1\xff\xffMMQQWW\0\0\x1\x1\xff\xff\xf8\xf8\xf9\xf9\xfa\xfa\0\0\x1\x1\xff\xff\xf8\xf8\xf9\xf9\xfa\xfa\0\0\x1\x1\xff\xff\0\0\0\0\0\0\0\0\x1\x1\x7f\x7f\x61\x61\x9e\x9e\xef\xef\0\0\x1\x1\xff\xffMMQQWW\0\0\x1\x1\xff\xff\x1a\x1ass\xe8\xe8\0\0\x1\x1\xff\xff\x66\x66\0\0\x98\x98\0\0\x1\x1\xff\xff\xf5\xf5\xf5\xf5\xf5\xf5\0\0\x1\x1\x66\x66MMQQWW\0\0\x1\x1\xff\xff\xf8\xf8\xf9\xf9\xfa\xfa\0\0\x1\x1\xff\xff\xff\xff\xff\xff\xff\xff\0\0\x1\x1\xff\xff\xf5\xf5\xf5\xf5\xf5\xf5\0\0\x1\x1\xff\xff\xbf\xbf\xbf\xbf\xbf\xbf\0\0\x1\x1\xff\xff\xa9:\xa9:\xa9:\0\0\x1\x1\x66\x66MMQQWW\0\0\x1\x1\xff\xff\xff\xff\xff\xff\xff\xff\0\0\x1\x1\x66\x66MMQQWW\0\0\x1\x1\xff\xff\xf8\xf8\xf9\xf9\xfa\xfa\0\0\x1\x1\xff\xff\xf8\xf8\xf9\xf9\xfa\xfa\0\0\x1\x1\xff\xff\0\0\0\0\0\0\0\0\x1\x1??MMQQWW\0\0\x1\x1\x66\x66MMQQWW\0\0\x1\x1??MMQQWW\0\0\x1\x1\x66\x66MMQQWW\0\0\x1\x1\xff\xff\xf5\xf5\xf5\xf5\xf5\xf5\0\0\x1\x1\xff\xffMMQQWW\0\0\x1\x1\xff\xff\xf8\xf8\xf9\xf9\xfa\xfa\0\0\x1\x1\xff\xff\xff\xff\xff\xff\xff\xff\0\0\x1\x1\xff\xff\xf5\xf5\xf5\xf5\xf5\xf5\0\0\x1\x1\xff\xff\xbf\xbf\xbf\xbf\xbf\xbf\0\0\x1\x1\xff\xff\xa9:\xa9:\xa9:\0\0\x1\x1\xff\xffMMQQWW\0\0\x1\x1\xff\xff\xff\xff\xff\xff\xff\xff\0\0\x1\x1\xff\xffMMQQWW\0\0\x1\x1\xff\xff\xf8\xf8\xf9\xf9\xfa\xfa\0\0\x1\x1\xff\xff\xf8\xf8\xf9\xf9\xfa\xfa\0\0\x1\x1\xff\xff\0\0\0\0\0\0\0\0\x1\x1\x7f\x7f\x61\x61\x9e\x9e\xef\xef\0\0\x1\x1\xff\xffMMQQWW\0\0\x1\x1\xff\xff\x1a\x1ass\xe8\xe8\0\0\x1\x1\xff\xff\x66\x66\0\0\x98\x98\0\0\x1\x1\xff\xff\xf5\xf5\xf5\xf5\xf5\xf5\0\0) +sizeIncrement=@Size(0 0) +sizePolicy=@Variant(\0\0\0K\0\0\0U) +statusTip= +styleSheet= +tabletTracking=false +toolTip= +toolTipDuration=-1 +updatesEnabled=true +visible=true +whatsThis= +windowFilePath= +windowIcon="@Variant(\0\0\0\x45\0\0\0\x1\x89PNG\r\n\x1a\n\0\0\0\rIHDR\0\0\0,\0\0\0,\b\x6\0\0\0\x1e\x84Z\x1\0\0\x1\x81iCCPsRGB IEC61966-2.1\0\0(\x91u\x91\xb9KCA\x10\x87\xbf\x1c\x1ehB\x4-,,\x82\x44\xab(^\x4m\x4\x13\x44\x85 !F\xf0j\x92g\xe!\xc7\xe3\xbd\x88\x4[\xc1\x36\xa0 \xdax\x15\xfa\x17h+X\v\x82\xa2\bb\x9dZ\xd1\x46\xc3s\x9e\x11\x12\xc4\xcc\x32;\xdf\xfevg\xd8\x9d\x5k$\xad\x64t\xfb\0\x64\xb2y-<\xe5w/,.\xb9\x9bJ4\xd0\x88\x13;#QEW'B\xa1 u\xed\xe3\x1\x8b\x19\xef\xfa\xccZ\xf5\xcf\xfdk\xad\xabq]\x1K\xb3\xf0\xb8\xa2jy\xe1i\xe1\xe0\x46^5yW\xb8\x43IEW\x85\xcf\x85\xbd\x9a\\P\xf8\xde\xd4\x63\x15.\x99\x9c\xac\xf0\x97\xc9Z$\x1c\0k\x9b\xb0;Y\xc3\xb1\x1aVRZFX^\x8e'\x93^W~\xef\x63\xbe\xc4\x11\xcf\xce\xcfI\xec\x16\xef\x42'\xcc\x14~\xdc\xcc\x30I\0\x1f\x83\x8c\xc9\xec\xa3\x8f!\xfa\x65\x45\x9d\xfc\x81\x9f\xfcYr\x92\xab\xc8\xacR@c\x8d$)\xf2xE]\x97\xeaq\x89\t\xd1\xe3\x32\xd2\x14\xcc\xfe\xff\xed\xab\x9e\x18\x1e\xaaTw\xf8\xa1\xe1\xc5\x30\xdez\xa0i\a\xca\x45\xc3\xf8<6\x8c\xf2\t\xd8\x9e\xe1*[\xcd\xcf\x1d\xc1\xe8\xbb\xe8\xc5\xaa\xe6\x39\x4\xd7\x16\\\\W\xb5\xd8\x1e\\nC\xe7\x93\x1a\xd5\xa2?\x92M\xdc\x9aH\xc0\xeb\x19\x38\x17\xa1\xfd\x16Z\x96+=\xfb\xdd\xe7\xf4\x11\"\x9b\xf2U7\xb0\x7f\0\xbdr\xde\xb5\xf2\r+\xf4g\xcbwA\x5\xc7\0\0\0\tpHYs\0\0=\x84\0\0=\x84\x1\xd5\xac\xaft\0\0\x6\xdaIDATX\x85\xed\x99mp\x15W\x19\xc7\x7fgor\xc3\xbd\xb9I/\x91$\xb7XB^J\x81\x84*F\xd3\x32\x86ON\xd5\nh\xb4\x36\x43p\xa0\xadT3\x9d\x86\xfa\xc1R-\x1a\x64\xca@\xeb\xe0\x94\xce\xd8\xb4\xe\x38\x3\xbe\xd1J\x98I\xc6\t\x16\x11\x61\x98\x90\x12\xa0)\x4\xa5-\xa0\xd4\xb6\xa9\x91\x4\b$$$\xdc\xdd=\xc7\xf\xbb\xf7}s\xefM\x88\xce\x38\xc3\x7f\x66gw\xcfy\xces\xfe\xe7\x7f\x9e}\xce\xd9]\xb8\x8d\xdb\xf8\xff\x86H\xc7\x66\xfb\xa7\x1f\x99\x93\xe5r\xd7(\xb4O\xe9(\x7fP\xa1\x19h\x18\n\x82\x80\x1\x18\x12\fD\xf8^W\x1a:`(\xd0\xb1\xe\x43\n\xeb\f\x4\x95\xc0\x4\x19\x84k\x86\x14\x7f\xbd\xa1\x8f\xfd\xe1\xf4\xfb\xbf\xfc;\xa0&M\xf8\x95\x5\x35\xb3\n\xb2\xf2\xb7*\xa1=\xa4\x83\x66u.\xd0U\x88\x94M\xc0>[\xe5V\xbdn\xd7\x1bv\xb9\x8e \xdc><\x90\x88\xaf\xa0\x12\xd2P\xb2\xe5\xfa\xf5K\xdf\xbf\xd0\xfb\xfb\x9e\xf1\x38i\xe3U\xec\xacX^U\xe4\xc9?\x91\xa1\x89\x87\x93\xd9M\b!y\x1c\x34\x14\xa0!\xb4\x87\xa7\xf9\n\xdf*-}\xacj<\x17\x8e\x44\x9a\xe6/\x99]\xe8\xcdi\x13\x88\xc0\x94\x10\r1U\xb1\xb7\x8e\x96\x42\x14\x66\x64\xdc\xd1\x16\b|\xbd\xd8\xa9\xde\x89\xb0\xab\xd4[\xf8\x92&D\xe1-qt\xea\xcc\xa5QQu7\xbe\\OR;!D\xa1\x37\xe7\xae\xad\x80+\xa1.\xbe\xe0\x17\xf3\x97.\xb8\xc7w\xe7i]\b-\x14_\xae|?\xca\x33\r\x13+\xf6\f\xac\xd8\x8c\xbf\xbe\xd2{\x95\xb1\x9b\x86\x63\fKM\xa3~\xf3*\x16/\xab\xa2\xe7\xc2\x45~\xb8\xba\x89\xfe\xcb\xd7\x93=\xfr\xe4\xc6\xbf\x16\xf6}\xbc\xe7o\xd1\xfc\x32\xe2\t\xcf\xf2\xe4\xd5 \"\xcag\x97\x4X\xf8\xf2\x1a\x84\x96:\x8c/\x1c=K\xebO^O(\xd7\\\x1a\xdf\xdd\xbc\x8a\xea\xa5Vh\xce*\v\xf0\xd3\x1dkX\xbb\xba\x89\xbeK\xd7\x63l\xa3\xc2[sg\xe5\x7f\r\x88!\x1c\xcf\x42\xcb\xd4\\\xf7\x46\x17\x64\xfa}\x16Y\xa5PI\xe\x80\xbc\xd9\xf9\x31\xcf\x93\0\x34-BV)\xc5\xa9\x8e\xf7\x30\f\x93\xa2\xb2\0?\xdb\xb1\x86\xbc\x19\x39\x31\x4\x62\xa6\\h\xf7\xc6s\x8cWX\b\x81?^\xa1\xd0\xc8\xf|s\x13\xba\x94\x91\xa9\xc6\x9a\xc2\x39\xcb\xaaXT\xff`B\x87\xc2%X\xbdi\x15\xf7\xdb\xca\x1e\xd8\xdd\xc1\xb6\xcd{\xb8\xef\x8b\vyz\xcb\xa3\xcc.\v\xf0\xd2\xce\x35<\xf5\xedW\x12\x94\xb6\x31=~\f\xf1\n\v\xa5\xc6Oa\xc6X\x10\x63L\xb7\xcf\x41\x8cQ\x1d},\x88\xd4\xcd\x4[M\xd3xt\xd3#Qd\x8f\xb0\xe3\xf9=(\x5\x1d\xfb\xbb\xd9\xf2\x83\xdf`\x18&\xc5\x65\x1^\xde\xd9\xc0'\xe2\x94\xb6U\xd2R\x11\x46\xa1\xc6M:\xe9,\x8b!\xc3o=\xb7\x92\xfbl\xb2\aw\x1f\xe1\xd7\x9b\x9bQ2\x12\x30G\xf6\x9f\xe2\x85g\"\xa4_\xfdU\x3\xb9\xb9\xde\xe4\xcb\x9c\x13\xe1\xa9\x80\x10P\xf9`%\0\xc3\xd7\x46hm\xda\x8bRDFl\x9f\xdb\xfts\xf2\xd8y\0J\xca\x2\xcc\x99;3\xa5(SNX\x1J\xc2\x9e\xe7\x9b\x91R\xe2\xf3g\xb3v[\x3\xbe\\oL\n\x10\x42\xf0\xbd\xc6Z\xaa\xaa\xe7\x1\xb0\xaf\xad\x8b\xee\xb7\xdfO\xe9\x7f\xca\t\x87\x14\xeal=\xcak\x1b_GJIqy\x11\xcfno \xfb\xe\xafM\x16\x9el\xac\xe5\xabu\x8b\x11\x42\xb0\xbf\xad\x8b\x8d\xebvaJ\x99\xd2\x7f\x42\x1e\x8e z\xe-\x14V\x97\x63H\x85\t\x98\xd6n\vSA^\xa9\xf3\n~\xb4\xa5\x13S\xc1\xaa\r+(\xa9(\xa2q{\x3\xcf\xd5\xbf\xca\xf2\xa7\x96\xb2\xa4n1\0\a\xda\xba\xd8\xb4n\x17\xa6\xa9\xc2\xbdN\x92p,Y!\x4\x95?^\x91\xd4\x99\x32\x63\x15R@Gk'&\xf0\xd8\x86\x15\x94V\x14\xd1\xf4\xc7\xf5\xe4\xf8\xb3\x1\xf8\xcb\xde.^\xf8\xd1.\xa4\x94\t\xfdM\x82\xb0\x85\x1b\x1f]b\xe4\xc3>\\>\xf\xa8\x88\x2N\xe7\xf3\x87\xcf\x38\xfaho\xe9\xc4\x44\xf0\xf8\x86\xba\x30\xd9\x43{\xbb\xd8\xb2\xeewae\xd3\x45J\xc2\xc1+\x83\x9cx\xe2\xe7\x8e{X\xa7\xfd\xf0xJ\x1dn\xe9\x44*X\xddXK\xfb\xbeSl]\xff\x1aRN\x8clJ\xc2i\xe7\xdd\x34q\xa8\xb5\x93?\xb7\x1e#\xa8@&\x19\\2$\xcd\x12\x13\x1f\x7fj(uk\x8eS\x86\x84\xdb\xef\x63\xd1\xc6\xc7\xc9\xf4Y\xab\x90\xf3!P\nN\xee=N\xc7\xee\xf6p\xdb\x65O.\xa1\xbc\xba<\xd6VE\xb5\xb1\xcb\xfa\xfb\ai|\xe6\xb7\xe8\x63zJ\xcdS\x12\xf6\xdf\x33\x8b\xc0\xa2\x8aTf\x96\x33o\x16oF\x11\xae~\xe8\xf3\xf8\v\x1c\xf7R1\x98\xf\xdc=w&\xdd\xa7?L\xddGZL\0\xa5\x14o\xbd\xd8\x8ci\xe7\x61I$\xf\xdf\xf5\xd9\x39\xcc{\xa0\x92\x8cL\x97\xe3lw\xbe\xd1\xc5\xf9\xee\x7f\"\x1\x33\xaa\x9d\xcb\x9d\x41\xfd\xda\x1a\x84\x10\x64\x66$\xbc\\L\x9cp\xcc\xf4(8\xd7|\x98\xa0T\tYB\x2\xf3\x1e\xa8Llc\xe3\xdd\xe3\xe7\x38\xd4r,\xe1\xad\xd9\xe5\xc9\xa2~mMZDC\xf8\xdf>tS\x90v\xa6t/\x91r\x80i(pK\xdb\xcb\x89\n2\xd5y\xdb\t\xf1\x84\x95\xb0\x42\xd2\xba\x99\x84\xc3[\r\xa3\xd8w:\x95\xe0.Aa\xa9\xd4\xe0\x64;K\xdc\xdf%1L\xa3J\xa1\x6\xe3\xad\xe3\xb3\x84\xbc\x61\x1a\xef\xba]\xee\x44O\x2\x96\x1f|\x91\xe8\x31\x87.]\xee\x8c\x84\xb2h\xac|\xb6\x96\xba\xa7\xbf\xe1P\x9f\x62x\xd2x'\x15\x61ur\xb0\xe7\x8d/\xcc\x98\xbb\x1e\x81&\0}x\x14\xa5\x14\x42\b\xdc\x39\xde\xa4\xfe\a\xff=\x10\x43\x61th\x14\x7f\x81\x9f,\x8f\x9b,\x8f\x83\b6LS\xd2\xdfw-\x81\xee\xe8\xc8\a\xfb\x88\nQp\x1e\xa2\xaf\xe5\x33+\x9b\xbd\x99\x9e\xaf\x84\xf2mNy\t\"\xdb\x13N\xf8\x86\x9d\xfc\x8d\xe8{\x5=\xe7>f\xe8\xeaH\xf8\xcb\x8f\xaf`:3J\x2Q\xed\xac\x85#\xbe\xdd\xe5\x81\x61\xde;\xdb\x1b\xf3\xb5\xe8\xa6\x31\xfa\xa7\xf.l\xab\x5\x86S\x11\xd6\xea\xee\\P\xb9\xf2\x93\xf7\xefSB\x9b\x11\xbb\x8d\xfc\xef}n\x8d\xf6\x15T\xf2\xf2\xc5\x8b\xedK\x86\x86\xba\xdf\x8eW\xd8i=Tg\x86\xfb\a\xf2\\\xdew\x8a\xb3\xf3\xbe\x84\xd0\xbc\x12\x81\xb4[:_\xa7\xaaO\xdf\xd6P\xf2\xca\xd5\xc1\x33\xdf\x19\x18\x38v\x14k\xdc\x31\x18o\x1\x37N\f\xf5|4l\x8c\x1e.\xf1\x16\x14\xb9]\x99%\x12\xc4\xe4\t\xa7\x35 \x19\xd4G\xe\xf6\xf6\x1dy\xe2\xf2\xc0\x89\x37\x81Q'b\xe9\xec\xe6\xa6\xd7\xce\xfc\xdc\xc2J\x7f\xf1\x97\xbd\xaei\xf3\xa4\x10\xb9\x6\x42\xe8\baH\xc2\xbf\0\x42_2#\xf7\xf6/\x3I\xccg-\x83\xf0/\x5\x65\xa2\xd4M%\x87\xc6\x8c\x9bg/\r\xfe\x63\x7f\xef\xc0\xf1\xd3\xc0U'e\xd3%\x1c\x82\v\xc8\xb4\xf\x17S\xb7\xa4[\x1b\x38\xfb\xaf\x81}}\x1b\xb7\x31\x11\xfc\a\x15\xfav.&\xc5\xfc~\0\0\0\0IEND\xae\x42`\x82)" +windowIconText= +windowModality=@Variant(\0\0\0\x7f\0\0\0\x18PySide::PyObjectWrapper\0\0\0\0Y\x80\x4\x95N\0\0\0\0\0\0\0\x8c\bbuiltins\x94\x8c\agetattr\x94\x93\x94\x8c\xePySide6.QtCore\x94\x8c\x11Qt.WindowModality\x94\x93\x94\x8c\bNonModal\x94\x86\x94R\x94.) +windowModified=false +windowOpacity=1 +windowTitle=Advanced Dock Area + +[BECMainWindowNoRPC.AdvancedDockArea.CDockManager.ads%3A%3ACDockSplitter.ads%3A%3ACDockAreaWidget.BECQueue.dockWidgetScrollArea.qt_scrollarea_viewport.BECQueue] +acceptDrops=false +accessibleDescription= +accessibleIdentifier= +accessibleName= +autoFillBackground=false +baseSize=@Size(0 0) +compact_view=false +contextMenuPolicy=@Variant(\0\0\0\x7f\0\0\0\x18PySide::PyObjectWrapper\0\0\0\0\x66\x80\x4\x95[\0\0\0\0\0\0\0\x8c\bbuiltins\x94\x8c\agetattr\x94\x93\x94\x8c\xePySide6.QtCore\x94\x8c\x14Qt.ContextMenuPolicy\x94\x93\x94\x8c\x12\x44\x65\x66\x61ultContextMenu\x94\x86\x94R\x94.) +cursor=@Variant(\0\0\0J\0\0) +enabled=true +expand_popup=true +focusPolicy=@Variant(\0\0\0\x7f\0\0\0\x18PySide::PyObjectWrapper\0\0\0\0U\x80\x4\x95J\0\0\0\0\0\0\0\x8c\bbuiltins\x94\x8c\agetattr\x94\x93\x94\x8c\xePySide6.QtCore\x94\x8c\xeQt.FocusPolicy\x94\x93\x94\x8c\aNoFocus\x94\x86\x94R\x94.) +font=@Variant(\0\0\0@\0\0\0$\0.\0\x41\0p\0p\0l\0\x65\0S\0y\0s\0t\0\x65\0m\0U\0I\0\x46\0o\0n\0t@*\0\0\0\0\0\0\xff\xff\xff\xff\x5\x1\0\x32\x10) +geometry=@Rect(0 0 1252 897) +hide_toolbar=false +inputMethodHints=@Variant(\0\0\0\x7f\0\0\0\x18PySide::PyObjectWrapper\0\0\0\0Y\x80\x4\x95N\0\0\0\0\0\0\0\x8c\bbuiltins\x94\x8c\agetattr\x94\x93\x94\x8c\xePySide6.QtCore\x94\x8c\x12Qt.InputMethodHint\x94\x93\x94\x8c\aImhNone\x94\x86\x94R\x94.) +label=BEC Queue +layoutDirection=@Variant(\0\0\0\x7f\0\0\0\x18PySide::PyObjectWrapper\0\0\0\0]\x80\x4\x95R\0\0\0\0\0\0\0\x8c\bbuiltins\x94\x8c\agetattr\x94\x93\x94\x8c\xePySide6.QtCore\x94\x8c\x12Qt.LayoutDirection\x94\x93\x94\x8c\vLeftToRight\x94\x86\x94R\x94.) +locale=@Variant(\0\0\0\x12\0\0\0\n\0\x65\0n\0_\0\x43\0H) +maximumSize=@Size(16777215 16777215) +minimumSize=@Size(0 0) +mouseTracking=false +palette=@Variant(\0\0\0\x44\x1\x1\xff\xffMMQQWW\0\0\x1\x1\xff\xff\xf8\xf8\xf9\xf9\xfa\xfa\0\0\x1\x1\xff\xff\xff\xff\xff\xff\xff\xff\0\0\x1\x1\xff\xff\xf5\xf5\xf5\xf5\xf5\xf5\0\0\x1\x1\xff\xff\xbf\xbf\xbf\xbf\xbf\xbf\0\0\x1\x1\xff\xff\xa9:\xa9:\xa9:\0\0\x1\x1\xff\xffMMQQWW\0\0\x1\x1\xff\xff\xff\xff\xff\xff\xff\xff\0\0\x1\x1\xff\xffMMQQWW\0\0\x1\x1\xff\xff\xf8\xf8\xf9\xf9\xfa\xfa\0\0\x1\x1\xff\xff\xf8\xf8\xf9\xf9\xfa\xfa\0\0\x1\x1\xff\xff\0\0\0\0\0\0\0\0\x1\x1\x7f\x7f\x61\x61\x9e\x9e\xef\xef\0\0\x1\x1\xff\xffMMQQWW\0\0\x1\x1\xff\xff\x1a\x1ass\xe8\xe8\0\0\x1\x1\xff\xff\x66\x66\0\0\x98\x98\0\0\x1\x1\xff\xff\xf5\xf5\xf5\xf5\xf5\xf5\0\0\x1\x1\x66\x66MMQQWW\0\0\x1\x1\xff\xff\xf8\xf8\xf9\xf9\xfa\xfa\0\0\x1\x1\xff\xff\xff\xff\xff\xff\xff\xff\0\0\x1\x1\xff\xff\xf5\xf5\xf5\xf5\xf5\xf5\0\0\x1\x1\xff\xff\xbf\xbf\xbf\xbf\xbf\xbf\0\0\x1\x1\xff\xff\xa9:\xa9:\xa9:\0\0\x1\x1\x66\x66MMQQWW\0\0\x1\x1\xff\xff\xff\xff\xff\xff\xff\xff\0\0\x1\x1\x66\x66MMQQWW\0\0\x1\x1\xff\xff\xf8\xf8\xf9\xf9\xfa\xfa\0\0\x1\x1\xff\xff\xf8\xf8\xf9\xf9\xfa\xfa\0\0\x1\x1\xff\xff\0\0\0\0\0\0\0\0\x1\x1??MMQQWW\0\0\x1\x1\x66\x66MMQQWW\0\0\x1\x1??MMQQWW\0\0\x1\x1\x66\x66MMQQWW\0\0\x1\x1\xff\xff\xf5\xf5\xf5\xf5\xf5\xf5\0\0\x1\x1\xff\xffMMQQWW\0\0\x1\x1\xff\xff\xf8\xf8\xf9\xf9\xfa\xfa\0\0\x1\x1\xff\xff\xff\xff\xff\xff\xff\xff\0\0\x1\x1\xff\xff\xf5\xf5\xf5\xf5\xf5\xf5\0\0\x1\x1\xff\xff\xbf\xbf\xbf\xbf\xbf\xbf\0\0\x1\x1\xff\xff\xa9:\xa9:\xa9:\0\0\x1\x1\xff\xffMMQQWW\0\0\x1\x1\xff\xff\xff\xff\xff\xff\xff\xff\0\0\x1\x1\xff\xffMMQQWW\0\0\x1\x1\xff\xff\xf8\xf8\xf9\xf9\xfa\xfa\0\0\x1\x1\xff\xff\xf8\xf8\xf9\xf9\xfa\xfa\0\0\x1\x1\xff\xff\0\0\0\0\0\0\0\0\x1\x1\x7f\x7f\x61\x61\x9e\x9e\xef\xef\0\0\x1\x1\xff\xffMMQQWW\0\0\x1\x1\xff\xff\x1a\x1ass\xe8\xe8\0\0\x1\x1\xff\xff\x66\x66\0\0\x98\x98\0\0\x1\x1\xff\xff\xf5\xf5\xf5\xf5\xf5\xf5\0\0) +sizeIncrement=@Size(0 0) +sizePolicy=@Variant(\0\0\0K\0\0\0U) +statusTip= +styleSheet= +tabletTracking=false +toolTip= +toolTipDuration=-1 +tooltip=BEC Queue status +updatesEnabled=true +visible=true +whatsThis= +windowFilePath= +windowIcon="@Variant(\0\0\0\x45\0\0\0\x1\x89PNG\r\n\x1a\n\0\0\0\rIHDR\0\0\0,\0\0\0,\b\x6\0\0\0\x1e\x84Z\x1\0\0\x1\x81iCCPsRGB IEC61966-2.1\0\0(\x91u\x91\xb9KCA\x10\x87\xbf\x1c\x1ehB\x4-,,\x82\x44\xab(^\x4m\x4\x13\x44\x85 !F\xf0j\x92g\xe!\xc7\xe3\xbd\x88\x4[\xc1\x36\xa0 \xdax\x15\xfa\x17h+X\v\x82\xa2\bb\x9dZ\xd1\x46\xc3s\x9e\x11\x12\xc4\xcc\x32;\xdf\xfevg\xd8\x9d\x5k$\xad\x64t\xfb\0\x64\xb2y-<\xe5w/,.\xb9\x9bJ4\xd0\x88\x13;#QEW'B\xa1 u\xed\xe3\x1\x8b\x19\xef\xfa\xccZ\xf5\xcf\xfdk\xad\xabq]\x1K\xb3\xf0\xb8\xa2jy\xe1i\xe1\xe0\x46^5yW\xb8\x43IEW\x85\xcf\x85\xbd\x9a\\P\xf8\xde\xd4\x63\x15.\x99\x9c\xac\xf0\x97\xc9Z$\x1c\0k\x9b\xb0;Y\xc3\xb1\x1aVRZFX^\x8e'\x93^W~\xef\x63\xbe\xc4\x11\xcf\xce\xcfI\xec\x16\xef\x42'\xcc\x14~\xdc\xcc\x30I\0\x1f\x83\x8c\xc9\xec\xa3\x8f!\xfa\x65\x45\x9d\xfc\x81\x9f\xfcYr\x92\xab\xc8\xacR@c\x8d$)\xf2xE]\x97\xeaq\x89\t\xd1\xe3\x32\xd2\x14\xcc\xfe\xff\xed\xab\x9e\x18\x1e\xaaTw\xf8\xa1\xe1\xc5\x30\xdez\xa0i\a\xca\x45\xc3\xf8<6\x8c\xf2\t\xd8\x9e\xe1*[\xcd\xcf\x1d\xc1\xe8\xbb\xe8\xc5\xaa\xe6\x39\x4\xd7\x16\\\\W\xb5\xd8\x1e\\nC\xe7\x93\x1a\xd5\xa2?\x92M\xdc\x9aH\xc0\xeb\x19\x38\x17\xa1\xfd\x16Z\x96+=\xfb\xdd\xe7\xf4\x11\"\x9b\xf2U7\xb0\x7f\0\xbdr\xde\xb5\xf2\r+\xf4g\xcbwA\x5\xc7\0\0\0\tpHYs\0\0=\x84\0\0=\x84\x1\xd5\xac\xaft\0\0\x6\xdaIDATX\x85\xed\x99mp\x15W\x19\xc7\x7fgor\xc3\xbd\xb9I/\x91$\xb7XB^J\x81\x84*F\xd3\x32\x86ON\xd5\nh\xb4\x36\x43p\xa0\xadT3\x9d\x86\xfa\xc1R-\x1a\x64\xca@\xeb\xe0\x94\xce\xd8\xb4\xe\x38\x3\xbe\xd1J\x98I\xc6\t\x16\x11\x61\x98\x90\x12\xa0)\x4\xa5-\xa0\xd4\xb6\xa9\x91\x4\b$$$\xdc\xdd=\xc7\xf\xbb\xf7}s\xefM\x88\xce\x38\xc3\x7f\x66gw\xcfy\xces\xfe\xe7\x7f\x9e}\xce\xd9]\xb8\x8d\xdb\xf8\xff\x86H\xc7\x66\xfb\xa7\x1f\x99\x93\xe5r\xd7(\xb4O\xe9(\x7fP\xa1\x19h\x18\n\x82\x80\x1\x18\x12\fD\xf8^W\x1a:`(\xd0\xb1\xe\x43\n\xeb\f\x4\x95\xc0\x4\x19\x84k\x86\x14\x7f\xbd\xa1\x8f\xfd\xe1\xf4\xfb\xbf\xfc;\xa0&M\xf8\x95\x5\x35\xb3\n\xb2\xf2\xb7*\xa1=\xa4\x83\x66u.\xd0U\x88\x94M\xc0>[\xe5V\xbdn\xd7\x1bv\xb9\x8e \xdc><\x90\x88\xaf\xa0\x12\xd2P\xb2\xe5\xfa\xf5K\xdf\xbf\xd0\xfb\xfb\x9e\xf1\x38i\xe3U\xec\xacX^U\xe4\xc9?\x91\xa1\x89\x87\x93\xd9M\b!y\x1c\x34\x14\xa0!\xb4\x87\xa7\xf9\n\xdf*-}\xacj<\x17\x8e\x44\x9a\xe6/\x99]\xe8\xcdi\x13\x88\xc0\x94\x10\r1U\xb1\xb7\x8e\x96\x42\x14\x66\x64\xdc\xd1\x16\b|\xbd\xd8\xa9\xde\x89\xb0\xab\xd4[\xf8\x92&D\xe1-qt\xea\xcc\xa5QQu7\xbe\\OR;!D\xa1\x37\xe7\xae\xad\x80+\xa1.\xbe\xe0\x17\xf3\x97.\xb8\xc7w\xe7i]\b-\x14_\xae|?\xca\x33\r\x13+\xf6\f\xac\xd8\x8c\xbf\xbe\xd2{\x95\xb1\x9b\x86\x63\fKM\xa3~\xf3*\x16/\xab\xa2\xe7\xc2\x45~\xb8\xba\x89\xfe\xcb\xd7\x93=\xfr\xe4\xc6\xbf\x16\xf6}\xbc\xe7o\xd1\xfc\x32\xe2\t\xcf\xf2\xe4\xd5 \"\xcag\x97\x4X\xf8\xf2\x1a\x84\x96:\x8c/\x1c=K\xebO^O(\xd7\\\x1a\xdf\xdd\xbc\x8a\xea\xa5Vh\xce*\v\xf0\xd3\x1dkX\xbb\xba\x89\xbeK\xd7\x63l\xa3\xc2[sg\xe5\x7f\r\x88!\x1c\xcf\x42\xcb\xd4\\\xf7\x46\x17\x64\xfa}\x16Y\xa5PI\xe\x80\xbc\xd9\xf9\x31\xcf\x93\0\x34-BV)\xc5\xa9\x8e\xf7\x30\f\x93\xa2\xb2\0?\xdb\xb1\x86\xbc\x19\x39\x31\x4\x62\xa6\\h\xf7\xc6s\x8cWX\b\x81?^\xa1\xd0\xc8\xf|s\x13\xba\x94\x91\xa9\xc6\x9a\xc2\x39\xcb\xaaXT\xff`B\x87\xc2%X\xbdi\x15\xf7\xdb\xca\x1e\xd8\xdd\xc1\xb6\xcd{\xb8\xef\x8b\vyz\xcb\xa3\xcc.\v\xf0\xd2\xce\x35<\xf5\xedW\x12\x94\xb6\x31=~\f\xf1\n\v\xa5\xc6Oa\xc6X\x10\x63L\xb7\xcf\x41\x8cQ\x1d},\x88\xd4\xcd\x4[M\xd3xt\xd3#Qd\x8f\xb0\xe3\xf9=(\x5\x1d\xfb\xbb\xd9\xf2\x83\xdf`\x18&\xc5\x65\x1^\xde\xd9\xc0'\xe2\x94\xb6U\xd2R\x11\x46\xa1\xc6M:\xe9,\x8b!\xc3o=\xb7\x92\xfbl\xb2\aw\x1f\xe1\xd7\x9b\x9bQ2\x12\x30G\xf6\x9f\xe2\x85g\"\xa4_\xfdU\x3\xb9\xb9\xde\xe4\xcb\x9c\x13\xe1\xa9\x80\x10P\xf9`%\0\xc3\xd7\x46hm\xda\x8bRDFl\x9f\xdb\xfts\xf2\xd8y\0J\xca\x2\xcc\x99;3\xa5(SNX\x1J\xc2\x9e\xe7\x9b\x91R\xe2\xf3g\xb3v[\x3\xbe\\oL\n\x10\x42\xf0\xbd\xc6Z\xaa\xaa\xe7\x1\xb0\xaf\xad\x8b\xee\xb7\xdfO\xe9\x7f\xca\t\x87\x14\xeal=\xcak\x1b_GJIqy\x11\xcfno \xfb\xe\xafM\x16\x9el\xac\xe5\xabu\x8b\x11\x42\xb0\xbf\xad\x8b\x8d\xebvaJ\x99\xd2\x7f\x42\x1e\x8e z\xe-\x14V\x97\x63H\x85\t\x98\xd6n\vSA^\xa9\xf3\n~\xb4\xa5\x13S\xc1\xaa\r+(\xa9(\xa2q{\x3\xcf\xd5\xbf\xca\xf2\xa7\x96\xb2\xa4n1\0\a\xda\xba\xd8\xb4n\x17\xa6\xa9\xc2\xbdN\x92p,Y!\x4\x95?^\x91\xd4\x99\x32\x63\x15R@Gk'&\xf0\xd8\x86\x15\x94V\x14\xd1\xf4\xc7\xf5\xe4\xf8\xb3\x1\xf8\xcb\xde.^\xf8\xd1.\xa4\x94\t\xfdM\x82\xb0\x85\x1b\x1f]b\xe4\xc3>\\>\xf\xa8\x88\x2N\xe7\xf3\x87\xcf\x38\xfaho\xe9\xc4\x44\xf0\xf8\x86\xba\x30\xd9\x43{\xbb\xd8\xb2\xeewae\xd3\x45J\xc2\xc1+\x83\x9cx\xe2\xe7\x8e{X\xa7\xfd\xf0xJ\x1dn\xe9\x44*X\xddXK\xfb\xbeSl]\xff\x1aRN\x8clJ\xc2i\xe7\xdd\x34q\xa8\xb5\x93?\xb7\x1e#\xa8@&\x19\\2$\xcd\x12\x13\x1f\x7fj(uk\x8eS\x86\x84\xdb\xef\x63\xd1\xc6\xc7\xc9\xf4Y\xab\x90\xf3!P\nN\xee=N\xc7\xee\xf6p\xdb\x65O.\xa1\xbc\xba<\xd6VE\xb5\xb1\xcb\xfa\xfb\ai|\xe6\xb7\xe8\x63zJ\xcdS\x12\xf6\xdf\x33\x8b\xc0\xa2\x8aTf\x96\x33o\x16oF\x11\xae~\xe8\xf3\xf8\v\x1c\xf7R1\x98\xf\xdc=w&\xdd\xa7?L\xddGZL\0\xa5\x14o\xbd\xd8\x8ci\xe7\x61I$\xf\xdf\xf5\xd9\x39\xcc{\xa0\x92\x8cL\x97\xe3lw\xbe\xd1\xc5\xf9\xee\x7f\"\x1\x33\xaa\x9d\xcb\x9d\x41\xfd\xda\x1a\x84\x10\x64\x66$\xbc\\L\x9cp\xcc\xf4(8\xd7|\x98\xa0T\tYB\x2\xf3\x1e\xa8Llc\xe3\xdd\xe3\xe7\x38\xd4r,\xe1\xad\xd9\xe5\xc9\xa2~mMZDC\xf8\xdf>tS\x90v\xa6t/\x91r\x80i(pK\xdb\xcb\x89\n2\xd5y\xdb\t\xf1\x84\x95\xb0\x42\xd2\xba\x99\x84\xc3[\r\xa3\xd8w:\x95\xe0.Aa\xa9\xd4\xe0\x64;K\xdc\xdf%1L\xa3J\xa1\x6\xe3\xad\xe3\xb3\x84\xbc\x61\x1a\xef\xba]\xee\x44O\x2\x96\x1f|\x91\xe8\x31\x87.]\xee\x8c\x84\xb2h\xac|\xb6\x96\xba\xa7\xbf\xe1P\x9f\x62x\xd2x'\x15\x61ur\xb0\xe7\x8d/\xcc\x98\xbb\x1e\x81&\0}x\x14\xa5\x14\x42\b\xdc\x39\xde\xa4\xfe\a\xff=\x10\x43\x61th\x14\x7f\x81\x9f,\x8f\x9b,\x8f\x83\b6LS\xd2\xdfw-\x81\xee\xe8\xc8\a\xfb\x88\nQp\x1e\xa2\xaf\xe5\x33+\x9b\xbd\x99\x9e\xaf\x84\xf2mNy\t\"\xdb\x13N\xf8\x86\x9d\xfc\x8d\xe8{\x5=\xe7>f\xe8\xeaH\xf8\xcb\x8f\xaf`:3J\x2Q\xed\xac\x85#\xbe\xdd\xe5\x81\x61\xde;\xdb\x1b\xf3\xb5\xe8\xa6\x31\xfa\xa7\xf.l\xab\x5\x86S\x11\xd6\xea\xee\\P\xb9\xf2\x93\xf7\xefSB\x9b\x11\xbb\x8d\xfc\xef}n\x8d\xf6\x15T\xf2\xf2\xc5\x8b\xedK\x86\x86\xba\xdf\x8eW\xd8i=Tg\x86\xfb\a\xf2\\\xdew\x8a\xb3\xf3\xbe\x84\xd0\xbc\x12\x81\xb4[:_\xa7\xaaO\xdf\xd6P\xf2\xca\xd5\xc1\x33\xdf\x19\x18\x38v\x14k\xdc\x31\x18o\x1\x37N\f\xf5|4l\x8c\x1e.\xf1\x16\x14\xb9]\x99%\x12\xc4\xe4\t\xa7\x35 \x19\xd4G\xe\xf6\xf6\x1dy\xe2\xf2\xc0\x89\x37\x81Q'b\xe9\xec\xe6\xa6\xd7\xce\xfc\xdc\xc2J\x7f\xf1\x97\xbd\xaei\xf3\xa4\x10\xb9\x6\x42\xe8\baH\xc2\xbf\0\x42_2#\xf7\xf6/\x3I\xccg-\x83\xf0/\x5\x65\xa2\xd4M%\x87\xc6\x8c\x9bg/\r\xfe\x63\x7f\xef\xc0\xf1\xd3\xc0U'e\xd3%\x1c\x82\v\xc8\xb4\xf\x17S\xb7\xa4[\x1b\x38\xfb\xaf\x81}}\x1b\xb7\x31\x11\xfc\a\x15\xfav.&\xc5\xfc~\0\0\0\0IEND\xae\x42`\x82)" +windowIconText= +windowModality=@Variant(\0\0\0\x7f\0\0\0\x18PySide::PyObjectWrapper\0\0\0\0Y\x80\x4\x95N\0\0\0\0\0\0\0\x8c\bbuiltins\x94\x8c\agetattr\x94\x93\x94\x8c\xePySide6.QtCore\x94\x8c\x11Qt.WindowModality\x94\x93\x94\x8c\bNonModal\x94\x86\x94R\x94.) +windowModified=false +windowOpacity=1 +windowTitle= + +[BECMainWindowNoRPC.AdvancedDockArea.CDockManager.ads%3A%3ACDockSplitter.ads%3A%3ACDockAreaWidget.Waveform.dockWidgetScrollArea.qt_scrollarea_viewport.Waveform] +acceptDrops=false +accessibleDescription= +accessibleIdentifier= +accessibleName= +autoFillBackground=false +auto_range_x=true +auto_range_y=true +baseSize=@Size(0 0) +color_palette=plasma +contextMenuPolicy=@Variant(\0\0\0\x7f\0\0\0\x18PySide::PyObjectWrapper\0\0\0\0\x66\x80\x4\x95[\0\0\0\0\0\0\0\x8c\bbuiltins\x94\x8c\agetattr\x94\x93\x94\x8c\xePySide6.QtCore\x94\x8c\x14Qt.ContextMenuPolicy\x94\x93\x94\x8c\x12\x44\x65\x66\x61ultContextMenu\x94\x86\x94R\x94.) +cursor=@Variant(\0\0\0J\0\0) +curve_json=[] +enable_fps_monitor=false +enable_popups=true +enable_side_panel=false +enable_toolbar=true +enabled=true +focusPolicy=@Variant(\0\0\0\x7f\0\0\0\x18PySide::PyObjectWrapper\0\0\0\0U\x80\x4\x95J\0\0\0\0\0\0\0\x8c\bbuiltins\x94\x8c\agetattr\x94\x93\x94\x8c\xePySide6.QtCore\x94\x8c\xeQt.FocusPolicy\x94\x93\x94\x8c\aNoFocus\x94\x86\x94R\x94.) +font=@Variant(\0\0\0@\0\0\0$\0.\0\x41\0p\0p\0l\0\x65\0S\0y\0s\0t\0\x65\0m\0U\0I\0\x46\0o\0n\0t@*\0\0\0\0\0\0\xff\xff\xff\xff\x5\x1\0\x32\x10) +geometry=@Rect(0 0 798 897) +inner_axes=true +inputMethodHints=@Variant(\0\0\0\x7f\0\0\0\x18PySide::PyObjectWrapper\0\0\0\0Y\x80\x4\x95N\0\0\0\0\0\0\0\x8c\bbuiltins\x94\x8c\agetattr\x94\x93\x94\x8c\xePySide6.QtCore\x94\x8c\x12Qt.InputMethodHint\x94\x93\x94\x8c\aImhNone\x94\x86\x94R\x94.) +layoutDirection=@Variant(\0\0\0\x7f\0\0\0\x18PySide::PyObjectWrapper\0\0\0\0]\x80\x4\x95R\0\0\0\0\0\0\0\x8c\bbuiltins\x94\x8c\agetattr\x94\x93\x94\x8c\xePySide6.QtCore\x94\x8c\x12Qt.LayoutDirection\x94\x93\x94\x8c\vLeftToRight\x94\x86\x94R\x94.) +legend_label_size=9 +locale=@Variant(\0\0\0\x12\0\0\0\n\0\x65\0n\0_\0\x43\0H) +lock_aspect_ratio=false +max_dataset_size_mb=10 +maximumSize=@Size(16777215 16777215) +minimal_crosshair_precision=3 +minimumSize=@Size(0 0) +mouseTracking=false +outer_axes=false +palette=@Variant(\0\0\0\x44\x1\x1\xff\xffMMQQWW\0\0\x1\x1\xff\xff\xf8\xf8\xf9\xf9\xfa\xfa\0\0\x1\x1\xff\xff\xff\xff\xff\xff\xff\xff\0\0\x1\x1\xff\xff\xf5\xf5\xf5\xf5\xf5\xf5\0\0\x1\x1\xff\xff\xbf\xbf\xbf\xbf\xbf\xbf\0\0\x1\x1\xff\xff\xa9:\xa9:\xa9:\0\0\x1\x1\xff\xffMMQQWW\0\0\x1\x1\xff\xff\xff\xff\xff\xff\xff\xff\0\0\x1\x1\xff\xffMMQQWW\0\0\x1\x1\xff\xff\xf8\xf8\xf9\xf9\xfa\xfa\0\0\x1\x1\xff\xff\xf8\xf8\xf9\xf9\xfa\xfa\0\0\x1\x1\xff\xff\0\0\0\0\0\0\0\0\x1\x1\x7f\x7f\x61\x61\x9e\x9e\xef\xef\0\0\x1\x1\xff\xffMMQQWW\0\0\x1\x1\xff\xff\x1a\x1ass\xe8\xe8\0\0\x1\x1\xff\xff\x66\x66\0\0\x98\x98\0\0\x1\x1\xff\xff\xf5\xf5\xf5\xf5\xf5\xf5\0\0\x1\x1\x66\x66MMQQWW\0\0\x1\x1\xff\xff\xf8\xf8\xf9\xf9\xfa\xfa\0\0\x1\x1\xff\xff\xff\xff\xff\xff\xff\xff\0\0\x1\x1\xff\xff\xf5\xf5\xf5\xf5\xf5\xf5\0\0\x1\x1\xff\xff\xbf\xbf\xbf\xbf\xbf\xbf\0\0\x1\x1\xff\xff\xa9:\xa9:\xa9:\0\0\x1\x1\x66\x66MMQQWW\0\0\x1\x1\xff\xff\xff\xff\xff\xff\xff\xff\0\0\x1\x1\x66\x66MMQQWW\0\0\x1\x1\xff\xff\xf8\xf8\xf9\xf9\xfa\xfa\0\0\x1\x1\xff\xff\xf8\xf8\xf9\xf9\xfa\xfa\0\0\x1\x1\xff\xff\0\0\0\0\0\0\0\0\x1\x1??MMQQWW\0\0\x1\x1\x66\x66MMQQWW\0\0\x1\x1??MMQQWW\0\0\x1\x1\x66\x66MMQQWW\0\0\x1\x1\xff\xff\xf5\xf5\xf5\xf5\xf5\xf5\0\0\x1\x1\xff\xffMMQQWW\0\0\x1\x1\xff\xff\xf8\xf8\xf9\xf9\xfa\xfa\0\0\x1\x1\xff\xff\xff\xff\xff\xff\xff\xff\0\0\x1\x1\xff\xff\xf5\xf5\xf5\xf5\xf5\xf5\0\0\x1\x1\xff\xff\xbf\xbf\xbf\xbf\xbf\xbf\0\0\x1\x1\xff\xff\xa9:\xa9:\xa9:\0\0\x1\x1\xff\xffMMQQWW\0\0\x1\x1\xff\xff\xff\xff\xff\xff\xff\xff\0\0\x1\x1\xff\xffMMQQWW\0\0\x1\x1\xff\xff\xf8\xf8\xf9\xf9\xfa\xfa\0\0\x1\x1\xff\xff\xf8\xf8\xf9\xf9\xfa\xfa\0\0\x1\x1\xff\xff\0\0\0\0\0\0\0\0\x1\x1\x7f\x7f\x61\x61\x9e\x9e\xef\xef\0\0\x1\x1\xff\xffMMQQWW\0\0\x1\x1\xff\xff\x1a\x1ass\xe8\xe8\0\0\x1\x1\xff\xff\x66\x66\0\0\x98\x98\0\0\x1\x1\xff\xff\xf5\xf5\xf5\xf5\xf5\xf5\0\0) +sizeIncrement=@Size(0 0) +sizePolicy=@Variant(\0\0\0K\0\0\0U) +skip_large_dataset_check=false +skip_large_dataset_warning=false +statusTip= +styleSheet= +tabletTracking=false +title= +toolTip= +toolTipDuration=-1 +updatesEnabled=true +visible=true +whatsThis= +windowFilePath= +windowIcon="@Variant(\0\0\0\x45\0\0\0\x1\x89PNG\r\n\x1a\n\0\0\0\rIHDR\0\0\0,\0\0\0,\b\x6\0\0\0\x1e\x84Z\x1\0\0\x1\x81iCCPsRGB IEC61966-2.1\0\0(\x91u\x91\xb9KCA\x10\x87\xbf\x1c\x1ehB\x4-,,\x82\x44\xab(^\x4m\x4\x13\x44\x85 !F\xf0j\x92g\xe!\xc7\xe3\xbd\x88\x4[\xc1\x36\xa0 \xdax\x15\xfa\x17h+X\v\x82\xa2\bb\x9dZ\xd1\x46\xc3s\x9e\x11\x12\xc4\xcc\x32;\xdf\xfevg\xd8\x9d\x5k$\xad\x64t\xfb\0\x64\xb2y-<\xe5w/,.\xb9\x9bJ4\xd0\x88\x13;#QEW'B\xa1 u\xed\xe3\x1\x8b\x19\xef\xfa\xccZ\xf5\xcf\xfdk\xad\xabq]\x1K\xb3\xf0\xb8\xa2jy\xe1i\xe1\xe0\x46^5yW\xb8\x43IEW\x85\xcf\x85\xbd\x9a\\P\xf8\xde\xd4\x63\x15.\x99\x9c\xac\xf0\x97\xc9Z$\x1c\0k\x9b\xb0;Y\xc3\xb1\x1aVRZFX^\x8e'\x93^W~\xef\x63\xbe\xc4\x11\xcf\xce\xcfI\xec\x16\xef\x42'\xcc\x14~\xdc\xcc\x30I\0\x1f\x83\x8c\xc9\xec\xa3\x8f!\xfa\x65\x45\x9d\xfc\x81\x9f\xfcYr\x92\xab\xc8\xacR@c\x8d$)\xf2xE]\x97\xeaq\x89\t\xd1\xe3\x32\xd2\x14\xcc\xfe\xff\xed\xab\x9e\x18\x1e\xaaTw\xf8\xa1\xe1\xc5\x30\xdez\xa0i\a\xca\x45\xc3\xf8<6\x8c\xf2\t\xd8\x9e\xe1*[\xcd\xcf\x1d\xc1\xe8\xbb\xe8\xc5\xaa\xe6\x39\x4\xd7\x16\\\\W\xb5\xd8\x1e\\nC\xe7\x93\x1a\xd5\xa2?\x92M\xdc\x9aH\xc0\xeb\x19\x38\x17\xa1\xfd\x16Z\x96+=\xfb\xdd\xe7\xf4\x11\"\x9b\xf2U7\xb0\x7f\0\xbdr\xde\xb5\xf2\r+\xf4g\xcbwA\x5\xc7\0\0\0\tpHYs\0\0=\x84\0\0=\x84\x1\xd5\xac\xaft\0\0\x6\xdaIDATX\x85\xed\x99mp\x15W\x19\xc7\x7fgor\xc3\xbd\xb9I/\x91$\xb7XB^J\x81\x84*F\xd3\x32\x86ON\xd5\nh\xb4\x36\x43p\xa0\xadT3\x9d\x86\xfa\xc1R-\x1a\x64\xca@\xeb\xe0\x94\xce\xd8\xb4\xe\x38\x3\xbe\xd1J\x98I\xc6\t\x16\x11\x61\x98\x90\x12\xa0)\x4\xa5-\xa0\xd4\xb6\xa9\x91\x4\b$$$\xdc\xdd=\xc7\xf\xbb\xf7}s\xefM\x88\xce\x38\xc3\x7f\x66gw\xcfy\xces\xfe\xe7\x7f\x9e}\xce\xd9]\xb8\x8d\xdb\xf8\xff\x86H\xc7\x66\xfb\xa7\x1f\x99\x93\xe5r\xd7(\xb4O\xe9(\x7fP\xa1\x19h\x18\n\x82\x80\x1\x18\x12\fD\xf8^W\x1a:`(\xd0\xb1\xe\x43\n\xeb\f\x4\x95\xc0\x4\x19\x84k\x86\x14\x7f\xbd\xa1\x8f\xfd\xe1\xf4\xfb\xbf\xfc;\xa0&M\xf8\x95\x5\x35\xb3\n\xb2\xf2\xb7*\xa1=\xa4\x83\x66u.\xd0U\x88\x94M\xc0>[\xe5V\xbdn\xd7\x1bv\xb9\x8e \xdc><\x90\x88\xaf\xa0\x12\xd2P\xb2\xe5\xfa\xf5K\xdf\xbf\xd0\xfb\xfb\x9e\xf1\x38i\xe3U\xec\xacX^U\xe4\xc9?\x91\xa1\x89\x87\x93\xd9M\b!y\x1c\x34\x14\xa0!\xb4\x87\xa7\xf9\n\xdf*-}\xacj<\x17\x8e\x44\x9a\xe6/\x99]\xe8\xcdi\x13\x88\xc0\x94\x10\r1U\xb1\xb7\x8e\x96\x42\x14\x66\x64\xdc\xd1\x16\b|\xbd\xd8\xa9\xde\x89\xb0\xab\xd4[\xf8\x92&D\xe1-qt\xea\xcc\xa5QQu7\xbe\\OR;!D\xa1\x37\xe7\xae\xad\x80+\xa1.\xbe\xe0\x17\xf3\x97.\xb8\xc7w\xe7i]\b-\x14_\xae|?\xca\x33\r\x13+\xf6\f\xac\xd8\x8c\xbf\xbe\xd2{\x95\xb1\x9b\x86\x63\fKM\xa3~\xf3*\x16/\xab\xa2\xe7\xc2\x45~\xb8\xba\x89\xfe\xcb\xd7\x93=\xfr\xe4\xc6\xbf\x16\xf6}\xbc\xe7o\xd1\xfc\x32\xe2\t\xcf\xf2\xe4\xd5 \"\xcag\x97\x4X\xf8\xf2\x1a\x84\x96:\x8c/\x1c=K\xebO^O(\xd7\\\x1a\xdf\xdd\xbc\x8a\xea\xa5Vh\xce*\v\xf0\xd3\x1dkX\xbb\xba\x89\xbeK\xd7\x63l\xa3\xc2[sg\xe5\x7f\r\x88!\x1c\xcf\x42\xcb\xd4\\\xf7\x46\x17\x64\xfa}\x16Y\xa5PI\xe\x80\xbc\xd9\xf9\x31\xcf\x93\0\x34-BV)\xc5\xa9\x8e\xf7\x30\f\x93\xa2\xb2\0?\xdb\xb1\x86\xbc\x19\x39\x31\x4\x62\xa6\\h\xf7\xc6s\x8cWX\b\x81?^\xa1\xd0\xc8\xf|s\x13\xba\x94\x91\xa9\xc6\x9a\xc2\x39\xcb\xaaXT\xff`B\x87\xc2%X\xbdi\x15\xf7\xdb\xca\x1e\xd8\xdd\xc1\xb6\xcd{\xb8\xef\x8b\vyz\xcb\xa3\xcc.\v\xf0\xd2\xce\x35<\xf5\xedW\x12\x94\xb6\x31=~\f\xf1\n\v\xa5\xc6Oa\xc6X\x10\x63L\xb7\xcf\x41\x8cQ\x1d},\x88\xd4\xcd\x4[M\xd3xt\xd3#Qd\x8f\xb0\xe3\xf9=(\x5\x1d\xfb\xbb\xd9\xf2\x83\xdf`\x18&\xc5\x65\x1^\xde\xd9\xc0'\xe2\x94\xb6U\xd2R\x11\x46\xa1\xc6M:\xe9,\x8b!\xc3o=\xb7\x92\xfbl\xb2\aw\x1f\xe1\xd7\x9b\x9bQ2\x12\x30G\xf6\x9f\xe2\x85g\"\xa4_\xfdU\x3\xb9\xb9\xde\xe4\xcb\x9c\x13\xe1\xa9\x80\x10P\xf9`%\0\xc3\xd7\x46hm\xda\x8bRDFl\x9f\xdb\xfts\xf2\xd8y\0J\xca\x2\xcc\x99;3\xa5(SNX\x1J\xc2\x9e\xe7\x9b\x91R\xe2\xf3g\xb3v[\x3\xbe\\oL\n\x10\x42\xf0\xbd\xc6Z\xaa\xaa\xe7\x1\xb0\xaf\xad\x8b\xee\xb7\xdfO\xe9\x7f\xca\t\x87\x14\xeal=\xcak\x1b_GJIqy\x11\xcfno \xfb\xe\xafM\x16\x9el\xac\xe5\xabu\x8b\x11\x42\xb0\xbf\xad\x8b\x8d\xebvaJ\x99\xd2\x7f\x42\x1e\x8e z\xe-\x14V\x97\x63H\x85\t\x98\xd6n\vSA^\xa9\xf3\n~\xb4\xa5\x13S\xc1\xaa\r+(\xa9(\xa2q{\x3\xcf\xd5\xbf\xca\xf2\xa7\x96\xb2\xa4n1\0\a\xda\xba\xd8\xb4n\x17\xa6\xa9\xc2\xbdN\x92p,Y!\x4\x95?^\x91\xd4\x99\x32\x63\x15R@Gk'&\xf0\xd8\x86\x15\x94V\x14\xd1\xf4\xc7\xf5\xe4\xf8\xb3\x1\xf8\xcb\xde.^\xf8\xd1.\xa4\x94\t\xfdM\x82\xb0\x85\x1b\x1f]b\xe4\xc3>\\>\xf\xa8\x88\x2N\xe7\xf3\x87\xcf\x38\xfaho\xe9\xc4\x44\xf0\xf8\x86\xba\x30\xd9\x43{\xbb\xd8\xb2\xeewae\xd3\x45J\xc2\xc1+\x83\x9cx\xe2\xe7\x8e{X\xa7\xfd\xf0xJ\x1dn\xe9\x44*X\xddXK\xfb\xbeSl]\xff\x1aRN\x8clJ\xc2i\xe7\xdd\x34q\xa8\xb5\x93?\xb7\x1e#\xa8@&\x19\\2$\xcd\x12\x13\x1f\x7fj(uk\x8eS\x86\x84\xdb\xef\x63\xd1\xc6\xc7\xc9\xf4Y\xab\x90\xf3!P\nN\xee=N\xc7\xee\xf6p\xdb\x65O.\xa1\xbc\xba<\xd6VE\xb5\xb1\xcb\xfa\xfb\ai|\xe6\xb7\xe8\x63zJ\xcdS\x12\xf6\xdf\x33\x8b\xc0\xa2\x8aTf\x96\x33o\x16oF\x11\xae~\xe8\xf3\xf8\v\x1c\xf7R1\x98\xf\xdc=w&\xdd\xa7?L\xddGZL\0\xa5\x14o\xbd\xd8\x8ci\xe7\x61I$\xf\xdf\xf5\xd9\x39\xcc{\xa0\x92\x8cL\x97\xe3lw\xbe\xd1\xc5\xf9\xee\x7f\"\x1\x33\xaa\x9d\xcb\x9d\x41\xfd\xda\x1a\x84\x10\x64\x66$\xbc\\L\x9cp\xcc\xf4(8\xd7|\x98\xa0T\tYB\x2\xf3\x1e\xa8Llc\xe3\xdd\xe3\xe7\x38\xd4r,\xe1\xad\xd9\xe5\xc9\xa2~mMZDC\xf8\xdf>tS\x90v\xa6t/\x91r\x80i(pK\xdb\xcb\x89\n2\xd5y\xdb\t\xf1\x84\x95\xb0\x42\xd2\xba\x99\x84\xc3[\r\xa3\xd8w:\x95\xe0.Aa\xa9\xd4\xe0\x64;K\xdc\xdf%1L\xa3J\xa1\x6\xe3\xad\xe3\xb3\x84\xbc\x61\x1a\xef\xba]\xee\x44O\x2\x96\x1f|\x91\xe8\x31\x87.]\xee\x8c\x84\xb2h\xac|\xb6\x96\xba\xa7\xbf\xe1P\x9f\x62x\xd2x'\x15\x61ur\xb0\xe7\x8d/\xcc\x98\xbb\x1e\x81&\0}x\x14\xa5\x14\x42\b\xdc\x39\xde\xa4\xfe\a\xff=\x10\x43\x61th\x14\x7f\x81\x9f,\x8f\x9b,\x8f\x83\b6LS\xd2\xdfw-\x81\xee\xe8\xc8\a\xfb\x88\nQp\x1e\xa2\xaf\xe5\x33+\x9b\xbd\x99\x9e\xaf\x84\xf2mNy\t\"\xdb\x13N\xf8\x86\x9d\xfc\x8d\xe8{\x5=\xe7>f\xe8\xeaH\xf8\xcb\x8f\xaf`:3J\x2Q\xed\xac\x85#\xbe\xdd\xe5\x81\x61\xde;\xdb\x1b\xf3\xb5\xe8\xa6\x31\xfa\xa7\xf.l\xab\x5\x86S\x11\xd6\xea\xee\\P\xb9\xf2\x93\xf7\xefSB\x9b\x11\xbb\x8d\xfc\xef}n\x8d\xf6\x15T\xf2\xf2\xc5\x8b\xedK\x86\x86\xba\xdf\x8eW\xd8i=Tg\x86\xfb\a\xf2\\\xdew\x8a\xb3\xf3\xbe\x84\xd0\xbc\x12\x81\xb4[:_\xa7\xaaO\xdf\xd6P\xf2\xca\xd5\xc1\x33\xdf\x19\x18\x38v\x14k\xdc\x31\x18o\x1\x37N\f\xf5|4l\x8c\x1e.\xf1\x16\x14\xb9]\x99%\x12\xc4\xe4\t\xa7\x35 \x19\xd4G\xe\xf6\xf6\x1dy\xe2\xf2\xc0\x89\x37\x81Q'b\xe9\xec\xe6\xa6\xd7\xce\xfc\xdc\xc2J\x7f\xf1\x97\xbd\xaei\xf3\xa4\x10\xb9\x6\x42\xe8\baH\xc2\xbf\0\x42_2#\xf7\xf6/\x3I\xccg-\x83\xf0/\x5\x65\xa2\xd4M%\x87\xc6\x8c\x9bg/\r\xfe\x63\x7f\xef\xc0\xf1\xd3\xc0U'e\xd3%\x1c\x82\v\xc8\xb4\xf\x17S\xb7\xa4[\x1b\x38\xfb\xaf\x81}}\x1b\xb7\x31\x11\xfc\a\x15\xfav.&\xc5\xfc~\0\0\0\0IEND\xae\x42`\x82)" +windowIconText= +windowModality=@Variant(\0\0\0\x7f\0\0\0\x18PySide::PyObjectWrapper\0\0\0\0Y\x80\x4\x95N\0\0\0\0\0\0\0\x8c\bbuiltins\x94\x8c\agetattr\x94\x93\x94\x8c\xePySide6.QtCore\x94\x8c\x11Qt.WindowModality\x94\x93\x94\x8c\bNonModal\x94\x86\x94R\x94.) +windowModified=false +windowOpacity=1 +windowTitle= +x_entry= +x_grid=false +x_label= +x_limits=@Variant(\0\0\0\x1a\0\0\0\0\0\0\0\0?\xf0\0\0\0\0\0\0) +x_log=false +x_mode=auto +y_grid=false +y_label= +y_limits=@Variant(\0\0\0\x1a\0\0\0\0\0\0\0\0?\xf0\0\0\0\0\0\0) +y_log=false + +[BECMainWindowNoRPC.AdvancedDockArea.ModularToolBar.QWidget.DarkModeButton] +acceptDrops=false +accessibleDescription= +accessibleIdentifier= +accessibleName= +autoFillBackground=false +baseSize=@Size(0 0) +contextMenuPolicy=@Variant(\0\0\0\x7f\0\0\0\x18PySide::PyObjectWrapper\0\0\0\0\x66\x80\x4\x95[\0\0\0\0\0\0\0\x8c\bbuiltins\x94\x8c\agetattr\x94\x93\x94\x8c\xePySide6.QtCore\x94\x8c\x14Qt.ContextMenuPolicy\x94\x93\x94\x8c\x12\x44\x65\x66\x61ultContextMenu\x94\x86\x94R\x94.) +cursor=@Variant(\0\0\0J\0\0) +dark_mode_enabled=false +enabled=true +focusPolicy=@Variant(\0\0\0\x7f\0\0\0\x18PySide::PyObjectWrapper\0\0\0\0U\x80\x4\x95J\0\0\0\0\0\0\0\x8c\bbuiltins\x94\x8c\agetattr\x94\x93\x94\x8c\xePySide6.QtCore\x94\x8c\xeQt.FocusPolicy\x94\x93\x94\x8c\aNoFocus\x94\x86\x94R\x94.) +font=@Variant(\0\0\0@\0\0\0$\0.\0\x41\0p\0p\0l\0\x65\0S\0y\0s\0t\0\x65\0m\0U\0I\0\x46\0o\0n\0t@*\0\0\0\0\0\0\xff\xff\xff\xff\x5\x1\0\x32\x10) +geometry=@Rect(0 0 40 40) +inputMethodHints=@Variant(\0\0\0\x7f\0\0\0\x18PySide::PyObjectWrapper\0\0\0\0Y\x80\x4\x95N\0\0\0\0\0\0\0\x8c\bbuiltins\x94\x8c\agetattr\x94\x93\x94\x8c\xePySide6.QtCore\x94\x8c\x12Qt.InputMethodHint\x94\x93\x94\x8c\aImhNone\x94\x86\x94R\x94.) +layoutDirection=@Variant(\0\0\0\x7f\0\0\0\x18PySide::PyObjectWrapper\0\0\0\0]\x80\x4\x95R\0\0\0\0\0\0\0\x8c\bbuiltins\x94\x8c\agetattr\x94\x93\x94\x8c\xePySide6.QtCore\x94\x8c\x12Qt.LayoutDirection\x94\x93\x94\x8c\vLeftToRight\x94\x86\x94R\x94.) +locale=@Variant(\0\0\0\x12\0\0\0\n\0\x65\0n\0_\0\x43\0H) +maximumSize=@Size(40 40) +minimumSize=@Size(40 40) +mouseTracking=false +palette=@Variant(\0\0\0\x44\x1\x1\xff\xffMMQQWW\0\0\x1\x1\xff\xff\xf8\xf8\xf9\xf9\xfa\xfa\0\0\x1\x1\xff\xff\xff\xff\xff\xff\xff\xff\0\0\x1\x1\xff\xff\xf5\xf5\xf5\xf5\xf5\xf5\0\0\x1\x1\xff\xff\xbf\xbf\xbf\xbf\xbf\xbf\0\0\x1\x1\xff\xff\xa9:\xa9:\xa9:\0\0\x1\x1\xff\xffMMQQWW\0\0\x1\x1\xff\xff\xff\xff\xff\xff\xff\xff\0\0\x1\x1\xff\xffMMQQWW\0\0\x1\x1\xff\xff\xf8\xf8\xf9\xf9\xfa\xfa\0\0\x1\x1\xff\xff\xf8\xf8\xf9\xf9\xfa\xfa\0\0\x1\x1\xff\xff\0\0\0\0\0\0\0\0\x1\x1\x7f\x7f\x61\x61\x9e\x9e\xef\xef\0\0\x1\x1\xff\xffMMQQWW\0\0\x1\x1\xff\xff\x1a\x1ass\xe8\xe8\0\0\x1\x1\xff\xff\x66\x66\0\0\x98\x98\0\0\x1\x1\xff\xff\xf5\xf5\xf5\xf5\xf5\xf5\0\0\x1\x1\x66\x66MMQQWW\0\0\x1\x1\xff\xff\xf8\xf8\xf9\xf9\xfa\xfa\0\0\x1\x1\xff\xff\xff\xff\xff\xff\xff\xff\0\0\x1\x1\xff\xff\xf5\xf5\xf5\xf5\xf5\xf5\0\0\x1\x1\xff\xff\xbf\xbf\xbf\xbf\xbf\xbf\0\0\x1\x1\xff\xff\xa9:\xa9:\xa9:\0\0\x1\x1\x66\x66MMQQWW\0\0\x1\x1\xff\xff\xff\xff\xff\xff\xff\xff\0\0\x1\x1\x66\x66MMQQWW\0\0\x1\x1\xff\xff\xf8\xf8\xf9\xf9\xfa\xfa\0\0\x1\x1\xff\xff\xf8\xf8\xf9\xf9\xfa\xfa\0\0\x1\x1\xff\xff\0\0\0\0\0\0\0\0\x1\x1??MMQQWW\0\0\x1\x1\x66\x66MMQQWW\0\0\x1\x1??MMQQWW\0\0\x1\x1\x66\x66MMQQWW\0\0\x1\x1\xff\xff\xf5\xf5\xf5\xf5\xf5\xf5\0\0\x1\x1\xff\xffMMQQWW\0\0\x1\x1\xff\xff\xf8\xf8\xf9\xf9\xfa\xfa\0\0\x1\x1\xff\xff\xff\xff\xff\xff\xff\xff\0\0\x1\x1\xff\xff\xf5\xf5\xf5\xf5\xf5\xf5\0\0\x1\x1\xff\xff\xbf\xbf\xbf\xbf\xbf\xbf\0\0\x1\x1\xff\xff\xa9:\xa9:\xa9:\0\0\x1\x1\xff\xffMMQQWW\0\0\x1\x1\xff\xff\xff\xff\xff\xff\xff\xff\0\0\x1\x1\xff\xffMMQQWW\0\0\x1\x1\xff\xff\xf8\xf8\xf9\xf9\xfa\xfa\0\0\x1\x1\xff\xff\xf8\xf8\xf9\xf9\xfa\xfa\0\0\x1\x1\xff\xff\0\0\0\0\0\0\0\0\x1\x1\x7f\x7f\x61\x61\x9e\x9e\xef\xef\0\0\x1\x1\xff\xffMMQQWW\0\0\x1\x1\xff\xff\x1a\x1ass\xe8\xe8\0\0\x1\x1\xff\xff\x66\x66\0\0\x98\x98\0\0\x1\x1\xff\xff\xf5\xf5\xf5\xf5\xf5\xf5\0\0) +sizeIncrement=@Size(0 0) +sizePolicy=@Variant(\0\0\0K\0\0\0U) +statusTip= +styleSheet= +tabletTracking=false +toolTip= +toolTipDuration=-1 +updatesEnabled=true +visible=true +whatsThis= +windowFilePath= +windowIcon="@Variant(\0\0\0\x45\0\0\0\x1\x89PNG\r\n\x1a\n\0\0\0\rIHDR\0\0\0,\0\0\0,\b\x6\0\0\0\x1e\x84Z\x1\0\0\x1\x81iCCPsRGB IEC61966-2.1\0\0(\x91u\x91\xb9KCA\x10\x87\xbf\x1c\x1ehB\x4-,,\x82\x44\xab(^\x4m\x4\x13\x44\x85 !F\xf0j\x92g\xe!\xc7\xe3\xbd\x88\x4[\xc1\x36\xa0 \xdax\x15\xfa\x17h+X\v\x82\xa2\bb\x9dZ\xd1\x46\xc3s\x9e\x11\x12\xc4\xcc\x32;\xdf\xfevg\xd8\x9d\x5k$\xad\x64t\xfb\0\x64\xb2y-<\xe5w/,.\xb9\x9bJ4\xd0\x88\x13;#QEW'B\xa1 u\xed\xe3\x1\x8b\x19\xef\xfa\xccZ\xf5\xcf\xfdk\xad\xabq]\x1K\xb3\xf0\xb8\xa2jy\xe1i\xe1\xe0\x46^5yW\xb8\x43IEW\x85\xcf\x85\xbd\x9a\\P\xf8\xde\xd4\x63\x15.\x99\x9c\xac\xf0\x97\xc9Z$\x1c\0k\x9b\xb0;Y\xc3\xb1\x1aVRZFX^\x8e'\x93^W~\xef\x63\xbe\xc4\x11\xcf\xce\xcfI\xec\x16\xef\x42'\xcc\x14~\xdc\xcc\x30I\0\x1f\x83\x8c\xc9\xec\xa3\x8f!\xfa\x65\x45\x9d\xfc\x81\x9f\xfcYr\x92\xab\xc8\xacR@c\x8d$)\xf2xE]\x97\xeaq\x89\t\xd1\xe3\x32\xd2\x14\xcc\xfe\xff\xed\xab\x9e\x18\x1e\xaaTw\xf8\xa1\xe1\xc5\x30\xdez\xa0i\a\xca\x45\xc3\xf8<6\x8c\xf2\t\xd8\x9e\xe1*[\xcd\xcf\x1d\xc1\xe8\xbb\xe8\xc5\xaa\xe6\x39\x4\xd7\x16\\\\W\xb5\xd8\x1e\\nC\xe7\x93\x1a\xd5\xa2?\x92M\xdc\x9aH\xc0\xeb\x19\x38\x17\xa1\xfd\x16Z\x96+=\xfb\xdd\xe7\xf4\x11\"\x9b\xf2U7\xb0\x7f\0\xbdr\xde\xb5\xf2\r+\xf4g\xcbwA\x5\xc7\0\0\0\tpHYs\0\0=\x84\0\0=\x84\x1\xd5\xac\xaft\0\0\x6\xdaIDATX\x85\xed\x99mp\x15W\x19\xc7\x7fgor\xc3\xbd\xb9I/\x91$\xb7XB^J\x81\x84*F\xd3\x32\x86ON\xd5\nh\xb4\x36\x43p\xa0\xadT3\x9d\x86\xfa\xc1R-\x1a\x64\xca@\xeb\xe0\x94\xce\xd8\xb4\xe\x38\x3\xbe\xd1J\x98I\xc6\t\x16\x11\x61\x98\x90\x12\xa0)\x4\xa5-\xa0\xd4\xb6\xa9\x91\x4\b$$$\xdc\xdd=\xc7\xf\xbb\xf7}s\xefM\x88\xce\x38\xc3\x7f\x66gw\xcfy\xces\xfe\xe7\x7f\x9e}\xce\xd9]\xb8\x8d\xdb\xf8\xff\x86H\xc7\x66\xfb\xa7\x1f\x99\x93\xe5r\xd7(\xb4O\xe9(\x7fP\xa1\x19h\x18\n\x82\x80\x1\x18\x12\fD\xf8^W\x1a:`(\xd0\xb1\xe\x43\n\xeb\f\x4\x95\xc0\x4\x19\x84k\x86\x14\x7f\xbd\xa1\x8f\xfd\xe1\xf4\xfb\xbf\xfc;\xa0&M\xf8\x95\x5\x35\xb3\n\xb2\xf2\xb7*\xa1=\xa4\x83\x66u.\xd0U\x88\x94M\xc0>[\xe5V\xbdn\xd7\x1bv\xb9\x8e \xdc><\x90\x88\xaf\xa0\x12\xd2P\xb2\xe5\xfa\xf5K\xdf\xbf\xd0\xfb\xfb\x9e\xf1\x38i\xe3U\xec\xacX^U\xe4\xc9?\x91\xa1\x89\x87\x93\xd9M\b!y\x1c\x34\x14\xa0!\xb4\x87\xa7\xf9\n\xdf*-}\xacj<\x17\x8e\x44\x9a\xe6/\x99]\xe8\xcdi\x13\x88\xc0\x94\x10\r1U\xb1\xb7\x8e\x96\x42\x14\x66\x64\xdc\xd1\x16\b|\xbd\xd8\xa9\xde\x89\xb0\xab\xd4[\xf8\x92&D\xe1-qt\xea\xcc\xa5QQu7\xbe\\OR;!D\xa1\x37\xe7\xae\xad\x80+\xa1.\xbe\xe0\x17\xf3\x97.\xb8\xc7w\xe7i]\b-\x14_\xae|?\xca\x33\r\x13+\xf6\f\xac\xd8\x8c\xbf\xbe\xd2{\x95\xb1\x9b\x86\x63\fKM\xa3~\xf3*\x16/\xab\xa2\xe7\xc2\x45~\xb8\xba\x89\xfe\xcb\xd7\x93=\xfr\xe4\xc6\xbf\x16\xf6}\xbc\xe7o\xd1\xfc\x32\xe2\t\xcf\xf2\xe4\xd5 \"\xcag\x97\x4X\xf8\xf2\x1a\x84\x96:\x8c/\x1c=K\xebO^O(\xd7\\\x1a\xdf\xdd\xbc\x8a\xea\xa5Vh\xce*\v\xf0\xd3\x1dkX\xbb\xba\x89\xbeK\xd7\x63l\xa3\xc2[sg\xe5\x7f\r\x88!\x1c\xcf\x42\xcb\xd4\\\xf7\x46\x17\x64\xfa}\x16Y\xa5PI\xe\x80\xbc\xd9\xf9\x31\xcf\x93\0\x34-BV)\xc5\xa9\x8e\xf7\x30\f\x93\xa2\xb2\0?\xdb\xb1\x86\xbc\x19\x39\x31\x4\x62\xa6\\h\xf7\xc6s\x8cWX\b\x81?^\xa1\xd0\xc8\xf|s\x13\xba\x94\x91\xa9\xc6\x9a\xc2\x39\xcb\xaaXT\xff`B\x87\xc2%X\xbdi\x15\xf7\xdb\xca\x1e\xd8\xdd\xc1\xb6\xcd{\xb8\xef\x8b\vyz\xcb\xa3\xcc.\v\xf0\xd2\xce\x35<\xf5\xedW\x12\x94\xb6\x31=~\f\xf1\n\v\xa5\xc6Oa\xc6X\x10\x63L\xb7\xcf\x41\x8cQ\x1d},\x88\xd4\xcd\x4[M\xd3xt\xd3#Qd\x8f\xb0\xe3\xf9=(\x5\x1d\xfb\xbb\xd9\xf2\x83\xdf`\x18&\xc5\x65\x1^\xde\xd9\xc0'\xe2\x94\xb6U\xd2R\x11\x46\xa1\xc6M:\xe9,\x8b!\xc3o=\xb7\x92\xfbl\xb2\aw\x1f\xe1\xd7\x9b\x9bQ2\x12\x30G\xf6\x9f\xe2\x85g\"\xa4_\xfdU\x3\xb9\xb9\xde\xe4\xcb\x9c\x13\xe1\xa9\x80\x10P\xf9`%\0\xc3\xd7\x46hm\xda\x8bRDFl\x9f\xdb\xfts\xf2\xd8y\0J\xca\x2\xcc\x99;3\xa5(SNX\x1J\xc2\x9e\xe7\x9b\x91R\xe2\xf3g\xb3v[\x3\xbe\\oL\n\x10\x42\xf0\xbd\xc6Z\xaa\xaa\xe7\x1\xb0\xaf\xad\x8b\xee\xb7\xdfO\xe9\x7f\xca\t\x87\x14\xeal=\xcak\x1b_GJIqy\x11\xcfno \xfb\xe\xafM\x16\x9el\xac\xe5\xabu\x8b\x11\x42\xb0\xbf\xad\x8b\x8d\xebvaJ\x99\xd2\x7f\x42\x1e\x8e z\xe-\x14V\x97\x63H\x85\t\x98\xd6n\vSA^\xa9\xf3\n~\xb4\xa5\x13S\xc1\xaa\r+(\xa9(\xa2q{\x3\xcf\xd5\xbf\xca\xf2\xa7\x96\xb2\xa4n1\0\a\xda\xba\xd8\xb4n\x17\xa6\xa9\xc2\xbdN\x92p,Y!\x4\x95?^\x91\xd4\x99\x32\x63\x15R@Gk'&\xf0\xd8\x86\x15\x94V\x14\xd1\xf4\xc7\xf5\xe4\xf8\xb3\x1\xf8\xcb\xde.^\xf8\xd1.\xa4\x94\t\xfdM\x82\xb0\x85\x1b\x1f]b\xe4\xc3>\\>\xf\xa8\x88\x2N\xe7\xf3\x87\xcf\x38\xfaho\xe9\xc4\x44\xf0\xf8\x86\xba\x30\xd9\x43{\xbb\xd8\xb2\xeewae\xd3\x45J\xc2\xc1+\x83\x9cx\xe2\xe7\x8e{X\xa7\xfd\xf0xJ\x1dn\xe9\x44*X\xddXK\xfb\xbeSl]\xff\x1aRN\x8clJ\xc2i\xe7\xdd\x34q\xa8\xb5\x93?\xb7\x1e#\xa8@&\x19\\2$\xcd\x12\x13\x1f\x7fj(uk\x8eS\x86\x84\xdb\xef\x63\xd1\xc6\xc7\xc9\xf4Y\xab\x90\xf3!P\nN\xee=N\xc7\xee\xf6p\xdb\x65O.\xa1\xbc\xba<\xd6VE\xb5\xb1\xcb\xfa\xfb\ai|\xe6\xb7\xe8\x63zJ\xcdS\x12\xf6\xdf\x33\x8b\xc0\xa2\x8aTf\x96\x33o\x16oF\x11\xae~\xe8\xf3\xf8\v\x1c\xf7R1\x98\xf\xdc=w&\xdd\xa7?L\xddGZL\0\xa5\x14o\xbd\xd8\x8ci\xe7\x61I$\xf\xdf\xf5\xd9\x39\xcc{\xa0\x92\x8cL\x97\xe3lw\xbe\xd1\xc5\xf9\xee\x7f\"\x1\x33\xaa\x9d\xcb\x9d\x41\xfd\xda\x1a\x84\x10\x64\x66$\xbc\\L\x9cp\xcc\xf4(8\xd7|\x98\xa0T\tYB\x2\xf3\x1e\xa8Llc\xe3\xdd\xe3\xe7\x38\xd4r,\xe1\xad\xd9\xe5\xc9\xa2~mMZDC\xf8\xdf>tS\x90v\xa6t/\x91r\x80i(pK\xdb\xcb\x89\n2\xd5y\xdb\t\xf1\x84\x95\xb0\x42\xd2\xba\x99\x84\xc3[\r\xa3\xd8w:\x95\xe0.Aa\xa9\xd4\xe0\x64;K\xdc\xdf%1L\xa3J\xa1\x6\xe3\xad\xe3\xb3\x84\xbc\x61\x1a\xef\xba]\xee\x44O\x2\x96\x1f|\x91\xe8\x31\x87.]\xee\x8c\x84\xb2h\xac|\xb6\x96\xba\xa7\xbf\xe1P\x9f\x62x\xd2x'\x15\x61ur\xb0\xe7\x8d/\xcc\x98\xbb\x1e\x81&\0}x\x14\xa5\x14\x42\b\xdc\x39\xde\xa4\xfe\a\xff=\x10\x43\x61th\x14\x7f\x81\x9f,\x8f\x9b,\x8f\x83\b6LS\xd2\xdfw-\x81\xee\xe8\xc8\a\xfb\x88\nQp\x1e\xa2\xaf\xe5\x33+\x9b\xbd\x99\x9e\xaf\x84\xf2mNy\t\"\xdb\x13N\xf8\x86\x9d\xfc\x8d\xe8{\x5=\xe7>f\xe8\xeaH\xf8\xcb\x8f\xaf`:3J\x2Q\xed\xac\x85#\xbe\xdd\xe5\x81\x61\xde;\xdb\x1b\xf3\xb5\xe8\xa6\x31\xfa\xa7\xf.l\xab\x5\x86S\x11\xd6\xea\xee\\P\xb9\xf2\x93\xf7\xefSB\x9b\x11\xbb\x8d\xfc\xef}n\x8d\xf6\x15T\xf2\xf2\xc5\x8b\xedK\x86\x86\xba\xdf\x8eW\xd8i=Tg\x86\xfb\a\xf2\\\xdew\x8a\xb3\xf3\xbe\x84\xd0\xbc\x12\x81\xb4[:_\xa7\xaaO\xdf\xd6P\xf2\xca\xd5\xc1\x33\xdf\x19\x18\x38v\x14k\xdc\x31\x18o\x1\x37N\f\xf5|4l\x8c\x1e.\xf1\x16\x14\xb9]\x99%\x12\xc4\xe4\t\xa7\x35 \x19\xd4G\xe\xf6\xf6\x1dy\xe2\xf2\xc0\x89\x37\x81Q'b\xe9\xec\xe6\xa6\xd7\xce\xfc\xdc\xc2J\x7f\xf1\x97\xbd\xaei\xf3\xa4\x10\xb9\x6\x42\xe8\baH\xc2\xbf\0\x42_2#\xf7\xf6/\x3I\xccg-\x83\xf0/\x5\x65\xa2\xd4M%\x87\xc6\x8c\x9bg/\r\xfe\x63\x7f\xef\xc0\xf1\xd3\xc0U'e\xd3%\x1c\x82\v\xc8\xb4\xf\x17S\xb7\xa4[\x1b\x38\xfb\xaf\x81}}\x1b\xb7\x31\x11\xfc\a\x15\xfav.&\xc5\xfc~\0\0\0\0IEND\xae\x42`\x82)" +windowIconText= +windowModality=@Variant(\0\0\0\x7f\0\0\0\x18PySide::PyObjectWrapper\0\0\0\0Y\x80\x4\x95N\0\0\0\0\0\0\0\x8c\bbuiltins\x94\x8c\agetattr\x94\x93\x94\x8c\xePySide6.QtCore\x94\x8c\x11Qt.WindowModality\x94\x93\x94\x8c\bNonModal\x94\x86\x94R\x94.) +windowModified=false +windowOpacity=1 +windowTitle= + +[BECMainWindowNoRPC.AdvancedDockArea.dockSettingsAction] +autoRepeat=true +checkable=false +checked=false +enabled=true +font=@Variant(\0\0\0@\0\0\0$\0.\0\x41\0p\0p\0l\0\x65\0S\0y\0s\0t\0\x65\0m\0U\0I\0\x46\0o\0n\0t@*\0\0\0\0\0\0\xff\xff\xff\xff\x5\x1\0\x32\x10) +icon="@Variant(\0\0\0\x45\0\0\0\x1\x89PNG\r\n\x1a\n\0\0\0\rIHDR\0\0\0,\0\0\0,\b\x6\0\0\0\x1e\x84Z\x1\0\0\0\tpHYs\0\0\v\x13\0\0\v\x13\x1\0\x9a\x9c\x18\0\0\x4\xc9IDATX\x85\xed\x99]lSu\x18\xc6\x7f\xef\xe9\x6\x85\xb0\xa0\xc1\x8b\xe9\x2\x46\xe3`\xa0!\xa0\x88\x6\x35*\x9a\xb9\x96\xb6\v\xed*\x82\x80\x46\x12\xe3\x95\x1a\x14I\xc4\x8b\xde(~$&&\x10/d\x17\x80\xdf]7i\xb7u \xc1%\x9a\x18\xd4\xa0\xf1\x3\x18j\"D\x3\xc6\xc4\x44\x18\xee\xa3=\xff\xd7\v$t]{\xce\x61\xad\xf3\x66\xcf\xddy\xdf\xe7<\xefs\xcey\xfb?\xef\xf9\x17\xa6\x30\x85\x31\x90j\x88\xb4\x84\xa2\xab\x5iu\xe2(\xba\xaf\xaf\xbb\xb3\xab\xd2Z5\x95\n\0 \xb2\x1a\xd8\xe0\x42\xb2\x80\x8a\r[\x95\n\0\b,\xf4\xc0i\xaa\x46\xadj\x18\x16\xbc\x99i\xa2\n-X\xb1\xe1P(~\r0\xcb\x3\xb5.\x10\x88\x36TZ\xcf\xd1p(\x14o\b\x6\xe3\xf5N\x9c\x9c\xcf\xb8\xb6\xc3\x45H\xad:r\x83\xc1x}(\x14w\xbc(G\xc3y1;\xd5gN\x6\xc3\xb1]\xcd\xe1\xd8\xb8\xc7\x1e\x88\xc4\x96Z\x86-\xde\xec\x82\xaal[\xd5\xda\xb6\xa2\x38\xde\x12\x89/h\tG\xdfR\x9f\x39i\x8b\xd9\xe1\xa4Q\xb6\xa7VE\xa2\xf7\x1a\x95\x43\x45\xe1\x8c\x31\xf2Z~\x9a}tZNv \xf2\x90W\xb3\x63\xabJ\x97\xed\xb3\x9f\xf0\xd9\xd2\xa8*[\x4\"\x85^,\xd1\x95=\xe9\xceO<\x1b\x8e\xc7\xe3\xbe\xc1\x11\xf3\x15\xca\x92\x32%\x87\x1\xff\x84\xccz\xd1\x10\xbe\x99\x35\xddZ\x96L&\xed\xe2T\xc9\x96\x18\x1c\x31\x8f\x38\x98\xa5l\xa1\xcb\x43y\re\xc9\xf9!\xddX*5\xee\xeG\"\x91\xba\x9c\xd6\x9e\0\x1c\x7fl\x93\x80\x33\x43~\xab\xb1?\x99\x1c,\f\x8e\xbb\xc3\x39\xad\xdd\xca\xffo\x16\xa0~\xe6\x88n-\xe\x8e\xb9\xc3\xe1p|^\x1e\x33@u\x1ey50\\\x83\xb5 \x93I\x9e\xba\x18\x18s\x87m\xb1\x1f\xa6\x12\xb3\xa2{\xc0\xba\xa7Vr\xf5\x62[W\x83\xb9\xf\xf8`\xc2z\xe0\xcf\x61\xd6\x15\x6\xc6\f?\xcbo^\xfc\xca\xe1#\xdf\x8e\xa2\xb2\x1d\xa8\xbd\f\xe1?Ty\xb4/\xd3\xd9[\x14?\x3\x1cj\x89\xb4}(\xaa\xed\xc0\x15\x97\xa1\x39\x82\xf0l_:\xb5\xb3\x30XrYk\tGo\x15\xe4}\xe0z/\xcajh\xe9\xebI\xedw\xe2\x4\xc3\xb1\x98\x42\x87G\xb3\xdfY\xc6\xac\xeb\xe9\xe9\xfa\xbe\x38QrY\xeb\xcbt~I\xde\xbf\x14x\xd7MY\xa0\xdd\xcd,@o&\x95\xc2K{(o\f\x9d\xab[^\xca,8\xcc\xc3\xd9\xec;g\x81\xf5\x81p\xb4\x1\xe4\xeer<\xa3V\xbb\xab\x89\x7f!b\xdaU\xad\x35\x65\xf3\xca\xc1\xde\xee\xd4\xd3N\x1an\xd3\x9a\"\xe2\x38\x89\xf9kr\xc7\\4.\x89\xe5\xc4\x91\xab\x16W\xbaiTe\x80\x9fL\xb8\x19\x16T\a\x9d\b\xc3\xf9\xda\xaa\x8d\x97\xa8\xfc\xe5\xa6Q\xb6\x87\xef\x8f\xc7gO\x1b\x36o*\x94\xed_\0K\xcc&\xe0s\xb7\x42\0\xaa\xd6&\x17\xc6\xca`8\xb6+?2\xf3\xa9\x3\a\xf6\x9e/\xc5(\xb9\xac\x5[c\xb7\x61xO\xe1:OF\xaa\xbd\xac\t'\x8c\xea\xda\xfd\x99\xce#\xc5)_\xe1\x41\"\x91\xb0\x66\xd4]\xf5\x1c\xca\xdb\xc0\x1cO\xe2\x80\b\xf\xdc\x30\x7f\xd1\xf?\x9d\x38\xf6\x63\xa9|K\xa4-\n\xb4\xe3\xfd-:G\x90\xc7\x1a\x9b\x16\xfe\xbd~\xed\x9a\xc3\xfd\xfd\xfdz\xe9Z\n\x10\b\xb7=\xf\xfa\xa2W\xa3\xc5P\xd8+X\xed\x62\x33`\xfbm\x91Qk\x11\xa2\x8f\v<8QM\x90m\xd9L\xc7K\x17\x8f\xc6\xf4\xb0m\xe5\xf6\xfaL\xcd\v\xc0\x8c\tI\xc3\x6\x30\x1b\xd4\aVN@\xd4\xfd$g\f\x91\x37{\n\x3\x63Z\xe2\xe7\x81\x81\xb3\x8d\v\x16NwzQL2\xb6g{:\xd3\x85\x81q\xcb\xda\x90\xdf\xf7*pz\xd2,\x95\xc7\xe9!\xbf\xf5Zq\xd0W\x1c\xf8\xe5\xe8\xd1\xd1\xf9\xf3o\xfc\x13\xc1q\xaf\xec\xbf\x86\xa2O\x1e\xecJ}Q\x1c/\xf9\xe2X\xbe\xec\xa6\xdd\xc0\xd7\xez\xc3U\xf0\xe4\xa4q\xe4\xf6[\x16\xef)\x95(i8\x91H\x18U\xd9\\\x14V\xe0#\xcb\x92;\xec\x1as-\x90\x9a\x98OP\xd8G\xde\x9a\x87\xe8\x9d\n\xfbJ\xd8\xda\x9cH$L\xa9s\x1d\xf7\xba\x2\xe1X\x17\x10\0v\xabX\xaf\xf7\xa5\x93\x3\x85\xf9U\xadm+T\xf5\x65U\xee\xf2\x62T\x84Om[\xb6\xed\xef\xe9\xf8\xec\x82\xef\vh\xe\xc7\x9a,\xf4\x19\x41\x36\"\xd2\x93MwD\xcbj8\x15\b\x85\xe2\r\x96\x35\x92O\xa7\xd3\xbf\x97\xbf\xa8\xb6\x66P\xd7y\xf8\x82\x61\xd3\xdc\x9b\xee\xfa\xb8\\>\x18\x8c\xd7[\x16\xbe\xee\xee\xe4o\x13\x32\xec\x5\xcd\xad\xads}\xa6\xe6\x94;\x13\xc8\xeb\xdcl\xb6\xf3\xd7J\xeaUc\a^\x2\xe1\xd8Y\xdcw0\xcf\x65\x33\xa9\xd9\x14\xb4\xc2\x44P\x8dyX\x81\xe3\x1ex\xc7\xa9\xd0,Ti\x80Wp\xff\xea\x10\xf5\xfc\x65\xe2\x84\xaa\xfc\xc7\x61!)\xd0Q'\x8e\x81\xb4S~\nS\x98 \xfe\x1\x1\xb5\x93\xa4\x97\x89\xb7\xcb\0\0\0\0IEND\xae\x42`\x82)" +iconText=Dock settings +iconVisibleInMenu=false +menuRole=@Variant(\0\0\0\x7f\0\0\0\x18PySide::PyObjectWrapper\0\0\0\0`\x80\x4\x95U\0\0\0\0\0\0\0\x8c\bbuiltins\x94\x8c\agetattr\x94\x93\x94\x8c\rPySide6.QtGui\x94\x8c\x10QAction.MenuRole\x94\x93\x94\x8c\x11TextHeuristicRole\x94\x86\x94R\x94.) +priority=@Variant(\0\0\0\x7f\0\0\0\x18PySide::PyObjectWrapper\0\0\0\0]\x80\x4\x95R\0\0\0\0\0\0\0\x8c\bbuiltins\x94\x8c\agetattr\x94\x93\x94\x8c\rPySide6.QtGui\x94\x8c\x10QAction.Priority\x94\x93\x94\x8c\xeNormalPriority\x94\x86\x94R\x94.) +shortcut= +shortcutContext=@Variant(\0\0\0\x7f\0\0\0\x18PySide::PyObjectWrapper\0\0\0\0`\x80\x4\x95U\0\0\0\0\0\0\0\x8c\bbuiltins\x94\x8c\agetattr\x94\x93\x94\x8c\xePySide6.QtCore\x94\x8c\x12Qt.ShortcutContext\x94\x93\x94\x8c\xeWindowShortcut\x94\x86\x94R\x94.) +shortcutVisibleInContextMenu=false +statusTip= +text=Dock settings +toolTip=Dock settings +visible=true +whatsThis= + +[Perspectives] +1\Name=test +1\State="@ByteArray(\0\0\x1\xb3x\xdau\x90\x41O\xc3\x30\f\x85\xef\xfc\n+\xf7\xd1\xae\x12h\x87\x34\xd3V\xd8\x11\x98\xba\xb1sh\xcc\x14\xadMP\x92V\x3\xf1\xe3q\n\x8aV\x4\xa7\xd8\xcf/\xdfK\xcc\x97\xe7\xae\x85\x1\x9d\xd7\xd6\x94l~\x9d\x33@\xd3X\xa5\xcd\xb1\x64\xfb\xdd\x66\xb6`K\xc1\xb7\x61\xa5\x6i\x1aTw\xb6\x39\xd1\xac~\xf7\x1;xN\x17\x19\xec=\xba\xd4\x13\xa6\xb2&HmH\x89\x63\xc1S\xf\x9b\xd6\xca\x30\x6\xe4\xa4\xd7o\xad\xe\x81\xe4G\xa7\x91,a\x4|F@oB\xc9\n\xf2\xac\x1cJ\xd8\xc9\x97\x11\x5U\xef\x1c\xc6\xd1\x41\xe\xf8j]G\x8e\x83VG\f\xf0 ;\xbc\xd0\xa1j\xadG\x15\x83\x32\xc1\xb3\x88\x99\xc0\x8a\v\xd8\xfa\xbe\xda\xf6\xd8\xe3oX\xd2\xa7\xb0\x89\xe7\xc9z\x1d\xdf\x8dnm\xcf\x7f\xa7\xd6\xfa\x3\xbdX\xe4\x5\xcc\x8b\x9b[\xe0\xd9\xb7@\xe7\xcf\xff\xa9L+\xa2\xfa\x9f\x95\x8b\xab/_\xa2\x8f\x42)" +size=1 + +[mainWindow] +DockingState="@ByteArray(\0\0\x1\xb3x\xdau\x90\x41O\xc3\x30\f\x85\xef\xfc\n+\xf7\xd1\xae\x12h\x87\x34\xd3V\xd8\x11\x98\xba\xb1sh\xcc\x14\xadMP\x92V\x3\xf1\xe3q\n\x8aV\x4\xa7\xd8\xcf/\xdfK\xcc\x97\xe7\xae\x85\x1\x9d\xd7\xd6\x94l~\x9d\x33@\xd3X\xa5\xcd\xb1\x64\xfb\xdd\x66\xb6`K\xc1\xb7\x61\xa5\x6i\x1aTw\xb6\x39\xd1\xac~\xf7\x1;xN\x17\x19\xec=\xba\xd4\x13\xa6\xb2&HmH\x89\x63\xc1S\xf\x9b\xd6\xca\x30\x6\xe4\xa4\xd7o\xad\xe\x81\xe4G\xa7\x91,a\x4|F@oB\xc9\n\xf2\xac\x1cJ\xd8\xc9\x97\x11\x5U\xef\x1c\xc6\xd1\x41\xe\xf8j]G\x8e\x83VG\f\xf0 ;\xbc\xd0\xa1j\xadG\x15\x83\x32\xc1\xb3\x88\x99\xc0\x8a\v\xd8\xfa\xbe\xda\xf6\xd8\xe3oX\xd2\xa7\xb0\x89\xe7\xc9z\x1d\xdf\x8dnm\xcf\x7f\xa7\xd6\xfa\x3\xbdX\xe4\x5\xcc\x8b\x9b[\xe0\xd9\xb7@\xe7\xcf\xff\xa9L+\xa2\xfa\x9f\x95\x8b\xab/_\xa2\x8f\x42)" +Geometry=@ByteArray(\x1\xd9\xd0\xcb\0\x3\0\0\0\0\0\0\0\0\0\x1d\0\0\b\x1a\0\0\x3\xea\0\0\0\0\0\0\0\0\xff\xff\xff\xff\xff\xff\xff\xff\0\0\0\x1\0\0\0\0\xf\0\0\0\0\0\0\0\0\x1d\0\0\b\x1a\0\0\x3\xea) +State=@Variant(\0\0\0\x7f\0\0\0\x18PySide::PyObjectWrapper\0\0\0\0\xf\x80\x4\x95\x4\0\0\0\0\0\0\0\x43\0\x94.) + +[manifest] +widgets\1\closable=true +widgets\1\floatable=true +widgets\1\movable=true +widgets\1\object_name=BECQueue +widgets\1\widget_class=BECQueue +widgets\2\closable=true +widgets\2\floatable=true +widgets\2\movable=true +widgets\2\object_name=PositionerBox +widgets\2\widget_class=PositionerBox +widgets\3\closable=true +widgets\3\floatable=true +widgets\3\movable=true +widgets\3\object_name=Waveform +widgets\3\widget_class=Waveform +widgets\size=3 diff --git a/bec_widgets/widgets/control/device_manager/components/__init__.py b/bec_widgets/widgets/control/device_manager/components/__init__.py index e69de29b..bec612ee 100644 --- a/bec_widgets/widgets/control/device_manager/components/__init__.py +++ b/bec_widgets/widgets/control/device_manager/components/__init__.py @@ -0,0 +1,4 @@ +from .device_table_view import DeviceTableView +from .dm_config_view import DMConfigView +from .dm_docstring_view import DocstringView +from .dm_ophyd_test import DMOphydTest diff --git a/bec_widgets/widgets/control/device_manager/components/device_table_view.py b/bec_widgets/widgets/control/device_manager/components/device_table_view.py index 40f31cec..423b8f06 100644 --- a/bec_widgets/widgets/control/device_manager/components/device_table_view.py +++ b/bec_widgets/widgets/control/device_manager/components/device_table_view.py @@ -3,16 +3,18 @@ from __future__ import annotations import copy -import json +import time from bec_lib.logger import bec_logger from bec_qthemes import material_icon from qtpy import QtCore, QtGui, QtWidgets from thefuzz import fuzz +from bec_widgets.utils.bec_signal_proxy import BECSignalProxy from bec_widgets.utils.bec_widget import BECWidget -from bec_widgets.utils.colors import get_accent_colors +from bec_widgets.utils.colors import get_accent_colors, get_theme_palette from bec_widgets.utils.error_popups import SafeSlot +from bec_widgets.widgets.control.device_manager.components.dm_ophyd_test import ValidationStatus logger = bec_logger.logger @@ -30,21 +32,25 @@ class DictToolTipDelegate(QtWidgets.QStyledItemDelegate): model: DeviceFilterProxyModel = index.model() model_index = model.mapToSource(index) row_dict = model.sourceModel().get_row_data(model_index) - QtWidgets.QToolTip.showText(event.globalPos(), row_dict["description"], view) + description = row_dict.get("description", "") + QtWidgets.QToolTip.showText(event.globalPos(), description, view) return True class CenterCheckBoxDelegate(DictToolTipDelegate): """Custom checkbox delegate to center checkboxes in table cells.""" - def __init__(self, parent=None): + def __init__(self, parent=None, colors=None): super().__init__(parent) - colors = get_accent_colors() + self._colors = colors if colors else get_accent_colors() self._icon_checked = material_icon( - "check_box", size=QtCore.QSize(16, 16), color=colors.default, filled=True + "check_box", size=QtCore.QSize(16, 16), color=self._colors.default, filled=True ) self._icon_unchecked = material_icon( - "check_box_outline_blank", size=QtCore.QSize(16, 16), color=colors.default, filled=True + "check_box_outline_blank", + size=QtCore.QSize(16, 16), + color=self._colors.default, + filled=True, ) def apply_theme(self, theme: str | None = None): @@ -75,9 +81,51 @@ class CenterCheckBoxDelegate(DictToolTipDelegate): return model.setData(index, new_state, QtCore.Qt.CheckStateRole) +class DeviceValidatedDelegate(DictToolTipDelegate): + """Custom delegate for displaying validated device configurations.""" + + def __init__(self, parent=None, colors=None): + super().__init__(parent) + self._colors = colors if colors else get_accent_colors() + self._icons = { + ValidationStatus.PENDING: material_icon( + icon_name="circle", size=(12, 12), color=self._colors.default, filled=True + ), + ValidationStatus.VALID: material_icon( + icon_name="circle", size=(12, 12), color=self._colors.success, filled=True + ), + ValidationStatus.FAILED: material_icon( + icon_name="circle", size=(12, 12), color=self._colors.emergency, filled=True + ), + } + + def apply_theme(self, theme: str | None = None): + colors = get_accent_colors() + for status, icon in self._icons.items(): + icon.setColor(colors[status]) + + def paint(self, painter, option, index): + status = index.model().data(index, QtCore.Qt.DisplayRole) + if status is None: + return super().paint(painter, option, index) + + pixmap = self._icons.get(status) + if pixmap: + rect = option.rect + pix_rect = pixmap.rect() + pix_rect.moveCenter(rect.center()) + painter.drawPixmap(pix_rect.topLeft(), pixmap) + + super().paint(painter, option, index) + + class WrappingTextDelegate(DictToolTipDelegate): """Custom delegate for wrapping text in table cells.""" + def __init__(self, table: BECTableView, parent=None): + super().__init__(parent) + self._table = table + def paint(self, painter, option, index): text = index.model().data(index, QtCore.Qt.DisplayRole) if not text: @@ -91,12 +139,14 @@ class WrappingTextDelegate(DictToolTipDelegate): def sizeHint(self, option, index): text = str(index.model().data(index, QtCore.Qt.DisplayRole) or "") - # if not text: - # return super().sizeHint(option, index) + column_width = self._table.columnWidth(index.column()) - 8 # -4 & 4 - # Use the actual column width - table = index.model().parent() # or store reference to QTableView - column_width = table.columnWidth(index.column()) # - 8 + # Avoid pathological heights for too-narrow columns + min_width = option.fontMetrics.averageCharWidth() * 4 + if column_width < min_width: + fm = QtGui.QFontMetrics(option.font) + elided = fm.elidedText(text, QtCore.Qt.ElideRight, column_width) + return QtCore.QSize(column_width, fm.height() + 4) doc = QtGui.QTextDocument() doc.setDefaultFont(option.font) @@ -104,8 +154,25 @@ class WrappingTextDelegate(DictToolTipDelegate): doc.setPlainText(text) layout_height = doc.documentLayout().documentSize().height() - height = int(layout_height) + 4 # Needs some extra padding, otherwise it gets cut off - return QtCore.QSize(column_width, height) + return QtCore.QSize(column_width, int(layout_height) + 4) + + # def sizeHint(self, option, index): + # text = str(index.model().data(index, QtCore.Qt.DisplayRole) or "") + # # if not text: + # # return super().sizeHint(option, index) + + # # Use the actual column width + # table = index.model().parent() # or store reference to QTableView + # column_width = table.columnWidth(index.column()) # - 8 + + # doc = QtGui.QTextDocument() + # doc.setDefaultFont(option.font) + # doc.setTextWidth(column_width) + # doc.setPlainText(text) + + # layout_height = doc.documentLayout().documentSize().height() + # height = int(layout_height) + 4 # Needs some extra padding, otherwise it gets cut off + # return QtCore.QSize(column_width, height) class DeviceTableModel(QtCore.QAbstractTableModel): @@ -115,10 +182,16 @@ class DeviceTableModel(QtCore.QAbstractTableModel): Sort logic is implemented directly on the data of the table view. """ - def __init__(self, device_config: list[dict] | None = None, parent=None): + device_configs_added = QtCore.Signal(dict) # Dict[str, dict] of configs that were added + devices_removed = QtCore.Signal(list) # List of strings with device names that were removed + + def __init__(self, parent=None): super().__init__(parent) - self._device_config = device_config or [] + self._device_config: dict[str, dict] = {} + self._list_items: list[dict] = [] + self._validation_status: dict[str, ValidationStatus] = {} self.headers = [ + "", "name", "deviceClass", "readoutPriority", @@ -133,7 +206,7 @@ class DeviceTableModel(QtCore.QAbstractTableModel): ############################################### def rowCount(self, parent=QtCore.QModelIndex()) -> int: - return len(self._device_config) + return len(self._list_items) def columnCount(self, parent=QtCore.QModelIndex()) -> int: return len(self.headers) @@ -147,15 +220,20 @@ class DeviceTableModel(QtCore.QAbstractTableModel): """Return the row data for the given index.""" if not index.isValid(): return {} - return copy.deepcopy(self._device_config[index.row()]) + return copy.deepcopy(self._list_items[index.row()]) def data(self, index, role=QtCore.Qt.DisplayRole): """Return data for the given index and role.""" if not index.isValid(): return None row, col = index.row(), index.column() + + if col == 0 and role == QtCore.Qt.DisplayRole: # QtCore.Qt.DisplayRole: + dev_name = self._list_items[row].get("name", "") + return self._validation_status.get(dev_name, ValidationStatus.PENDING) + key = self.headers[col] - value = self._device_config[row].get(key) + value = self._list_items[row].get(key) if role == QtCore.Qt.DisplayRole: if key in ("enabled", "readOnly"): @@ -210,7 +288,7 @@ class DeviceTableModel(QtCore.QAbstractTableModel): if key in ("enabled", "readOnly") and role == QtCore.Qt.CheckStateRole: if not self._checkable_columns_enabled.get(key, True): return False # ignore changes if column is disabled - self._device_config[row][key] = value == QtCore.Qt.Checked + self._list_items[row][key] = value == QtCore.Qt.Checked self.dataChanged.emit(index, index, [QtCore.Qt.CheckStateRole]) return True return False @@ -219,87 +297,115 @@ class DeviceTableModel(QtCore.QAbstractTableModel): ############ Public methods ######## #################################### - def get_device_config(self) -> list[dict]: - """Return the current device config (with checkbox updates applied).""" + def get_device_config(self) -> dict[str, dict]: + """Method to get the device configuration.""" return self._device_config - def set_checkbox_enabled(self, column_name: str, enabled: bool): + def add_device_configs(self, device_configs: dict[str, dict]): """ - Enable/Disable the checkbox column. + Add devices to the model. Args: - column_name (str): The name of the column to modify. - enabled (bool): Whether the checkbox should be enabled or disabled. + device_configs (dict[str, dict]): A dictionary of device configurations to add. """ - if column_name in self._checkable_columns_enabled: - self._checkable_columns_enabled[column_name] = enabled - col = self.headers.index(column_name) - top_left = self.index(0, col) - bottom_right = self.index(self.rowCount() - 1, col) - self.dataChanged.emit( - top_left, bottom_right, [QtCore.Qt.CheckStateRole, QtCore.Qt.DisplayRole] - ) + already_in_list = [] + for k, cfg in device_configs.items(): + if k in self._device_config: + logger.warning(f"Device {k} already exists in the model.") + already_in_list.append(k) + continue + self._device_config[k] = cfg + new_list_cfg = copy.deepcopy(cfg) + new_list_cfg["name"] = k + row = len(self._list_items) + self.beginInsertRows(QtCore.QModelIndex(), row, row) + self._list_items.append(new_list_cfg) + self.endInsertRows() + for k in already_in_list: + device_configs.pop(k) + self.device_configs_added.emit(device_configs) - def set_device_config(self, device_config: list[dict]): + def set_device_config(self, device_configs: dict[str, dict]): """ Replace the device config. Args: - device_config (list[dict]): The new device config to set. + device_config (dict[str, dict]): The new device config to set. """ + diff_names = set(device_configs.keys()) - set(self._device_config.keys()) self.beginResetModel() - self._device_config = list(device_config) + self._device_config.clear() + self._list_items.clear() + for k, cfg in device_configs.items(): + self._device_config[k] = cfg + new_list_cfg = copy.deepcopy(cfg) + new_list_cfg["name"] = k + self._list_items.append(new_list_cfg) self.endResetModel() + self.devices_removed.emit(diff_names) + self.device_configs_added.emit(device_configs) - @SafeSlot(dict) - def add_device(self, device: dict): + def remove_device_configs(self, device_configs: dict[str, dict]): """ - Add an extra device to the device config at the bottom. + Remove devices from the model. Args: - device (dict): The device configuration to add. + device_configs (dict[str, dict]): A dictionary of device configurations to remove. """ - row = len(self._device_config) - self.beginInsertRows(QtCore.QModelIndex(), row, row) - self._device_config.append(device) - self.endInsertRows() - - @SafeSlot(int) - def remove_device_by_row(self, row: int): - """ - Remove one device row by index. This maps to the row to the source of the data model - - Args: - row (int): The index of the device row to remove. - """ - if 0 <= row < len(self._device_config): + removed = [] + for k in device_configs.keys(): + if k not in self._device_config: + logger.warning(f"Device {k} does not exist in the model.") + continue + new_cfg = self._device_config.pop(k) + new_cfg["name"] = k + row = self._list_items.index(new_cfg) self.beginRemoveRows(QtCore.QModelIndex(), row, row) - self._device_config.pop(row) + self._list_items.pop(row) self.endRemoveRows() + removed.append(k) + self.devices_removed.emit(removed) - @SafeSlot(list) - def remove_devices_by_rows(self, rows: list[int]): + def clear_table(self): """ - Remove multiple device rows by their indices. + Clear the table. + """ + device_names = list(self._device_config.keys()) + self.beginResetModel() + self._device_config.clear() + self._list_items.clear() + self.endResetModel() + self.devices_removed.emit(device_names) + + def update_validation_status(self, device_name: str, status: int | ValidationStatus): + """ + Handle device status changes. Args: - rows (list[int]): The indices of the device rows to remove. + device_name (str): The name of the device. + status (int): The new status of the device. """ - for row in sorted(rows, reverse=True): - self.remove_device_by_row(row) - - @SafeSlot(str) - def remove_device_by_name(self, name: str): - """ - Remove one device row by name. - - Args: - name (str): The name of the device to remove. - """ - for row, device in enumerate(self._device_config): - if device.get("name") == name: - self.remove_device_by_row(row) + if isinstance(status, int): + status = ValidationStatus(status) + if device_name not in self._device_config: + logger.warning( + f"Device {device_name} not found in device_config dict {self._device_config}" + ) + return + self._validation_status[device_name] = status + row = None + for ii, item in enumerate(self._list_items): + if item["name"] == device_name: + row = ii break + if row is None: + logger.warning( + f"Device {device_name} not found in device_status dict {self._validation_status}" + ) + return + # Emit dataChanged for column 0 (status column) + index = self.index(row, 0) + self.dataChanged.emit(index, index, [QtCore.Qt.DisplayRole]) class BECTableView(QtWidgets.QTableView): @@ -319,12 +425,7 @@ class BECTableView(QtWidgets.QTableView): if not proxy_indexes: return - # Get unique rows (proxy indices) in reverse order so removal indexes stay valid - proxy_rows = sorted({idx.row() for idx in proxy_indexes}, reverse=True) - # Map to source model rows - source_rows = [ - self.model().mapToSource(self.model().index(row, 0)).row() for row in proxy_rows - ] + source_rows = self._get_source_rows(proxy_indexes) model: DeviceTableModel = self.model().sourceModel() # access underlying model # Delegate confirmation and removal to helper @@ -332,14 +433,28 @@ class BECTableView(QtWidgets.QTableView): if not removed: return + def _get_source_rows(self, proxy_indexes: list[QtWidgets.QModelIndex]) -> list[int]: + """ + Map proxy model indices to source model row indices. + + Args: + proxy_indexes (list[QModelIndex]): List of proxy model indices. + + Returns: + list[int]: List of source model row indices. + """ + proxy_rows = sorted({idx for idx in proxy_indexes}, reverse=True) + source_rows = [self.model().mapToSource(idx).row() for idx in proxy_rows] + return list(set(source_rows)) + def _confirm_and_remove_rows(self, model: DeviceTableModel, source_rows: list[int]) -> bool: """ Prompt the user to confirm removal of rows and remove them from the model if accepted. Returns True if rows were removed, False otherwise. """ - cfg = model.get_device_config() - names = [str(cfg[r].get("name", "")) for r in sorted(source_rows)] + configs = [model._list_items[r] for r in sorted(source_rows)] + names = [cfg.get("name", "") for cfg in configs] msg = QtWidgets.QMessageBox(self) msg.setIcon(QtWidgets.QMessageBox.Warning) @@ -354,8 +469,8 @@ class BECTableView(QtWidgets.QTableView): res = msg.exec_() if res == QtWidgets.QMessageBox.Ok: - model.remove_devices_by_rows(source_rows) - # TODO add signal for removed devices + configs_to_be_removed = {model._device_config[name] for name in names} + model.remove_device_configs(configs_to_be_removed) return True return False @@ -367,7 +482,7 @@ class DeviceFilterProxyModel(QtCore.QSortFilterProxyModel): self._hidden_rows = set() self._filter_text = "" self._enable_fuzzy = True - self._filter_columns = [0, 1] # name and deviceClass for search + self._filter_columns = [1, 2] # name and deviceClass for search def hide_rows(self, row_indices: list[int]): """ @@ -431,11 +546,12 @@ class DeviceFilterProxyModel(QtCore.QSortFilterProxyModel): class DeviceTableView(BECWidget, QtWidgets.QWidget): """Device Table View for the device manager.""" - selected_device = QtCore.Signal(dict) + selected_device = QtCore.Signal(dict) # Selected device configuration dict[str,dict] + device_configs_added = QtCore.Signal(dict) # Dict[str, dict] of configs that were added + devices_removed = QtCore.Signal(list) # List of strings with device names that were removed RPC = False PLUGIN = False - devices_removed = QtCore.Signal(list) def __init__(self, parent=None, client=None): super().__init__(client=client, parent=parent, theme_update=True) @@ -452,6 +568,10 @@ class DeviceTableView(BECWidget, QtWidgets.QWidget): self.layout.addLayout(self.search_controls) self.layout.addWidget(self.table) + # Connect signals + self._model.devices_removed.connect(self.devices_removed.emit) + self._model.device_configs_added.connect(self.device_configs_added.emit) + def _setup_search(self): """Create components related to the search functionality""" @@ -492,43 +612,48 @@ class DeviceTableView(BECWidget, QtWidgets.QWidget): """Setup the table view.""" # Model + Proxy self.table = BECTableView(self) - self.model = DeviceTableModel(parent=self.table) + self._model = DeviceTableModel(parent=self.table) self.proxy = DeviceFilterProxyModel(parent=self.table) - self.proxy.setSourceModel(self.model) + self.proxy.setSourceModel(self._model) self.table.setModel(self.proxy) self.table.setSortingEnabled(True) # Delegates - self.checkbox_delegate = CenterCheckBoxDelegate(self.table) + colors = get_accent_colors() + self.checkbox_delegate = CenterCheckBoxDelegate(self.table, colors=colors) self.wrap_delegate = WrappingTextDelegate(self.table) self.tool_tip_delegate = DictToolTipDelegate(self.table) - self.table.setItemDelegateForColumn(0, self.tool_tip_delegate) # name - self.table.setItemDelegateForColumn(1, self.tool_tip_delegate) # deviceClass - self.table.setItemDelegateForColumn(2, self.tool_tip_delegate) # readoutPriority - self.table.setItemDelegateForColumn(3, self.wrap_delegate) # deviceTags - self.table.setItemDelegateForColumn(4, self.checkbox_delegate) # enabled - self.table.setItemDelegateForColumn(5, self.checkbox_delegate) # readOnly + self.validated_delegate = DeviceValidatedDelegate(self.table, colors=colors) + self.table.setItemDelegateForColumn(0, self.validated_delegate) # ValidationStatus + self.table.setItemDelegateForColumn(1, self.tool_tip_delegate) # name + self.table.setItemDelegateForColumn(2, self.tool_tip_delegate) # deviceClass + self.table.setItemDelegateForColumn(3, self.tool_tip_delegate) # readoutPriority + self.table.setItemDelegateForColumn(4, self.wrap_delegate) # deviceTags + self.table.setItemDelegateForColumn(5, self.checkbox_delegate) # enabled + self.table.setItemDelegateForColumn(6, self.checkbox_delegate) # readOnly # Column resize policies - # TODO maybe we need here a flexible header options as deviceClass - # may get quite long for beamlines plugin repos header = self.table.horizontalHeader() - header.setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeToContents) # name - header.setSectionResizeMode(1, QtWidgets.QHeaderView.ResizeToContents) # deviceClass - header.setSectionResizeMode(2, QtWidgets.QHeaderView.ResizeToContents) # readoutPriority - header.setSectionResizeMode(3, QtWidgets.QHeaderView.Stretch) # deviceTags - header.setSectionResizeMode(4, QtWidgets.QHeaderView.Fixed) # enabled - header.setSectionResizeMode(5, QtWidgets.QHeaderView.Fixed) # readOnly + header.setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed) # ValidationStatus + header.setSectionResizeMode(1, QtWidgets.QHeaderView.ResizeToContents) # name + header.setSectionResizeMode(2, QtWidgets.QHeaderView.ResizeToContents) # deviceClass + header.setSectionResizeMode(3, QtWidgets.QHeaderView.ResizeToContents) # readoutPriority + header.setSectionResizeMode(4, QtWidgets.QHeaderView.Stretch) # deviceTags + header.setSectionResizeMode(5, QtWidgets.QHeaderView.Fixed) # enabled + header.setSectionResizeMode(6, QtWidgets.QHeaderView.Fixed) # readOnly - self.table.setColumnWidth(3, 70) - self.table.setColumnWidth(4, 70) + self.table.setColumnWidth(0, 25) + self.table.setColumnWidth(5, 70) + self.table.setColumnWidth(6, 70) # Ensure column widths stay fixed - header.setMinimumSectionSize(70) + header.setMinimumSectionSize(25) header.setDefaultSectionSize(90) # Enable resizing of column - header.sectionResized.connect(self.on_table_resized) + self._geometry_resize_proxy = BECSignalProxy( + header.geometriesChanged, rateLimit=10, slot=self._on_table_resized + ) # Selection behavior self.table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows) @@ -539,28 +664,26 @@ class DeviceTableView(BECWidget, QtWidgets.QWidget): # QtCore.QTimer.singleShot(0, lambda: header.sectionResized.emit(0, 0, 0)) - def device_config(self) -> list[dict]: + def get_device_config(self) -> dict[str, dict]: """Get the device config.""" - return self.model.get_device_config() + return self._model.get_device_config() def apply_theme(self, theme: str | None = None): self.checkbox_delegate.apply_theme(theme) + self.validated_delegate.apply_theme(theme) ###################################### ########### Slot API ################# ###################################### - @SafeSlot(int, int, int) - def on_table_resized(self, column, old_width, new_width): + @SafeSlot() + def _on_table_resized(self, *args): """Handle changes to the table column resizing.""" - if column != len(self.model.headers) - 1: - return - - for row in range(self.table.model().rowCount()): - index = self.table.model().index(row, column) - delegate = self.table.itemDelegate(index) - option = QtWidgets.QStyleOptionViewItem() - height = delegate.sizeHint(option, index).height() + option = QtWidgets.QStyleOptionViewItem() + model = self.table.model() + for row in range(model.rowCount()): + index = model.index(row, 4) + height = self.wrap_delegate.sizeHint(option, index).height() self.table.setRowHeight(row, height) @SafeSlot(QtCore.QItemSelection, QtCore.QItemSelection) @@ -582,86 +705,106 @@ class DeviceTableView(BECWidget, QtWidgets.QWidget): source_indexes = [self.proxy.mapToSource(idx) for idx in selected_indexes] source_rows = {idx.row() for idx in source_indexes} - # Ignore if multiple are selected - if len(source_rows) > 1: - self.selected_device.emit({}) - return - - # Get the single row - (row,) = source_rows - source_index = self.model.index(row, 0) # pick column 0 or whichever - device = self.model.get_row_data(source_index) - self.selected_device.emit(device) - - @SafeSlot(QtCore.QModelIndex) - def _on_row_selected(self, index: QtCore.QModelIndex): - """Handle row selection in the device table.""" - if not index.isValid(): - return - source_index = self.proxy.mapToSource(index) - device = self.model.get_device_at_index(source_index) - self.selected_device.emit(device) + configs = [copy.deepcopy(self._model._list_items[r]) for r in sorted(source_rows)] + names = [cfg.pop("name") for cfg in configs] + selected_cfgs = {name: cfg for name, cfg in zip(names, configs)} + self.selected_device.emit(selected_cfgs) ###################################### ##### Ext. Slot API ################# ###################################### - @SafeSlot(list) - def set_device_config(self, config: list[dict]): + @SafeSlot(dict) + def set_device_config(self, device_configs: dict[str, dict]): """ Set the device config. Args: - config (list[dict]): The device config to set. + config (dict[str,dict]): The device config to set. """ - self.model.set_device_config(config) + self._model.set_device_config(device_configs) @SafeSlot() - def clear_device_config(self): - """ - Clear the device config. - """ - self.model.set_device_config([]) + def clear_device_configs(self): + """Clear the device configs.""" + self._model.clear_table() @SafeSlot(dict) - def add_device(self, device: dict): + def add_device_configs(self, device_configs: dict[str, dict]): """ - Add a device to the config. + Add devices to the config. Args: - device (dict): The device to add. + device_configs (dict[str, dict]): The device configs to add. """ - self.model.add_device(device) + self._model.add_device_configs(device_configs) + + @SafeSlot(dict) + def remove_device_configs(self, device_configs: dict[str, dict]): + """ + Remove devices from the config. + + Args: + device_configs (dict[str, dict]): The device configs to remove. + """ + self._model.remove_device_configs(device_configs) - @SafeSlot(int) @SafeSlot(str) - def remove_device(self, dev: int | str): + def remove_device(self, device_name: str): """ - Remove the device from the config either by row id, or device name. + Remove a device from the config. Args: - dev (int | str): The device to remove, either by row id or device name. + device_name (str): The name of the device to remove. """ - if isinstance(dev, int): - # TODO test this properly, check with proxy index and source index - # Use the proxy model to map to the correct row - model_source_index = self.table.model().mapToSource(self.table.model().index(dev, 0)) - self.model.remove_device_by_row(model_source_index.row()) - return - if isinstance(dev, str): - self.model.remove_device_by_name(dev) + cfg = self._model._device_config.get(device_name, None) + if cfg is None: + logger.warning(f"Device {device_name} not found in device_config dict") return + self._model.remove_device_configs({device_name: cfg}) + + @SafeSlot(str, int) + def update_device_validation( + self, device_name: str, validation_status: int | ValidationStatus + ) -> None: + """ + Update the validation status of a device. + + Args: + device_name (str): The name of the device. + validation_status (int | ValidationStatus): The new validation status. + """ + self._model.update_validation_status(device_name, validation_status) if __name__ == "__main__": import sys + import numpy as np from qtpy.QtWidgets import QApplication app = QApplication(sys.argv) + widget = QtWidgets.QWidget() + layout = QtWidgets.QVBoxLayout(widget) + layout.setContentsMargins(0, 0, 0, 0) window = DeviceTableView() + layout.addWidget(window) + # QPushButton + button = QtWidgets.QPushButton("Test status_update") + layout.addWidget(button) + + def _button_clicked(): + names = list(window._model._device_config.keys()) + for name in names: + window.update_device_validation( + name, ValidationStatus.VALID if np.random.rand() > 0.5 else ValidationStatus.FAILED + ) + + button.clicked.connect(_button_clicked) # pylint: disable=protected-access config = window.client.device_manager._get_redis_device_config() - window.set_device_config(config) - window.show() + names = [cfg.pop("name") for cfg in config] + config_dict = {name: cfg for name, cfg in zip(names, config)} + window.set_device_config(config_dict) + widget.show() sys.exit(app.exec_()) diff --git a/bec_widgets/widgets/control/device_manager/components/dm_config_view.py b/bec_widgets/widgets/control/device_manager/components/dm_config_view.py index 155f82d9..846c84ee 100644 --- a/bec_widgets/widgets/control/device_manager/components/dm_config_view.py +++ b/bec_widgets/widgets/control/device_manager/components/dm_config_view.py @@ -2,17 +2,23 @@ from __future__ import annotations +import traceback + import yaml +from bec_lib.logger import bec_logger from qtpy import QtCore, QtWidgets from bec_widgets.utils.bec_widget import BECWidget +from bec_widgets.utils.colors import get_accent_colors, get_theme_palette from bec_widgets.utils.error_popups import SafeSlot from bec_widgets.widgets.editors.monaco.monaco_widget import MonacoWidget +logger = bec_logger.logger + class DMConfigView(BECWidget, QtWidgets.QWidget): def __init__(self, parent=None, client=None): - super().__init__(client=client, parent=parent) + super().__init__(client=client, parent=parent, theme_update=True) self.stacked_layout = QtWidgets.QStackedLayout() self.stacked_layout.setContentsMargins(0, 0, 0, 0) self.stacked_layout.setSpacing(0) @@ -35,12 +41,11 @@ class DMConfigView(BECWidget, QtWidgets.QWidget): self.monaco_editor.set_minimap_enabled(False) # self.monaco_editor.setFixedHeight(600) self.monaco_editor.set_readonly(True) + self.monaco_editor.editor.set_scroll_beyond_last_line_enabled(False) + self.monaco_editor.editor.set_line_numbers_mode("off") def _customize_overlay(self): self._overlay_widget.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self._overlay_widget.setStyleSheet( - "background: qlineargradient(x1:0, y1:0, x2:0, y2:1,stop:0 #ffffff, stop:1 #e0e0e0);" - ) self._overlay_widget.setAutoFillBackground(True) self._overlay_widget.setSizePolicy( QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding @@ -49,13 +54,20 @@ class DMConfigView(BECWidget, QtWidgets.QWidget): @SafeSlot(dict) def on_select_config(self, device: dict): """Handle selection of a device from the device table.""" - if not device: + if len(device) != 1: text = "" self.stacked_layout.setCurrentWidget(self._overlay_widget) else: - text = yaml.dump(device, default_flow_style=False) - self.stacked_layout.setCurrentWidget(self.monaco_editor) + try: + text = yaml.dump(device, default_flow_style=False) + self.stacked_layout.setCurrentWidget(self.monaco_editor) + except Exception: + content = traceback.format_exc() + logger.error(f"Error converting device to YAML:\n{content}") + text = "" + self.stacked_layout.setCurrentWidget(self._overlay_widget) self.monaco_editor.set_readonly(False) # Enable editing + text = text.rstrip() self.monaco_editor.set_text(text) self.monaco_editor.set_readonly(True) # Disable editing again diff --git a/bec_widgets/widgets/control/device_manager/components/dm_docstring_view.py b/bec_widgets/widgets/control/device_manager/components/dm_docstring_view.py new file mode 100644 index 00000000..7b128992 --- /dev/null +++ b/bec_widgets/widgets/control/device_manager/components/dm_docstring_view.py @@ -0,0 +1,128 @@ +"""Module to visualize the docstring of a device class.""" + +from __future__ import annotations + +import inspect +import re +import traceback + +from bec_lib.logger import bec_logger +from bec_lib.plugin_helper import get_plugin_class, plugin_package_name +from bec_lib.utils.rpc_utils import rgetattr +from qtpy import QtCore, QtWidgets + +from bec_widgets.utils.error_popups import SafeSlot + +logger = bec_logger.logger + +try: + import ophyd + import ophyd_devices + + READY_TO_VIEW = True +except ImportError: + logger.warning(f"Optional dependencies not available: {ImportError}") + ophyd_devices = None + ophyd = None + + +class DocstringView(QtWidgets.QTextEdit): + def __init__(self, parent: QtWidgets.QWidget | None = None): + super().__init__(parent) + self.setReadOnly(True) + self.setFocusPolicy(QtCore.Qt.NoFocus) + if not READY_TO_VIEW: + self._set_text("Ophyd or ophyd_devices not installed, cannot show docstrings.") + self.setEnabled(False) + return + + def _format_docstring(self, doc: str | None) -> str: + if not doc: + return "No docstring available." + + # Escape HTML + doc = doc.replace("&", "&").replace("<", "<").replace(">", ">") + + # Remove leading/trailing blank lines from the entire docstring + lines = [line.rstrip() for line in doc.splitlines()] + while lines and lines[0].strip() == "": + lines.pop(0) + while lines and lines[-1].strip() == "": + lines.pop() + doc = "\n".join(lines) + + # Improved regex: match section header + all following indented lines + section_regex = re.compile( + r"(?m)^(Parameters|Args|Returns|Examples|Attributes|Raises)\b(?:\n([ \t]+.*))*", + re.MULTILINE, + ) + + def strip_section(match: re.Match) -> str: + # Capture all lines in the match + block = match.group(0) + lines = block.splitlines() + # Remove leading/trailing empty lines within the section + lines = [line for line in lines if line.strip() != ""] + return "\n".join(lines) + + doc = section_regex.sub(strip_section, doc) + + # Highlight section titles + doc = re.sub( + r"(?m)^(Parameters|Args|Returns|Examples|Attributes|Raises)\b", r"\1", doc + ) + + # Convert indented blocks to
 and strip leading/trailing newlines
+        def pre_block(match: re.Match) -> str:
+            text = match.group(0).strip("\n")
+            return f"
{text}
" + + doc = re.sub(r"(?m)(?:\n[ \t]+.*)+", pre_block, doc) + + # Replace remaining newlines with
and collapse multiple
+ doc = doc.replace("\n", "
") + doc = re.sub(r"(
)+", r"
", doc) + doc = doc.strip("
") + + return f"
{doc}
" + + def _set_text(self, text: str): + self.setReadOnly(False) + self.setMarkdown(text) + # self.setHtml(self._format_docstring(text)) + self.setReadOnly(True) + + @SafeSlot(dict) + def on_select_config(self, device: dict): + if len(device) != 1: + self._set_text("") + return + k = next(iter(device)) + device_class = device[k].get("deviceClass", "") + self.set_device_class(device_class) + + @SafeSlot(str) + def set_device_class(self, device_class_str: str) -> None: + docstring = "" + if not READY_TO_VIEW: + return + try: + module_cls = get_plugin_class(device_class_str, [ophyd_devices, ophyd]) + docstring = inspect.getdoc(module_cls) + self._set_text(docstring or "No docstring available.") + except Exception: + content = traceback.format_exc() + logger.error(f"Error retrieving docstring for {device_class_str}: {content}") + self._set_text(f"Error retrieving docstring for {device_class_str}") + + +if __name__ == "__main__": + import sys + + from qtpy.QtWidgets import QApplication + + app = QApplication(sys.argv) + config_view = DocstringView() + config_view.set_device_class("ophyd_devices.sim.sim_camera.SimCamera") + config_view.show() + sys.exit(app.exec_()) diff --git a/bec_widgets/widgets/control/device_manager/components/dm_ophyd_test.py b/bec_widgets/widgets/control/device_manager/components/dm_ophyd_test.py index a2a44f42..ef9e9fec 100644 --- a/bec_widgets/widgets/control/device_manager/components/dm_ophyd_test.py +++ b/bec_widgets/widgets/control/device_manager/components/dm_ophyd_test.py @@ -1,18 +1,24 @@ -"""Module to run a static test for the current config and see if it is valid.""" +"""Module to run a static tests for devices from a yaml config.""" from __future__ import annotations import enum +import re +import traceback +from html import escape +from typing import TYPE_CHECKING import bec_lib from bec_lib.logger import bec_logger from bec_qthemes import material_icon +from ophyd import status from qtpy import QtCore, QtGui, QtWidgets from bec_widgets.utils.bec_widget import BECWidget from bec_widgets.utils.colors import get_accent_colors from bec_widgets.utils.error_popups import SafeProperty, SafeSlot from bec_widgets.widgets.editors.web_console.web_console import WebConsole +from bec_widgets.widgets.utility.spinner.spinner import SpinnerWidget READY_TO_TEST = False @@ -28,305 +34,380 @@ except ImportError: ophyd_devices = None bec_server = None +if TYPE_CHECKING: # pragma no cover + try: + from ophyd_devices.utils.static_device_test import StaticDeviceTest + except ImportError: + StaticDeviceTest = None + class ValidationStatus(int, enum.Enum): """Validation status for device configurations.""" - UNKNOWN = 0 # colors.default - ERROR = 1 # colors.emergency - VALID = 2 # colors.highlight - CANT_CONNECT = 3 # colors.warning - CONNECTED = 4 # colors.success + PENDING = 0 # colors.default + VALID = 1 # colors.highlight + FAILED = 2 # colors.emergency -class DeviceValidationListItem(QtWidgets.QWidget): - """Custom list item widget showing device name and validation status.""" +class DeviceValidationResult(QtCore.QObject): + """Simple object to inject validation signals into QRunnable.""" - status_changed = QtCore.Signal(int) # Signal emitted when status changes -> ValidationStatus - # Signal emitted when device was validated with name, success, msg - device_validated = QtCore.Signal(str, str) + # Device validation signal, device_name, ValidationStatus as int, error message or '' + device_validated = QtCore.Signal(str, bool, str) + + +class DeviceValidationRunnable(QtCore.QRunnable): + """Runnable for validating a device configuration.""" def __init__( self, - device_config: dict[str, dict], - status: ValidationStatus, - status_icons: dict[ValidationStatus, QtGui.QPixmap], - validate_icon: QtGui.QPixmap, - parent=None, - static_device_test=None, + device_name: str, + config: dict, + static_device_test: StaticDeviceTest | None, + connect: bool = False, ): - super().__init__(parent) - if len(device_config.keys()) > 1: - logger.warning( - f"Multiple devices found for config: {list(device_config.keys())}, using first one" - ) + """ + Initialize the device validation runnable. + + Args: + device_name (str): The name of the device to validate. + config (dict): The configuration dictionary for the device. + static_device_test (StaticDeviceTest): The static device test instance. + connect (bool, optional): Whether to connect to the device. Defaults to False. + """ + super().__init__() + self.device_name = device_name + self.config = config + self._connect = connect self._static_device_test = static_device_test - self.device_name = list(device_config.keys())[0] + self.signals = DeviceValidationResult() + + def run(self): + """Run method for device validation.""" + if self._static_device_test is None: + logger.error( + f"Ophyd devices or bec_server not available, cannot run validation for device {self.device_name}." + ) + return + try: + self._static_device_test.config = {self.device_name: self.config} + results = self._static_device_test.run_with_list_output(connect=self._connect) + success = results[0].success + msg = results[0].message + self.signals.device_validated.emit(self.device_name, success, msg) + except Exception: + content = traceback.format_exc() + logger.error(f"Validation failed for device {self.device_name}. Exception: {content}") + self.signals.device_validated.emit(self.device_name, False, content) + + +class ValidationListItem(QtWidgets.QWidget): + """Custom list item widget showing device name and validation status.""" + + def __init__(self, device_name: str, device_config: dict, parent=None): + """ + Initialize the validation list item. + + Args: + device_name (str): The name of the device. + device_config (dict): The configuration of the device. + validation_colors (dict[ValidationStatus, QtGui.QColor]): The colors for each validation status. + parent (QtWidgets.QWidget, optional): The parent widget. + """ + super().__init__(parent) + self.main_layout = QtWidgets.QHBoxLayout(self) + self.main_layout.setContentsMargins(2, 2, 2, 2) + self.main_layout.setSpacing(4) + self.device_name = device_name self.device_config = device_config - self.status: ValidationStatus = status - colors = get_accent_colors() - self._status_icon = status_icons - self._validate_icon = validate_icon + self.validation_msg = "Validation in progress..." self._setup_ui() - self._update_status_indicator() def _setup_ui(self): """Setup the UI for the list item.""" - layout = QtWidgets.QHBoxLayout(self) - layout.setContentsMargins(4, 4, 4, 4) + label = QtWidgets.QLabel(self.device_name) + self.main_layout.addWidget(label) + self.main_layout.addStretch() + self._spinner = SpinnerWidget(parent=self) + self._spinner.speed = 80 + self._spinner.setFixedSize(24, 24) + self.main_layout.addWidget(self._spinner) + self._base_style = "font-weight: bold;" + self.setStyleSheet(self._base_style) + self._start_spinner() - # Device name label - self.name_label = QtWidgets.QLabel(self.device_name) - self.name_label.setStyleSheet("font-weight: bold;") - layout.addWidget(self.name_label) + def _start_spinner(self): + """Start the spinner animation.""" + self._spinner.start() + QtWidgets.QApplication.processEvents() - # Make sure status is on the right - layout.addStretch() - self.request_validation_button = QtWidgets.QPushButton("Validate") - self.request_validation_button.setIcon(self._validate_icon) - if self._static_device_test is None: - self.request_validation_button.setDisabled(True) - else: - self.request_validation_button.clicked.connect(self.on_request_validation) - # self.request_validation_button.setVisible(False) -> Hide it?? - layout.addWidget(self.request_validation_button) - # Status indicator - self.status_indicator = QtWidgets.QLabel() - self._update_status_indicator() - layout.addWidget(self.status_indicator) + def _stop_spinner(self): + """Stop the spinner animation.""" + self._spinner.stop() + self._spinner.setVisible(False) @SafeSlot() - def on_request_validation(self): - """Handle validate button click.""" - if self._static_device_test is None: - logger.warning("Static device test not available.") - return - self._static_device_test.config = self.device_config - # TODO logic if connect is allowed - ret = self._static_device_test.run_with_list_output(connect=False)[0] - if ret.success: - self.set_status(ValidationStatus.VALID) - else: - self.set_status(ValidationStatus.ERROR) - self.device_validated.emit(ret.name, ret.message) + def on_validation_restart(self): + """Handle validation restart.""" + self.validation_msg = "" + self._start_spinner() + self.setStyleSheet("") # Check if this works as expected - def _update_status_indicator(self): - """Update the status indicator color based on validation status.""" - self.status_indicator.setPixmap(self._status_icon[self.status]) - - def set_status(self, status: ValidationStatus): - """Update the validation status.""" - self.status = status - self._update_status_indicator() - self.status_changed.emit(self.status) - - def get_status(self) -> ValidationStatus: - """Get the current validation status.""" - return self.status + @SafeSlot(str) + def on_validation_failed(self, error_msg: str): + """Handle validation failure.""" + self.validation_msg = error_msg + colors = get_accent_colors() + self._stop_spinner() + self.main_layout.removeWidget(self._spinner) + self._spinner.deleteLater() + label = QtWidgets.QLabel("") + icon = material_icon("error", color=colors.emergency, size=(24, 24)) + label.setPixmap(icon) + self.main_layout.addWidget(label) -class DeviceManagerOphydTest(BECWidget, QtWidgets.QWidget): +class DMOphydTest(BECWidget, QtWidgets.QWidget): + """Widget to test device configurations using ophyd devices.""" - config_changed = QtCore.Signal( - dict, dict - ) # Signal emitted when the device config changed, new_config, old_config + # Signal to emit the validation status of a device + device_validated = QtCore.Signal(str, int) def __init__(self, parent=None, client=None): super().__init__(parent=parent, client=client) if not READY_TO_TEST: - self._set_disabled() - static_device_test = None + self.setDisabled(True) + self.static_device_test = None else: from ophyd_devices.utils.static_device_test import StaticDeviceTest - static_device_test = StaticDeviceTest(config_dict={}) - self._static_device_test = static_device_test - self._device_config: dict[str, dict] = {} + self.static_device_test = StaticDeviceTest(config_dict={}) + self._device_list_items: dict[str, QtWidgets.QListWidgetItem] = {} + self._thread_pool = QtCore.QThreadPool.globalInstance() + self._main_layout = QtWidgets.QVBoxLayout(self) self._main_layout.setContentsMargins(0, 0, 0, 0) self._main_layout.setSpacing(4) - # Setup icons - colors = get_accent_colors() - self._validate_icon = material_icon( - icon_name="play_arrow", color=colors.default, filled=True - ) - self._status_icons = { - ValidationStatus.UNKNOWN: material_icon( - icon_name="circle", size=(12, 12), color=colors.default, filled=True - ), - ValidationStatus.ERROR: material_icon( - icon_name="circle", size=(12, 12), color=colors.emergency, filled=True - ), - ValidationStatus.VALID: material_icon( - icon_name="circle", size=(12, 12), color=colors.highlight, filled=True - ), - ValidationStatus.CANT_CONNECT: material_icon( - icon_name="circle", size=(12, 12), color=colors.warning, filled=True - ), - ValidationStatus.CONNECTED: material_icon( - icon_name="circle", size=(12, 12), color=colors.success, filled=True - ), - } - - self.setLayout(self._main_layout) - - # splitter + # We add a splitter between the list and the text box self.splitter = QtWidgets.QSplitter(QtCore.Qt.Orientation.Vertical) self._main_layout.addWidget(self.splitter) - # Add custom list - self.setup_device_validation_list() - - # Setup text box - self.setup_text_box() + self._setup_list_ui() + self._setup_textbox_ui() + def _setup_list_ui(self): + """Setup the list UI.""" + self._list_widget = QtWidgets.QListWidget(self) + self._list_widget.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection) + self.splitter.addWidget(self._list_widget) # Connect signals - self.config_changed.connect(self.on_config_updated) + self._list_widget.currentItemChanged.connect(self._on_current_item_changed) - @SafeSlot(list) - def on_device_config_update(self, config: list[dict]): - old_cfg = self._device_config - self._device_config = self._compile_device_config_list(config) - self.config_changed.emit(self._device_config, old_cfg) + def _setup_textbox_ui(self): + """Setup the text box UI.""" + self._text_box = QtWidgets.QTextEdit(self) + self._text_box.setReadOnly(True) + self._text_box.setFocusPolicy(QtCore.Qt.NoFocus) + self.splitter.addWidget(self._text_box) - def _compile_device_config_list(self, config: list[dict]) -> dict[str, dict]: - return {dev["name"]: {k: v for k, v in dev.items() if k != "name"} for dev in config} - - @SafeSlot(dict, dict) - def on_config_updated(self, new_config: dict, old_config: dict): - """Handle config updates and refresh the validation list.""" - # Find differences for potential re-validation - diffs = self._find_diffs(new_config, old_config) - # Check diff first - for diff in diffs: - if not diff: - continue - if len(diff) > 1: - logger.warning(f"Multiple devices found in diff: {diff}, using first one") - name = list(diff.keys())[0] - if name in self.client.device_manager.devices: - status = ValidationStatus.CONNECTED - else: - status = ValidationStatus.UNKNOWN - if self.get_device_status(diff) is None: - self.add_device(diff, status) - else: - self.update_device_status(diff, status) - - def _find_diffs(self, new_config: dict, old_config: dict) -> list[dict]: - """ - Return list of keys/paths where d1 and d2 differ. This goes recursively through the dictionary. + @SafeSlot(dict) + def add_device_configs(self, device_configs: dict[str, dict]) -> None: + """Receive an update with device configs. Args: - new_config: The first dictionary to compare. - old_config: The second dictionary to compare. + device_configs (dict[str, dict]): The updated device configurations. """ - diffs = [] - keys = set(new_config.keys()) | set(old_config.keys()) - for k in keys: - if k not in old_config: # New device - diffs.append({k: new_config[k]}) - continue - if k not in new_config: # Removed device - diffs.append({k: old_config[k]}) - continue - # Compare device config if exists in both - v1, v2 = old_config[k], new_config[k] - if isinstance(v1, dict) and isinstance(v2, dict): - if self._find_diffs(v2, v1): # recurse: something inside changed - diffs.append({k: new_config[k]}) - elif v1 != v2: - diffs.append({k: new_config[k]}) - return diffs + for device_name, device_config in device_configs.items(): + if device_name in self._device_list_items: + logger.error(f"Device {device_name} is already in the list.") + return + item = QtWidgets.QListWidgetItem(self._list_widget) + widget = ValidationListItem(device_name=device_name, device_config=device_config) - def setup_device_validation_list(self): - """Setup the device validation list.""" - # Create the custom validation list widget - self.validation_list = QtWidgets.QListWidget() - self.validation_list.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection) - self.splitter.addWidget(self.validation_list) - # self._main_layout.addWidget(self.validation_list) + # wrap it in a QListWidgetItem + item.setSizeHint(widget.sizeHint()) + self._list_widget.addItem(item) + self._list_widget.setItemWidget(item, widget) + self._device_list_items[device_name] = item + self._run_device_validation(widget) - def setup_text_box(self): - """Setup the text box for device validation messages.""" - self.validation_text_box = QtWidgets.QTextEdit() - self.validation_text_box.setReadOnly(True) - self.splitter.addWidget(self.validation_text_box) - # self._main_layout.addWidget(self.validation_text_box) + @SafeSlot(dict) + def remove_device_configs(self, device_configs: dict[str, dict]) -> None: + """Remove device configs from the list. - @SafeSlot(str, str) - def on_device_validated(self, device_name: str, message: str): - """Handle device validation results.""" - text = f"Device {device_name} was validated. Message: {message}" - self.validation_text_box.setText(text) + Args: + device_name (str): The name of the device to remove. + """ + for device_name in device_configs.keys(): + if device_name not in self._device_list_items: + logger.warning(f"Device {device_name} not found in list.") + return + self._remove_list_item(device_name) - def _set_disabled(self) -> None: - """Disable the full view""" - self.setDisabled(True) + def _remove_list_item(self, device_name: str): + """Remove a device from the list.""" + # Get the list item + item = self._device_list_items.pop(device_name) - def add_device( - self, device_config: dict[str, dict], status: ValidationStatus = ValidationStatus.UNKNOWN + # Retrieve the custom widget attached to the item + widget = self._list_widget.itemWidget(item) + if widget is not None: + widget.deleteLater() # clean up custom widget + + # Remove the item from the QListWidget + row = self._list_widget.row(item) + self._list_widget.takeItem(row) + + def _run_device_validation(self, widget: ValidationListItem): + """ + Run the device validation in a separate thread. + + Args: + widget (ValidationListItem): The widget to validate. + """ + if not READY_TO_TEST: + logger.error("Ophyd devices or bec_server not available, cannot run validation.") + return + if ( + widget.device_name in self.client.device_manager.devices + ): # TODO and config has to be exact the same.. + self._on_device_validated( + widget.device_name, + ValidationStatus.VALID, + f"Device {widget.device_name} is already in active config", + ) + return + runnable = DeviceValidationRunnable( + device_name=widget.device_name, + config=widget.device_config, + static_device_test=self.static_device_test, + connect=False, + ) + runnable.signals.device_validated.connect(self._on_device_validated) + self._thread_pool.start(runnable) + + @SafeSlot(str, bool, str) + def _on_device_validated(self, device_name: str, success: bool, message: str): + """Handle the device validation result. + + Args: + device_name (str): The name of the device. + success (bool): Whether the validation was successful. + message (str): The validation message. + """ + logger.info(f"Device {device_name} validation result: {success}, message: {message}") + item = self._device_list_items.get(device_name, None) + if not item: + logger.error(f"Device {device_name} not found in the list.") + return + if success: + self._remove_list_item(device_name=device_name) + self.device_validated.emit(device_name, ValidationStatus.VALID.value) + else: + widget: ValidationListItem = self._list_widget.itemWidget(item) + widget.on_validation_failed(message) + self.device_validated.emit(device_name, ValidationStatus.FAILED.value) + + def _on_current_item_changed( + self, current: QtWidgets.QListWidgetItem, previous: QtWidgets.QListWidgetItem ): - """Add a device to the validation list.""" - # Create the custom widget - item_widget = DeviceValidationListItem( - device_config=device_config, - status=status, - status_icons=self._status_icons, - validate_icon=self._validate_icon, - static_device_test=self._static_device_test, + """Handle the current item change in the list widget. + + Args: + current (QListWidgetItem): The currently selected item. + previous (QListWidgetItem): The previously selected item. + """ + widget: ValidationListItem = self._list_widget.itemWidget(current) + if widget: + try: + formatted_html = self._format_validation_message(widget.validation_msg) + self._text_box.setHtml(formatted_html) + except Exception as e: + logger.error(f"Error formatting validation message: {e}") + self._text_box.setPlainText(widget.validation_msg) + + def _format_validation_message(self, raw_msg: str) -> str: + """Simple HTML formatting for validation messages, wrapping text naturally.""" + if not raw_msg.strip(): + return "Validation in progress..." + if raw_msg == "Validation in progress...": + return "Validation in progress..." + + raw_msg = escape(raw_msg) + + # Split into lines + lines = raw_msg.splitlines() + summary = lines[0] if lines else "Validation Result" + rest = "\n".join(lines[1:]).strip() + + # Split traceback / final ERROR + tb_match = re.search(r"(Traceback.*|ERROR:.*)$", rest, re.DOTALL | re.MULTILINE) + if tb_match: + main_text = rest[: tb_match.start()].strip() + error_detail = tb_match.group().strip() + else: + main_text = rest + error_detail = "" + + # Highlight field names in orange (simple regex for word: Field) + main_text_html = re.sub( + r"(\b\w+\b)(?=: Field required)", + r'\1', + main_text, + ) + # Wrap in div for monospace, allowing wrapping + main_text_html = ( + f'
{main_text_html}
' if main_text_html else "" ) - # Create a list widget item - list_item = QtWidgets.QListWidgetItem() - list_item.setSizeHint(item_widget.sizeHint()) + # Traceback / error in red + error_html = ( + f'
{error_detail}
' + if error_detail + else "" + ) - # Add item to list and set custom widget - self.validation_list.addItem(list_item) - self.validation_list.setItemWidget(list_item, item_widget) - item_widget.device_validated.connect(self.on_device_validated) + # Summary at top, dark red + html = ( + f'
' + f'
{summary}
' + f"{main_text_html}" + f"{error_html}" + f"
" + ) + return html - def update_device_status(self, device_config: dict[str, dict], status: ValidationStatus): - """Update the validation status for a specific device.""" - for i in range(self.validation_list.count()): - item = self.validation_list.item(i) - widget = self.validation_list.itemWidget(item) - if ( - isinstance(widget, DeviceValidationListItem) - and widget.device_config == device_config - ): - widget.set_status(status) - break + @SafeSlot() + def clear_list(self): + """Clear the device list.""" + self._thread_pool.clear() + if self._thread_pool.waitForDone(2000) is False: # Wait for threads to finish + logger.error("Failed to wait for threads to finish. Removing items from the list.") + self._device_list_items.clear() + self._list_widget.clear() - def clear_devices(self): - """Clear all devices from the list.""" - self.validation_list.clear() - - def get_device_status(self, device_config: dict[str, dict]) -> ValidationStatus | None: - """Get the validation status for a specific device.""" - for i in range(self.validation_list.count()): - item = self.validation_list.item(i) - widget = self.validation_list.itemWidget(item) - if ( - isinstance(widget, DeviceValidationListItem) - and widget.device_config == device_config - ): - return widget.get_status() - return None + def remove_device(self, device_name: str): + """Remove a device from the list.""" + item = self._device_list_items.pop(device_name, None) + if item: + self._list_widget.removeItemWidget(item) if __name__ == "__main__": import sys + from bec_lib.bec_yaml_loader import yaml_load + # pylint: disable=ungrouped-imports from qtpy.QtWidgets import QApplication app = QApplication(sys.argv) - device_manager_ophyd_test = DeviceManagerOphydTest() - cfg = device_manager_ophyd_test.client.device_manager._get_redis_device_config() - cfg.append({"name": "Wrong_Device", "type": "test"}) - device_manager_ophyd_test.on_device_config_update(cfg) + device_manager_ophyd_test = DMOphydTest() + config_path = "/Users/appel_c/work_psi_awi/bec_workspace/csaxs_bec/csaxs_bec/device_configs/endstation.yaml" + cfg = yaml_load(config_path) + cfg.update({"device_will_fail": {"name": "device_will_fail", "some_param": 1}}) + device_manager_ophyd_test.add_device_configs(cfg) device_manager_ophyd_test.show() device_manager_ophyd_test.setWindowTitle("Device Manager Ophyd Test") device_manager_ophyd_test.resize(800, 600)