refactor: refactor device_manager_view

This commit is contained in:
appel_c authored and wyzula-jan committed 2025-10-15 14:10:29 +02:00
1 parent 29b7214b19
commit 7f476ca5e3
10 files changed
+1446 -458

No files matched your search

Whitespace-only changes.
@@ -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_())
@@ -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_())
@@ -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)
@@ -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\Line truncated
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\Line truncated
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\Line truncated
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\Line truncated
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
@@ -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
@@ -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", "<unknown>")) for r in sorted(source_rows)]
configs = [model._list_items[r] for r in sorted(source_rows)]
names = [cfg.get("name", "<unknown>") 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_())
@@ -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
@@ -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 "<i>No docstring available.</i>"
# Escape HTML
doc = doc.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
# 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"<b>\1</b>", doc
)
# Convert indented blocks to <pre> and strip leading/trailing newlines
def pre_block(match: re.Match) -> str:
text = match.group(0).strip("\n")
return f"<pre>{text}</pre>"
doc = re.sub(r"(?m)(?:\n[ \t]+.*)+", pre_block, doc)
# Replace remaining newlines with <br> and collapse multiple <br>
doc = doc.replace("\n", "<br>")
doc = re.sub(r"(<br>)+", r"<br>", doc)
doc = doc.strip("<br>")
return f"<div style='font-family: sans-serif; font-size: 12pt;'>{doc}</div>"
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_())
@@ -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 "<i>Validation in progress...</i>"
if raw_msg == "Validation in progress...":
return "<i>Validation in progress...</i>"
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'<span style="color:#FF8C00; font-weight:bold;">\1</span>',
main_text,
)
# Wrap in div for monospace, allowing wrapping
main_text_html = (
f'<div style="white-space: pre-wrap;">{main_text_html}</div>' 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'<div style="white-space: pre-wrap; color:#A00000;">{error_detail}</div>'
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'<div style="font-family: monospace; font-size:13px; white-space: pre-wrap;">'
f'<div style="font-weight:bold; color:#8B0000; margin-bottom:4px;">{summary}</div>'
f"{main_text_html}"
f"{error_html}"
f"</div>"
)
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)