bec-widgets 1.25.0__py3-none-any.whl → 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- .gitlab-ci.yml +11 -6
- CHANGELOG.md +650 -0
- PKG-INFO +3 -3
- bec_widgets/__init__.py +4 -0
- bec_widgets/applications/bw_launch.py +23 -0
- bec_widgets/applications/launch_window.py +430 -0
- bec_widgets/assets/app_icons/auto_update.png +0 -0
- bec_widgets/assets/app_icons/ui_loader_tile.png +0 -0
- bec_widgets/cli/__init__.py +0 -1
- bec_widgets/cli/client.py +1779 -2064
- bec_widgets/cli/client_utils.py +346 -174
- bec_widgets/cli/generate_cli.py +143 -37
- bec_widgets/cli/rpc/rpc_base.py +152 -21
- bec_widgets/cli/rpc/rpc_register.py +113 -6
- bec_widgets/cli/rpc/rpc_widget_handler.py +13 -11
- bec_widgets/cli/server.py +125 -239
- bec_widgets/examples/jupyter_console/jupyter_console_window.py +97 -145
- bec_widgets/examples/plugin_example_pyside/tictactoetaskmenu.py +1 -1
- bec_widgets/utils/bec_connector.py +190 -21
- bec_widgets/utils/bec_designer.py +7 -0
- bec_widgets/utils/bec_dispatcher.py +71 -4
- bec_widgets/utils/bec_plugin_helper.py +89 -0
- bec_widgets/utils/bec_signal_proxy.py +1 -1
- bec_widgets/utils/bec_widget.py +26 -10
- bec_widgets/utils/colors.py +1 -1
- bec_widgets/{qt_utils → utils}/compact_popup.py +2 -0
- bec_widgets/utils/container_utils.py +37 -12
- bec_widgets/utils/crosshair.py +25 -8
- bec_widgets/utils/entry_validator.py +3 -1
- bec_widgets/{qt_utils → utils}/error_popups.py +18 -0
- bec_widgets/{qt_utils → utils}/expandable_frame.py +2 -2
- bec_widgets/utils/forms_from_types/forms.py +182 -0
- bec_widgets/{widgets/editors/scan_metadata/_metadata_widgets.py → utils/forms_from_types/items.py} +41 -30
- bec_widgets/utils/generate_designer_plugin.py +40 -36
- bec_widgets/utils/linear_region_selector.py +2 -0
- bec_widgets/utils/name_utils.py +16 -0
- bec_widgets/{qt_utils → utils}/palette_viewer.py +2 -2
- bec_widgets/utils/plot_indicator_items.py +2 -5
- bec_widgets/utils/plugin_utils.py +47 -1
- bec_widgets/{qt_utils → utils}/round_frame.py +14 -14
- bec_widgets/utils/rpc_server.py +277 -0
- bec_widgets/utils/serialization.py +44 -0
- bec_widgets/{qt_utils → utils}/settings_dialog.py +26 -1
- bec_widgets/{qt_utils → utils}/side_panel.py +17 -10
- bec_widgets/{qt_utils → utils}/toolbar.py +69 -25
- bec_widgets/utils/ui_loader.py +8 -8
- bec_widgets/utils/widget_io.py +166 -25
- bec_widgets/widgets/containers/auto_update/auto_updates.py +364 -0
- bec_widgets/widgets/containers/dock/dock.py +157 -49
- bec_widgets/widgets/containers/dock/dock_area.py +186 -138
- bec_widgets/widgets/containers/layout_manager/layout_manager.py +2 -1
- bec_widgets/widgets/containers/main_window/addons/web_links.py +15 -0
- bec_widgets/widgets/containers/main_window/main_window.py +189 -41
- bec_widgets/widgets/control/buttons/button_abort/button_abort.py +3 -4
- bec_widgets/widgets/control/buttons/button_reset/button_reset.py +3 -4
- bec_widgets/widgets/control/buttons/button_resume/button_resume.py +3 -3
- bec_widgets/widgets/control/buttons/stop_button/stop_button.py +18 -7
- bec_widgets/widgets/control/device_control/position_indicator/position_indicator.py +22 -3
- bec_widgets/widgets/control/device_control/positioner_box/_base/positioner_box_base.py +37 -18
- bec_widgets/widgets/control/device_control/positioner_box/positioner_box/positioner_box.py +28 -4
- bec_widgets/widgets/control/device_control/positioner_box/positioner_box/positioner_box.ui +27 -4
- bec_widgets/widgets/control/device_control/positioner_box/positioner_box_2d/positioner_box_2d.py +5 -2
- bec_widgets/widgets/control/device_control/positioner_box/positioner_box_2d/positioner_box_2d.ui +97 -31
- bec_widgets/widgets/control/device_control/positioner_box/positioner_control_line/positioner_control_line.ui +11 -4
- bec_widgets/widgets/control/device_control/positioner_group/positioner_group.py +2 -3
- bec_widgets/widgets/control/device_input/base_classes/device_input_base.py +29 -4
- bec_widgets/widgets/control/device_input/base_classes/device_signal_input_base.py +1 -0
- bec_widgets/widgets/control/device_input/device_combobox/device_combobox.py +2 -2
- bec_widgets/widgets/control/device_input/device_line_edit/device_line_edit.py +2 -2
- bec_widgets/widgets/control/device_input/signal_combobox/signal_combobox.py +1 -2
- bec_widgets/widgets/control/device_input/signal_line_edit/signal_line_edit.py +1 -2
- bec_widgets/widgets/control/scan_control/scan_control.py +7 -5
- bec_widgets/widgets/control/scan_control/scan_group_box.py +28 -5
- bec_widgets/widgets/dap/dap_combo_box/dap_combo_box.py +1 -2
- bec_widgets/widgets/dap/lmfit_dialog/lmfit_dialog.py +3 -4
- bec_widgets/widgets/dap/lmfit_dialog/lmfit_dialog_vertical.ui +14 -8
- bec_widgets/widgets/editors/console/console.py +1 -1
- bec_widgets/widgets/editors/{scan_metadata/additional_metadata_table.py → dict_backed_table.py} +29 -6
- bec_widgets/widgets/editors/scan_metadata/__init__.py +0 -7
- bec_widgets/widgets/editors/scan_metadata/_util.py +1 -1
- bec_widgets/widgets/{plots/motor_map/register_bec_motor_map_widget.py → editors/scan_metadata/register_scan_metadata.py} +2 -4
- bec_widgets/widgets/editors/scan_metadata/scan_metadata.py +42 -136
- bec_widgets/widgets/editors/scan_metadata/scan_metadata.pyproject +1 -0
- bec_widgets/widgets/{plots/multi_waveform/bec_multi_waveform_widget_plugin.py → editors/scan_metadata/scan_metadata_plugin.py} +9 -9
- bec_widgets/widgets/editors/text_box/text_box.py +2 -3
- bec_widgets/widgets/editors/website/website.py +2 -2
- bec_widgets/widgets/games/minesweeper.py +3 -2
- bec_widgets/widgets/plots/image/image.py +960 -0
- bec_widgets/widgets/plots/image/image.pyproject +1 -0
- bec_widgets/widgets/plots/image/image_item.py +279 -0
- bec_widgets/widgets/plots/{motor_map/bec_motor_map_widget_plugin.py → image/image_plugin.py} +11 -13
- bec_widgets/widgets/{containers/figure/plots → plots}/image/image_processor.py +31 -64
- bec_widgets/widgets/plots/image/{register_bec_image_widget.py → register_image.py} +2 -2
- bec_widgets/widgets/plots/image/toolbar_bundles/image_selection.py +59 -0
- bec_widgets/widgets/plots/image/toolbar_bundles/processing.py +79 -0
- bec_widgets/widgets/plots/motor_map/motor_map.py +832 -0
- bec_widgets/widgets/plots/motor_map/motor_map.pyproject +1 -0
- bec_widgets/widgets/plots/motor_map/motor_map_plugin.py +54 -0
- bec_widgets/widgets/plots/{multi_waveform/register_bec_multi_waveform_widget.py → motor_map/register_motor_map.py} +2 -4
- bec_widgets/widgets/plots/motor_map/settings/motor_map_settings.py +129 -0
- bec_widgets/widgets/plots/motor_map/settings/motor_map_settings.ui +120 -0
- bec_widgets/widgets/plots/motor_map/toolbar_bundles/motor_selection.py +70 -0
- bec_widgets/widgets/plots/multi_waveform/multi_waveform.py +508 -0
- bec_widgets/widgets/plots/multi_waveform/multi_waveform.pyproject +1 -0
- bec_widgets/widgets/plots/multi_waveform/multi_waveform_plugin.py +54 -0
- bec_widgets/widgets/plots/multi_waveform/register_multi_waveform.py +15 -0
- bec_widgets/widgets/plots/multi_waveform/settings/control_panel.py +144 -0
- bec_widgets/widgets/plots/multi_waveform/settings/multi_waveform_controls.ui +164 -0
- bec_widgets/widgets/plots/multi_waveform/toolbar_bundles/monitor_selection.py +65 -0
- bec_widgets/widgets/{plots_next_gen → plots}/plot_base.py +321 -40
- bec_widgets/widgets/plots/{waveform/register_bec_waveform_widget.py → scatter_waveform/register_scatter_waveform.py} +3 -3
- bec_widgets/widgets/plots/scatter_waveform/scatter_curve.py +197 -0
- bec_widgets/widgets/plots/scatter_waveform/scatter_waveform.py +553 -0
- bec_widgets/widgets/plots/scatter_waveform/scatter_waveform.pyproject +1 -0
- bec_widgets/widgets/plots/{image/bec_image_widget_plugin.py → scatter_waveform/scatter_waveform_plugin.py} +9 -13
- bec_widgets/widgets/plots/scatter_waveform/settings/scatter_curve_setting.py +138 -0
- bec_widgets/widgets/plots/scatter_waveform/settings/scatter_curve_settings_horizontal.ui +195 -0
- bec_widgets/widgets/plots/scatter_waveform/settings/scatter_curve_settings_vertical.ui +204 -0
- bec_widgets/widgets/{plots_next_gen → plots}/setting_menus/axis_settings.py +8 -8
- bec_widgets/widgets/{plots_next_gen → plots}/toolbar_bundles/mouse_interactions.py +4 -18
- bec_widgets/widgets/{plots_next_gen → plots}/toolbar_bundles/plot_export.py +14 -3
- bec_widgets/widgets/{plots_next_gen → plots}/toolbar_bundles/roi_bundle.py +6 -1
- bec_widgets/widgets/{plots_next_gen → plots}/toolbar_bundles/save_state.py +2 -2
- bec_widgets/widgets/{containers/figure/plots/waveform/waveform_curve.py → plots/waveform/curve.py} +119 -49
- bec_widgets/widgets/plots/waveform/register_waveform.py +15 -0
- bec_widgets/widgets/plots/waveform/settings/curve_settings/curve_setting.py +125 -0
- bec_widgets/widgets/plots/waveform/settings/curve_settings/curve_tree.py +576 -0
- bec_widgets/widgets/plots/waveform/utils/__init__.py +0 -0
- bec_widgets/widgets/plots/waveform/utils/roi_manager.py +84 -0
- bec_widgets/widgets/plots/waveform/waveform.py +1794 -0
- bec_widgets/widgets/plots/waveform/waveform.pyproject +1 -0
- bec_widgets/widgets/plots/waveform/{bec_waveform_widget_plugin.py → waveform_plugin.py} +9 -13
- bec_widgets/widgets/progress/bec_progressbar/bec_progressbar.py +1 -2
- bec_widgets/widgets/progress/ring_progress_bar/ring.py +11 -10
- bec_widgets/widgets/progress/ring_progress_bar/ring_progress_bar.py +24 -14
- bec_widgets/widgets/services/bec_queue/bec_queue.py +13 -11
- bec_widgets/widgets/services/bec_status_box/bec_status_box.py +3 -4
- bec_widgets/widgets/services/device_browser/device_browser.py +5 -2
- bec_widgets/widgets/services/device_browser/device_item/device_item.py +1 -1
- bec_widgets/widgets/utility/logpanel/logpanel.py +36 -17
- bec_widgets/widgets/utility/spinbox/decimal_spinbox.py +3 -3
- bec_widgets/widgets/utility/spinner/spinner.py +2 -2
- bec_widgets/widgets/utility/visual/color_button/color_button.py +1 -1
- bec_widgets/widgets/utility/visual/colormap_widget/colormap_widget.py +4 -6
- bec_widgets/widgets/utility/visual/dark_mode_button/dark_mode_button.py +4 -8
- {bec_widgets-1.25.0.dist-info → bec_widgets-2.0.0.dist-info}/METADATA +3 -3
- {bec_widgets-1.25.0.dist-info → bec_widgets-2.0.0.dist-info}/RECORD +169 -154
- pyproject.toml +3 -3
- bec_widgets/applications/alignment/alignment_1d/alignment_1d.py +0 -198
- bec_widgets/applications/alignment/alignment_1d/alignment_1d.ui +0 -615
- bec_widgets/applications/bec_app.py +0 -84
- bec_widgets/cli/auto_updates.py +0 -168
- bec_widgets/widgets/containers/figure/__init__.py +0 -1
- bec_widgets/widgets/containers/figure/figure.py +0 -796
- bec_widgets/widgets/containers/figure/plots/axis_settings.py +0 -91
- bec_widgets/widgets/containers/figure/plots/axis_settings.ui +0 -256
- bec_widgets/widgets/containers/figure/plots/image/image.py +0 -772
- bec_widgets/widgets/containers/figure/plots/image/image_item.py +0 -337
- bec_widgets/widgets/containers/figure/plots/motor_map/motor_map.py +0 -525
- bec_widgets/widgets/containers/figure/plots/multi_waveform/multi_waveform.py +0 -340
- bec_widgets/widgets/containers/figure/plots/plot_base.py +0 -505
- bec_widgets/widgets/containers/figure/plots/waveform/waveform.py +0 -1563
- bec_widgets/widgets/plots/image/bec_image_widget.pyproject +0 -1
- bec_widgets/widgets/plots/image/image_widget.py +0 -515
- bec_widgets/widgets/plots/motor_map/bec_motor_map_widget.pyproject +0 -1
- bec_widgets/widgets/plots/motor_map/motor_map_dialog/motor_map_settings.py +0 -56
- bec_widgets/widgets/plots/motor_map/motor_map_dialog/motor_map_settings.ui +0 -108
- bec_widgets/widgets/plots/motor_map/motor_map_widget.py +0 -234
- bec_widgets/widgets/plots/multi_waveform/bec_multi_waveform_widget.pyproject +0 -1
- bec_widgets/widgets/plots/multi_waveform/multi_waveform_controls.ui +0 -99
- bec_widgets/widgets/plots/multi_waveform/multi_waveform_widget.py +0 -536
- bec_widgets/widgets/plots/waveform/bec_waveform_widget.pyproject +0 -1
- bec_widgets/widgets/plots/waveform/waveform_popups/curve_dialog/curve_dialog.py +0 -336
- bec_widgets/widgets/plots/waveform/waveform_popups/curve_dialog/curve_dialog.ui +0 -372
- bec_widgets/widgets/plots/waveform/waveform_popups/dap_summary_dialog/dap_summary_dialog.py +0 -25
- bec_widgets/widgets/plots/waveform/waveform_widget.py +0 -751
- /bec_widgets/{qt_utils → utils}/collapsible_panel_manager.py +0 -0
- /bec_widgets/{applications/alignment → utils/forms_from_types}/__init__.py +0 -0
- /bec_widgets/{qt_utils → utils}/redis_message_waiter.py +0 -0
- /bec_widgets/{applications/alignment/alignment_1d → widgets/containers/auto_update}/__init__.py +0 -0
- /bec_widgets/{qt_utils → widgets/containers/main_window/addons}/__init__.py +0 -0
- /bec_widgets/widgets/{containers/figure/plots → plots/image/toolbar_bundles}/__init__.py +0 -0
- /bec_widgets/widgets/{containers/figure/plots/image → plots/motor_map/settings}/__init__.py +0 -0
- /bec_widgets/widgets/{containers/figure/plots/motor_map → plots/motor_map/toolbar_bundles}/__init__.py +0 -0
- /bec_widgets/widgets/{containers/figure/plots/multi_waveform → plots/multi_waveform/settings}/__init__.py +0 -0
- /bec_widgets/widgets/{containers/figure/plots/waveform → plots/multi_waveform/toolbar_bundles}/__init__.py +0 -0
- /bec_widgets/widgets/plots/{motor_map/motor_map_dialog → scatter_waveform}/__init__.py +0 -0
- /bec_widgets/widgets/plots/{waveform/waveform_popups → scatter_waveform/settings}/__init__.py +0 -0
- /bec_widgets/widgets/plots/{waveform/waveform_popups/curve_dialog → setting_menus}/__init__.py +0 -0
- /bec_widgets/widgets/{plots_next_gen → plots}/setting_menus/axis_settings_horizontal.ui +0 -0
- /bec_widgets/widgets/{plots_next_gen → plots}/setting_menus/axis_settings_vertical.ui +0 -0
- /bec_widgets/widgets/plots/{waveform/waveform_popups/dap_summary_dialog → toolbar_bundles}/__init__.py +0 -0
- /bec_widgets/widgets/{plots_next_gen/setting_menus → plots/waveform/settings}/__init__.py +0 -0
- /bec_widgets/widgets/{plots_next_gen/toolbar_bundles → plots/waveform/settings/curve_settings}/__init__.py +0 -0
- {bec_widgets-1.25.0.dist-info → bec_widgets-2.0.0.dist-info}/WHEEL +0 -0
- {bec_widgets-1.25.0.dist-info → bec_widgets-2.0.0.dist-info}/entry_points.txt +0 -0
- {bec_widgets-1.25.0.dist-info → bec_widgets-2.0.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,6 +1,8 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
3
|
import collections
|
4
|
+
import random
|
5
|
+
import string
|
4
6
|
from collections.abc import Callable
|
5
7
|
from typing import TYPE_CHECKING, Union
|
6
8
|
|
@@ -12,11 +14,15 @@ from bec_lib.service_config import ServiceConfig
|
|
12
14
|
from qtpy.QtCore import QObject
|
13
15
|
from qtpy.QtCore import Signal as pyqtSignal
|
14
16
|
|
17
|
+
from bec_widgets.utils.serialization import register_serializer_extension
|
18
|
+
|
15
19
|
logger = bec_logger.logger
|
16
20
|
|
17
|
-
if TYPE_CHECKING:
|
21
|
+
if TYPE_CHECKING: # pragma: no cover
|
18
22
|
from bec_lib.endpoints import EndpointInfo
|
19
23
|
|
24
|
+
from bec_widgets.utils.rpc_server import RPCServer
|
25
|
+
|
20
26
|
|
21
27
|
class QtThreadSafeCallback(QObject):
|
22
28
|
cb_signal = pyqtSignal(dict, dict)
|
@@ -73,14 +79,23 @@ class BECDispatcher:
|
|
73
79
|
|
74
80
|
_instance = None
|
75
81
|
_initialized = False
|
76
|
-
|
77
|
-
|
82
|
+
client: BECClient
|
83
|
+
cli_server: RPCServer | None = None
|
84
|
+
|
85
|
+
def __new__(
|
86
|
+
cls,
|
87
|
+
client=None,
|
88
|
+
config: str | ServiceConfig | None = None,
|
89
|
+
gui_id: str = None,
|
90
|
+
*args,
|
91
|
+
**kwargs,
|
92
|
+
):
|
78
93
|
if cls._instance is None:
|
79
94
|
cls._instance = super(BECDispatcher, cls).__new__(cls)
|
80
95
|
cls._initialized = False
|
81
96
|
return cls._instance
|
82
97
|
|
83
|
-
def __init__(self, client=None, config: str | ServiceConfig = None):
|
98
|
+
def __init__(self, client=None, config: str | ServiceConfig | None = None, gui_id: str = None):
|
84
99
|
if self._initialized:
|
85
100
|
return
|
86
101
|
|
@@ -107,11 +122,18 @@ class BECDispatcher:
|
|
107
122
|
except redis.exceptions.ConnectionError:
|
108
123
|
logger.warning("Could not connect to Redis, skipping start of BECClient.")
|
109
124
|
|
125
|
+
register_serializer_extension()
|
126
|
+
|
110
127
|
logger.success("Initialized BECDispatcher")
|
128
|
+
|
129
|
+
self.start_cli_server(gui_id=gui_id)
|
111
130
|
self._initialized = True
|
112
131
|
|
113
132
|
@classmethod
|
114
133
|
def reset_singleton(cls):
|
134
|
+
"""
|
135
|
+
Reset the singleton instance of the BECDispatcher.
|
136
|
+
"""
|
115
137
|
cls._instance = None
|
116
138
|
cls._initialized = False
|
117
139
|
|
@@ -178,4 +200,49 @@ class BECDispatcher:
|
|
178
200
|
*args: Arbitrary positional arguments
|
179
201
|
**kwargs: Arbitrary keyword arguments
|
180
202
|
"""
|
203
|
+
# pylint: disable=protected-access
|
181
204
|
self.disconnect_topics(self.client.connector._topics_cb)
|
205
|
+
|
206
|
+
def start_cli_server(self, gui_id: str | None = None):
|
207
|
+
"""
|
208
|
+
Start the CLI server.
|
209
|
+
|
210
|
+
Args:
|
211
|
+
gui_id(str, optional): The GUI ID. Defaults to None. If None, a unique identifier will be generated.
|
212
|
+
"""
|
213
|
+
# pylint: disable=import-outside-toplevel
|
214
|
+
from bec_widgets.utils.rpc_server import RPCServer
|
215
|
+
|
216
|
+
if gui_id is None:
|
217
|
+
gui_id = self.generate_unique_identifier()
|
218
|
+
|
219
|
+
if not self.client.started:
|
220
|
+
logger.error("Cannot start CLI server without a running client")
|
221
|
+
return
|
222
|
+
self.cli_server = RPCServer(gui_id, dispatcher=self, client=self.client)
|
223
|
+
logger.success(f"Started CLI server with gui_id: {gui_id}")
|
224
|
+
|
225
|
+
def stop_cli_server(self):
|
226
|
+
"""
|
227
|
+
Stop the CLI server.
|
228
|
+
"""
|
229
|
+
if self.cli_server is None:
|
230
|
+
logger.error("Cannot stop CLI server without starting it first")
|
231
|
+
return
|
232
|
+
self.cli_server.shutdown()
|
233
|
+
self.cli_server = None
|
234
|
+
logger.success("Stopped CLI server")
|
235
|
+
|
236
|
+
@staticmethod
|
237
|
+
def generate_unique_identifier(length: int = 4) -> str:
|
238
|
+
"""
|
239
|
+
Generate a unique identifier for the application.
|
240
|
+
|
241
|
+
Args:
|
242
|
+
length: The length of the identifier. Defaults to 4.
|
243
|
+
|
244
|
+
Returns:
|
245
|
+
str: The unique identifier.
|
246
|
+
"""
|
247
|
+
allowed_chars = string.ascii_lowercase + string.digits
|
248
|
+
return "".join(random.choices(allowed_chars, k=length))
|
@@ -0,0 +1,89 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import importlib.metadata
|
4
|
+
import inspect
|
5
|
+
import pkgutil
|
6
|
+
from importlib import util as importlib_util
|
7
|
+
from importlib.machinery import FileFinder, ModuleSpec, SourceFileLoader
|
8
|
+
from types import ModuleType
|
9
|
+
from typing import Generator
|
10
|
+
|
11
|
+
from bec_widgets.utils.bec_widget import BECWidget
|
12
|
+
|
13
|
+
|
14
|
+
def _submodule_specs(module: ModuleType) -> tuple[ModuleSpec | None, ...]:
|
15
|
+
"""Return specs for all submodules of the given module."""
|
16
|
+
return tuple(
|
17
|
+
module_info.module_finder.find_spec(module_info.name)
|
18
|
+
for module_info in pkgutil.iter_modules(module.__path__)
|
19
|
+
if isinstance(module_info.module_finder, FileFinder)
|
20
|
+
)
|
21
|
+
|
22
|
+
|
23
|
+
def _loaded_submodules_from_specs(
|
24
|
+
submodule_specs: tuple[ModuleSpec | None, ...]
|
25
|
+
) -> Generator[ModuleType, None, None]:
|
26
|
+
"""Load all submodules from the given specs."""
|
27
|
+
for submodule in (
|
28
|
+
importlib_util.module_from_spec(spec) for spec in submodule_specs if spec is not None
|
29
|
+
):
|
30
|
+
assert isinstance(
|
31
|
+
submodule.__loader__, SourceFileLoader
|
32
|
+
), "Module found from FileFinder should have SourceFileLoader!"
|
33
|
+
submodule.__loader__.exec_module(submodule)
|
34
|
+
yield submodule
|
35
|
+
|
36
|
+
|
37
|
+
def _submodule_by_name(module: ModuleType, name: str):
|
38
|
+
for submod in _loaded_submodules_from_specs(_submodule_specs(module)):
|
39
|
+
if submod.__name__ == name:
|
40
|
+
return submod
|
41
|
+
return None
|
42
|
+
|
43
|
+
|
44
|
+
def _get_widgets_from_module(module: ModuleType) -> dict[str, "type[BECWidget]"]:
|
45
|
+
"""Find any BECWidget subclasses in the given module and return them with their names."""
|
46
|
+
from bec_widgets.utils.bec_widget import BECWidget # avoid circular import
|
47
|
+
|
48
|
+
return dict(
|
49
|
+
inspect.getmembers(
|
50
|
+
module,
|
51
|
+
predicate=lambda item: inspect.isclass(item)
|
52
|
+
and issubclass(item, BECWidget)
|
53
|
+
and item is not BECWidget,
|
54
|
+
)
|
55
|
+
)
|
56
|
+
|
57
|
+
|
58
|
+
def _all_widgets_from_all_submods(module):
|
59
|
+
"""Recursively load submodules, find any BECWidgets, and return them all as a flat dict."""
|
60
|
+
widgets = _get_widgets_from_module(module)
|
61
|
+
if not hasattr(module, "__path__"):
|
62
|
+
return widgets
|
63
|
+
for submod in _loaded_submodules_from_specs(_submodule_specs(module)):
|
64
|
+
widgets.update(_all_widgets_from_all_submods(submod))
|
65
|
+
return widgets
|
66
|
+
|
67
|
+
|
68
|
+
def user_widget_plugin() -> ModuleType | None:
|
69
|
+
plugins = importlib.metadata.entry_points(group="bec.widgets.user_widgets") # type: ignore
|
70
|
+
return None if len(plugins) == 0 else tuple(plugins)[0].load()
|
71
|
+
|
72
|
+
|
73
|
+
def get_plugin_client_module() -> ModuleType | None:
|
74
|
+
"""If there is a plugin repository installed, return the client module."""
|
75
|
+
return _submodule_by_name(plugin, "client") if (plugin := user_widget_plugin()) else None
|
76
|
+
|
77
|
+
|
78
|
+
def get_all_plugin_widgets() -> dict[str, "type[BECWidget]"]:
|
79
|
+
"""If there is a plugin repository installed, load all widgets from it."""
|
80
|
+
if plugin := user_widget_plugin():
|
81
|
+
return _all_widgets_from_all_submods(plugin)
|
82
|
+
else:
|
83
|
+
return {}
|
84
|
+
|
85
|
+
|
86
|
+
if __name__ == "__main__": # pragma: no cover
|
87
|
+
# print(get_all_plugin_widgets())
|
88
|
+
client = get_plugin_client_module()
|
89
|
+
...
|
@@ -7,7 +7,7 @@ will allow you to decide by yourself when to unblock and execute the callback ag
|
|
7
7
|
from pyqtgraph import SignalProxy
|
8
8
|
from qtpy.QtCore import QTimer, Signal
|
9
9
|
|
10
|
-
from bec_widgets.
|
10
|
+
from bec_widgets.utils.error_popups import SafeSlot
|
11
11
|
|
12
12
|
|
13
13
|
class BECSignalProxy(SignalProxy):
|
bec_widgets/utils/bec_widget.py
CHANGED
@@ -1,13 +1,19 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
+
from typing import TYPE_CHECKING
|
4
|
+
|
3
5
|
import darkdetect
|
4
6
|
from bec_lib.logger import bec_logger
|
5
|
-
from qtpy.QtCore import Slot
|
6
|
-
from qtpy.QtWidgets import QApplication
|
7
|
+
from qtpy.QtCore import QObject, Slot
|
8
|
+
from qtpy.QtWidgets import QApplication
|
7
9
|
|
10
|
+
from bec_widgets.cli.rpc.rpc_register import RPCRegister
|
8
11
|
from bec_widgets.utils.bec_connector import BECConnector, ConnectionConfig
|
9
12
|
from bec_widgets.utils.colors import set_theme
|
10
13
|
|
14
|
+
if TYPE_CHECKING: # pragma: no cover
|
15
|
+
from bec_widgets.widgets.containers.dock import BECDock
|
16
|
+
|
11
17
|
logger = bec_logger.logger
|
12
18
|
|
13
19
|
|
@@ -17,13 +23,16 @@ class BECWidget(BECConnector):
|
|
17
23
|
# The icon name is the name of the icon in the icon theme, typically a name taken
|
18
24
|
# from fonts.google.com/icons. Override this in subclasses to set the icon name.
|
19
25
|
ICON_NAME = "widgets"
|
26
|
+
USER_ACCESS = ["remove"]
|
20
27
|
|
28
|
+
# pylint: disable=too-many-arguments
|
21
29
|
def __init__(
|
22
30
|
self,
|
23
31
|
client=None,
|
24
32
|
config: ConnectionConfig = None,
|
25
|
-
gui_id: str = None,
|
33
|
+
gui_id: str | None = None,
|
26
34
|
theme_update: bool = False,
|
35
|
+
parent_dock: BECDock | None = None, # TODO should go away -> issue created #473
|
27
36
|
**kwargs,
|
28
37
|
):
|
29
38
|
"""
|
@@ -43,11 +52,12 @@ class BECWidget(BECConnector):
|
|
43
52
|
theme_update(bool, optional): Whether to subscribe to theme updates. Defaults to False. When set to True, the
|
44
53
|
widget's apply_theme method will be called when the theme changes.
|
45
54
|
"""
|
46
|
-
if not isinstance(self, QWidget):
|
47
|
-
raise RuntimeError(f"{repr(self)} is not a subclass of QWidget")
|
48
|
-
super().__init__(client=client, config=config, gui_id=gui_id, **kwargs)
|
49
55
|
|
50
|
-
|
56
|
+
super().__init__(
|
57
|
+
client=client, config=config, gui_id=gui_id, parent_dock=parent_dock, **kwargs
|
58
|
+
)
|
59
|
+
if not isinstance(self, QObject):
|
60
|
+
raise RuntimeError(f"{repr(self)} is not a subclass of QWidget")
|
51
61
|
app = QApplication.instance()
|
52
62
|
if not hasattr(app, "theme"):
|
53
63
|
# DO NOT SET THE THEME TO AUTO! Otherwise, the qwebengineview will segfault
|
@@ -88,10 +98,16 @@ class BECWidget(BECConnector):
|
|
88
98
|
|
89
99
|
def cleanup(self):
|
90
100
|
"""Cleanup the widget."""
|
101
|
+
with RPCRegister.delayed_broadcast():
|
102
|
+
# All widgets need to call super().cleanup() in their cleanup method
|
103
|
+
logger.info(f"Registry cleanup for widget {self.__class__.__name__}")
|
104
|
+
self.rpc_register.remove_rpc(self)
|
91
105
|
|
92
106
|
def closeEvent(self, event):
|
93
|
-
|
107
|
+
"""Wrap the close even to ensure the rpc_register is cleaned up."""
|
94
108
|
try:
|
95
|
-
self.
|
109
|
+
if not self._destroyed:
|
110
|
+
self.cleanup()
|
111
|
+
self._destroyed = True
|
96
112
|
finally:
|
97
|
-
super().closeEvent(event)
|
113
|
+
super().closeEvent(event) # pylint: disable=no-member
|
bec_widgets/utils/colors.py
CHANGED
@@ -1,30 +1,55 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
3
|
import itertools
|
4
|
-
from typing import Type
|
4
|
+
from typing import Literal, Type
|
5
5
|
|
6
6
|
from qtpy.QtWidgets import QWidget
|
7
7
|
|
8
|
+
from bec_widgets.cli.rpc.rpc_register import RPCRegister
|
9
|
+
|
8
10
|
|
9
11
|
class WidgetContainerUtils:
|
10
12
|
|
13
|
+
# We need one handler that checks if a WIDGET of a given name is already created for that DOCKAREA
|
14
|
+
# 1. If the name exists, then it depends whether the name was auto-generated -> add _1 to the name
|
15
|
+
# or alternatively raise an error that it can't be added again ( just raise an error)
|
16
|
+
# 2. Dock names in between docks should also be unique
|
17
|
+
|
11
18
|
@staticmethod
|
12
|
-
def
|
13
|
-
"""
|
14
|
-
Generate a unique widget ID.
|
19
|
+
def has_name_valid_chars(name: str) -> bool:
|
20
|
+
"""Check if the name is valid.
|
15
21
|
|
16
22
|
Args:
|
17
|
-
|
18
|
-
|
23
|
+
name(str): The name to be checked.
|
24
|
+
|
25
|
+
Returns:
|
26
|
+
bool: True if the name is valid, False otherwise.
|
27
|
+
"""
|
28
|
+
if not name or len(name) > 256:
|
29
|
+
return False # Don't accept empty names or names longer than 256 characters
|
30
|
+
check_value = name.replace("_", "").replace("-", "")
|
31
|
+
if not check_value.isalnum() or not check_value.isascii():
|
32
|
+
return False
|
33
|
+
return True
|
19
34
|
|
35
|
+
@staticmethod
|
36
|
+
def generate_unique_name(name: str, list_of_names: list[str] | None = None) -> str:
|
37
|
+
"""Generate a unique ID.
|
38
|
+
|
39
|
+
Args:
|
40
|
+
name(str): The name of the widget.
|
20
41
|
Returns:
|
21
|
-
|
42
|
+
tuple (str): The unique name
|
22
43
|
"""
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
44
|
+
if list_of_names is None:
|
45
|
+
list_of_names = []
|
46
|
+
ii = 0
|
47
|
+
while ii < 1000: # 1000 is arbritrary!
|
48
|
+
name_candidate = f"{name}_{ii}"
|
49
|
+
if name_candidate not in list_of_names:
|
50
|
+
return name_candidate
|
51
|
+
ii += 1
|
52
|
+
raise ValueError("Could not generate a unique name after within 1000 attempts.")
|
28
53
|
|
29
54
|
@staticmethod
|
30
55
|
def find_first_widget_by_class(
|
bec_widgets/utils/crosshair.py
CHANGED
@@ -1,4 +1,7 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
1
3
|
from collections import defaultdict
|
4
|
+
from typing import Any
|
2
5
|
|
3
6
|
import numpy as np
|
4
7
|
import pyqtgraph as pg
|
@@ -197,15 +200,18 @@ class Crosshair(QObject):
|
|
197
200
|
self.marker_2d = pg.ROI(
|
198
201
|
[0, 0], size=[1, 1], pen=pg.mkPen("r", width=2), movable=False
|
199
202
|
)
|
203
|
+
self.marker_2d.skip_auto_range = True
|
200
204
|
self.plot_item.addItem(self.marker_2d)
|
201
205
|
|
202
|
-
def snap_to_data(
|
206
|
+
def snap_to_data(
|
207
|
+
self, x: float, y: float
|
208
|
+
) -> tuple[None, None] | tuple[defaultdict[Any, list], defaultdict[Any, list]]:
|
203
209
|
"""
|
204
210
|
Finds the nearest data points to the given x and y coordinates.
|
205
211
|
|
206
212
|
Args:
|
207
|
-
x: The x-coordinate of the mouse cursor
|
208
|
-
y: The y-coordinate of the mouse cursor
|
213
|
+
x(float): The x-coordinate of the mouse cursor
|
214
|
+
y(float): The y-coordinate of the mouse cursor
|
209
215
|
|
210
216
|
Returns:
|
211
217
|
tuple: x and y values snapped to the nearest data
|
@@ -235,7 +241,7 @@ class Crosshair(QObject):
|
|
235
241
|
y_values[name] = closest_y
|
236
242
|
x_values[name] = closest_x
|
237
243
|
elif isinstance(item, pg.ImageItem): # 2D plot
|
238
|
-
name = item.config.monitor
|
244
|
+
name = item.config.monitor or str(id(item))
|
239
245
|
image_2d = item.image
|
240
246
|
# Clip the x and y values to the image dimensions to avoid out of bounds errors
|
241
247
|
y_values[name] = int(np.clip(y, 0, image_2d.shape[1] - 1))
|
@@ -320,7 +326,7 @@ class Crosshair(QObject):
|
|
320
326
|
)
|
321
327
|
self.coordinatesChanged1D.emit(coordinate_to_emit)
|
322
328
|
elif isinstance(item, pg.ImageItem):
|
323
|
-
name = item.config.monitor
|
329
|
+
name = item.config.monitor or str(id(item))
|
324
330
|
x, y = x_snap_values[name], y_snap_values[name]
|
325
331
|
if x is None or y is None:
|
326
332
|
continue
|
@@ -374,7 +380,7 @@ class Crosshair(QObject):
|
|
374
380
|
)
|
375
381
|
self.coordinatesClicked1D.emit(coordinate_to_emit)
|
376
382
|
elif isinstance(item, pg.ImageItem):
|
377
|
-
name = item.config.monitor
|
383
|
+
name = item.config.monitor or str(id(item))
|
378
384
|
x, y = x_snap_values[name], y_snap_values[name]
|
379
385
|
if x is None or y is None:
|
380
386
|
continue
|
@@ -418,9 +424,17 @@ class Crosshair(QObject):
|
|
418
424
|
"""
|
419
425
|
x, y = pos
|
420
426
|
x_scaled, y_scaled = self.scale_emitted_coordinates(x, y)
|
421
|
-
|
427
|
+
text = f"({x_scaled:.{self.precision}g}, {y_scaled:.{self.precision}g})"
|
428
|
+
for item in self.items:
|
429
|
+
if isinstance(item, pg.ImageItem):
|
430
|
+
image = item.image
|
431
|
+
ix = int(np.clip(x, 0, image.shape[0] - 1))
|
432
|
+
iy = int(np.clip(y, 0, image.shape[1] - 1))
|
433
|
+
intensity = image[ix, iy]
|
434
|
+
text += f"\nIntensity: {intensity:.{self.precision}g}"
|
435
|
+
break
|
422
436
|
# Update coordinate label
|
423
|
-
self.coord_label.setText(
|
437
|
+
self.coord_label.setText(text)
|
424
438
|
self.coord_label.setPos(x, y)
|
425
439
|
self.coord_label.setVisible(True)
|
426
440
|
|
@@ -436,6 +450,9 @@ class Crosshair(QObject):
|
|
436
450
|
self.clear_markers()
|
437
451
|
|
438
452
|
def cleanup(self):
|
453
|
+
if self.marker_2d is not None:
|
454
|
+
self.plot_item.removeItem(self.marker_2d)
|
455
|
+
self.marker_2d = None
|
439
456
|
self.plot_item.removeItem(self.v_line)
|
440
457
|
self.plot_item.removeItem(self.h_line)
|
441
458
|
self.plot_item.removeItem(self.coord_label)
|
@@ -22,7 +22,9 @@ class EntryValidator:
|
|
22
22
|
if entry is None or entry == "":
|
23
23
|
entry = next(iter(device._hints), name) if hasattr(device, "_hints") else name
|
24
24
|
if entry not in description:
|
25
|
-
raise ValueError(
|
25
|
+
raise ValueError(
|
26
|
+
f"Entry '{entry}' not found in device '{name}' signals. Available signals: {description.keys()}"
|
27
|
+
)
|
26
28
|
|
27
29
|
return entry
|
28
30
|
|
@@ -96,15 +96,33 @@ def SafeSlot(*slot_args, **slot_kwargs): # pylint: disable=invalid-name
|
|
96
96
|
|
97
97
|
'popup_error' keyword argument can be passed with boolean value if a dialog should pop up,
|
98
98
|
otherwise error display is left to the original exception hook
|
99
|
+
'verify_sender' keyword argument can be passed with boolean value if the sender should be verified
|
100
|
+
before executing the slot. If True, the slot will only execute if the sender is a QObject. This is
|
101
|
+
useful to prevent function calls from already deleted objects.
|
99
102
|
"""
|
100
103
|
popup_error = bool(slot_kwargs.pop("popup_error", False))
|
104
|
+
verify_sender = bool(slot_kwargs.pop("verify_sender", False))
|
101
105
|
|
102
106
|
def error_managed(method):
|
103
107
|
@Slot(*slot_args, **slot_kwargs)
|
104
108
|
@functools.wraps(method)
|
105
109
|
def wrapper(*args, **kwargs):
|
106
110
|
try:
|
111
|
+
if not verify_sender or len(args) == 0:
|
112
|
+
return method(*args, **kwargs)
|
113
|
+
|
114
|
+
_instance = args[0]
|
115
|
+
if not isinstance(_instance, QObject):
|
116
|
+
return method(*args, **kwargs)
|
117
|
+
sender = _instance.sender()
|
118
|
+
if sender is None:
|
119
|
+
logger.info(
|
120
|
+
f"Sender is None for {method.__module__}.{method.__qualname__}, "
|
121
|
+
"skipping method call."
|
122
|
+
)
|
123
|
+
return
|
107
124
|
return method(*args, **kwargs)
|
125
|
+
|
108
126
|
except Exception:
|
109
127
|
slot_name = f"{method.__module__}.{method.__qualname__}"
|
110
128
|
error_msg = traceback.format_exc()
|
@@ -12,7 +12,7 @@ from qtpy.QtWidgets import (
|
|
12
12
|
QWidget,
|
13
13
|
)
|
14
14
|
|
15
|
-
from bec_widgets.
|
15
|
+
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
|
16
16
|
|
17
17
|
|
18
18
|
class ExpandableGroupFrame(QFrame):
|
@@ -37,7 +37,7 @@ class ExpandableGroupFrame(QFrame):
|
|
37
37
|
self._title_layout.addWidget(self._expansion_button)
|
38
38
|
self._title_layout.addWidget(self._title)
|
39
39
|
|
40
|
-
self._contents = QWidget()
|
40
|
+
self._contents = QWidget(self)
|
41
41
|
self._layout.addWidget(self._contents)
|
42
42
|
|
43
43
|
self._expansion_button.clicked.connect(self.switch_expanded_state)
|