bec-widgets 1.25.1__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 +3 -5
- CHANGELOG.md +631 -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 +31 -13
- bec_widgets/widgets/control/device_control/positioner_box/positioner_box/positioner_box.py +3 -1
- 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/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.1.dist-info → bec_widgets-2.0.0.dist-info}/METADATA +3 -3
- {bec_widgets-1.25.1.dist-info → bec_widgets-2.0.0.dist-info}/RECORD +168 -153
- 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.1.dist-info → bec_widgets-2.0.0.dist-info}/WHEEL +0 -0
- {bec_widgets-1.25.1.dist-info → bec_widgets-2.0.0.dist-info}/entry_points.txt +0 -0
- {bec_widgets-1.25.1.dist-info → bec_widgets-2.0.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,277 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import functools
|
4
|
+
import traceback
|
5
|
+
import types
|
6
|
+
from contextlib import contextmanager
|
7
|
+
from typing import TYPE_CHECKING, Callable, TypeVar
|
8
|
+
|
9
|
+
from bec_lib.client import BECClient
|
10
|
+
from bec_lib.endpoints import MessageEndpoints
|
11
|
+
from bec_lib.logger import bec_logger
|
12
|
+
from bec_lib.utils.import_utils import lazy_import
|
13
|
+
from qtpy.QtCore import QTimer
|
14
|
+
from qtpy.QtWidgets import QApplication
|
15
|
+
from redis.exceptions import RedisError
|
16
|
+
|
17
|
+
from bec_widgets.cli.rpc.rpc_register import RPCRegister
|
18
|
+
from bec_widgets.utils import BECDispatcher
|
19
|
+
from bec_widgets.utils.bec_connector import BECConnector
|
20
|
+
from bec_widgets.utils.error_popups import ErrorPopupUtility
|
21
|
+
from bec_widgets.widgets.containers.main_window.main_window import BECMainWindow
|
22
|
+
|
23
|
+
if TYPE_CHECKING: # pragma: no cover
|
24
|
+
from bec_lib import messages
|
25
|
+
from qtpy.QtCore import QObject
|
26
|
+
else:
|
27
|
+
messages = lazy_import("bec_lib.messages")
|
28
|
+
logger = bec_logger.logger
|
29
|
+
|
30
|
+
|
31
|
+
T = TypeVar("T")
|
32
|
+
|
33
|
+
|
34
|
+
@contextmanager
|
35
|
+
def rpc_exception_hook(err_func):
|
36
|
+
"""This context replaces the popup message box for error display with a specific hook"""
|
37
|
+
# get error popup utility singleton
|
38
|
+
popup = ErrorPopupUtility()
|
39
|
+
# save current setting
|
40
|
+
old_exception_hook = popup.custom_exception_hook
|
41
|
+
|
42
|
+
# install err_func, if it is a callable
|
43
|
+
# IMPORTANT, Keep self here, because this method is overwriting the custom_exception_hook
|
44
|
+
# of the ErrorPopupUtility (popup instance) class.
|
45
|
+
def custom_exception_hook(self, exc_type, value, tb, **kwargs):
|
46
|
+
err_func({"error": popup.get_error_message(exc_type, value, tb)})
|
47
|
+
|
48
|
+
popup.custom_exception_hook = types.MethodType(custom_exception_hook, popup)
|
49
|
+
|
50
|
+
try:
|
51
|
+
yield popup
|
52
|
+
finally:
|
53
|
+
# restore state of error popup utility singleton
|
54
|
+
popup.custom_exception_hook = old_exception_hook
|
55
|
+
|
56
|
+
|
57
|
+
class RPCServer:
|
58
|
+
|
59
|
+
client: BECClient
|
60
|
+
|
61
|
+
def __init__(
|
62
|
+
self,
|
63
|
+
gui_id: str,
|
64
|
+
dispatcher: BECDispatcher | None = None,
|
65
|
+
client: BECClient | None = None,
|
66
|
+
config=None,
|
67
|
+
gui_class_id: str = "bec",
|
68
|
+
) -> None:
|
69
|
+
self.status = messages.BECStatus.BUSY
|
70
|
+
self.dispatcher = BECDispatcher(config=config) if dispatcher is None else dispatcher
|
71
|
+
self.client = self.dispatcher.client if client is None else client
|
72
|
+
self.client.start()
|
73
|
+
self.gui_id = gui_id
|
74
|
+
# register broadcast callback
|
75
|
+
self.rpc_register = RPCRegister()
|
76
|
+
self.rpc_register.add_callback(self.broadcast_registry_update)
|
77
|
+
|
78
|
+
self.dispatcher.connect_slot(
|
79
|
+
self.on_rpc_update, MessageEndpoints.gui_instructions(self.gui_id)
|
80
|
+
)
|
81
|
+
|
82
|
+
# Setup QTimer for heartbeat
|
83
|
+
self._heartbeat_timer = QTimer()
|
84
|
+
self._heartbeat_timer.timeout.connect(self.emit_heartbeat)
|
85
|
+
self._heartbeat_timer.start(200)
|
86
|
+
self._registry_update_callbacks = []
|
87
|
+
self._broadcasted_data = {}
|
88
|
+
|
89
|
+
self.status = messages.BECStatus.RUNNING
|
90
|
+
logger.success(f"Server started with gui_id: {self.gui_id}")
|
91
|
+
|
92
|
+
def on_rpc_update(self, msg: dict, metadata: dict):
|
93
|
+
request_id = metadata.get("request_id")
|
94
|
+
if request_id is None:
|
95
|
+
logger.error("Received RPC instruction without request_id")
|
96
|
+
return
|
97
|
+
logger.debug(f"Received RPC instruction: {msg}, metadata: {metadata}")
|
98
|
+
with rpc_exception_hook(functools.partial(self.send_response, request_id, False)):
|
99
|
+
try:
|
100
|
+
obj = self.get_object_from_config(msg["parameter"])
|
101
|
+
method = msg["action"]
|
102
|
+
args = msg["parameter"].get("args", [])
|
103
|
+
kwargs = msg["parameter"].get("kwargs", {})
|
104
|
+
res = self.run_rpc(obj, method, args, kwargs)
|
105
|
+
except Exception:
|
106
|
+
content = traceback.format_exc()
|
107
|
+
logger.error(f"Error while executing RPC instruction: {content}")
|
108
|
+
self.send_response(request_id, False, {"error": content})
|
109
|
+
else:
|
110
|
+
logger.debug(f"RPC instruction executed successfully: {res}")
|
111
|
+
self.send_response(request_id, True, {"result": res})
|
112
|
+
|
113
|
+
def send_response(self, request_id: str, accepted: bool, msg: dict):
|
114
|
+
self.client.connector.set_and_publish(
|
115
|
+
MessageEndpoints.gui_instruction_response(request_id),
|
116
|
+
messages.RequestResponseMessage(accepted=accepted, message=msg),
|
117
|
+
expire=60,
|
118
|
+
)
|
119
|
+
|
120
|
+
def get_object_from_config(self, config: dict):
|
121
|
+
gui_id = config.get("gui_id")
|
122
|
+
obj = self.rpc_register.get_rpc_by_id(gui_id)
|
123
|
+
if obj is None:
|
124
|
+
raise ValueError(f"Object with gui_id {gui_id} not found")
|
125
|
+
return obj
|
126
|
+
|
127
|
+
def run_rpc(self, obj, method, args, kwargs):
|
128
|
+
# Run with rpc registry broadcast, but only once
|
129
|
+
with RPCRegister.delayed_broadcast():
|
130
|
+
logger.debug(f"Running RPC instruction: {method} with args: {args}, kwargs: {kwargs}")
|
131
|
+
method_obj = getattr(obj, method)
|
132
|
+
# check if the method accepts args and kwargs
|
133
|
+
if not callable(method_obj):
|
134
|
+
if not args:
|
135
|
+
res = method_obj
|
136
|
+
else:
|
137
|
+
setattr(obj, method, args[0])
|
138
|
+
res = None
|
139
|
+
else:
|
140
|
+
res = method_obj(*args, **kwargs)
|
141
|
+
|
142
|
+
if isinstance(res, list):
|
143
|
+
res = [self.serialize_object(obj) for obj in res]
|
144
|
+
elif isinstance(res, dict):
|
145
|
+
res = {key: self.serialize_object(val) for key, val in res.items()}
|
146
|
+
else:
|
147
|
+
res = self.serialize_object(res)
|
148
|
+
return res
|
149
|
+
|
150
|
+
def serialize_object(self, obj: T) -> None | dict | T:
|
151
|
+
"""
|
152
|
+
Serialize all BECConnector objects.
|
153
|
+
|
154
|
+
Args:
|
155
|
+
obj: The object to be serialized.
|
156
|
+
|
157
|
+
Returns:
|
158
|
+
None | dict | T: The serialized object or None if the object is not a BECConnector.
|
159
|
+
"""
|
160
|
+
if not isinstance(obj, BECConnector):
|
161
|
+
return obj
|
162
|
+
# Respect RPC = False
|
163
|
+
if getattr(obj, "RPC", True) is False:
|
164
|
+
return None
|
165
|
+
return self._serialize_bec_connector(obj, wait=True)
|
166
|
+
|
167
|
+
def emit_heartbeat(self) -> None:
|
168
|
+
"""
|
169
|
+
Emit a heartbeat message to the GUI server.
|
170
|
+
This method is called periodically to indicate that the server is still running.
|
171
|
+
"""
|
172
|
+
logger.trace(f"Emitting heartbeat for {self.gui_id}")
|
173
|
+
try:
|
174
|
+
self.client.connector.set(
|
175
|
+
MessageEndpoints.gui_heartbeat(self.gui_id),
|
176
|
+
messages.StatusMessage(name=self.gui_id, status=self.status, info={}),
|
177
|
+
expire=10,
|
178
|
+
)
|
179
|
+
except RedisError as exc:
|
180
|
+
logger.error(f"Error while emitting heartbeat: {exc}")
|
181
|
+
|
182
|
+
def broadcast_registry_update(self, connections: dict) -> None:
|
183
|
+
"""
|
184
|
+
Broadcast the registry update to all the callbacks.
|
185
|
+
This method is called whenever the registry is updated.
|
186
|
+
"""
|
187
|
+
data = {}
|
188
|
+
for key, val in connections.items():
|
189
|
+
if not isinstance(val, BECConnector):
|
190
|
+
continue
|
191
|
+
if not getattr(val, "RPC", True):
|
192
|
+
continue
|
193
|
+
data[key] = self._serialize_bec_connector(val)
|
194
|
+
if self._broadcasted_data == data:
|
195
|
+
return
|
196
|
+
self._broadcasted_data = data
|
197
|
+
|
198
|
+
logger.info(f"Broadcasting registry update: {data} for {self.gui_id}")
|
199
|
+
self.client.connector.xadd(
|
200
|
+
MessageEndpoints.gui_registry_state(self.gui_id),
|
201
|
+
msg_dict={"data": messages.GUIRegistryStateMessage(state=data)},
|
202
|
+
max_size=1,
|
203
|
+
)
|
204
|
+
|
205
|
+
def _serialize_bec_connector(self, connector: BECConnector, wait=False) -> dict:
|
206
|
+
"""
|
207
|
+
Create the serialization dict for a single BECConnector.
|
208
|
+
|
209
|
+
Args:
|
210
|
+
connector (BECConnector): The BECConnector to serialize.
|
211
|
+
wait (bool): If True, wait until the object is registered in the RPC register.
|
212
|
+
|
213
|
+
Returns:
|
214
|
+
dict: The serialized BECConnector object.
|
215
|
+
"""
|
216
|
+
|
217
|
+
config_dict = connector.config.model_dump()
|
218
|
+
config_dict["parent_id"] = getattr(connector, "parent_id", None)
|
219
|
+
|
220
|
+
try:
|
221
|
+
parent = connector.parent()
|
222
|
+
if isinstance(parent, BECMainWindow):
|
223
|
+
container_proxy = parent.gui_id
|
224
|
+
else:
|
225
|
+
container_proxy = None
|
226
|
+
except Exception:
|
227
|
+
container_proxy = None
|
228
|
+
|
229
|
+
if wait:
|
230
|
+
while not self.rpc_register.object_is_registered(connector):
|
231
|
+
QApplication.processEvents()
|
232
|
+
|
233
|
+
widget_class = getattr(connector, "rpc_widget_class", None)
|
234
|
+
if not widget_class:
|
235
|
+
widget_class = connector.__class__.__name__
|
236
|
+
|
237
|
+
return {
|
238
|
+
"gui_id": connector.gui_id,
|
239
|
+
"object_name": connector.object_name or connector.__class__.__name__,
|
240
|
+
"widget_class": widget_class,
|
241
|
+
"config": config_dict,
|
242
|
+
"container_proxy": container_proxy,
|
243
|
+
"__rpc__": True,
|
244
|
+
}
|
245
|
+
|
246
|
+
@staticmethod
|
247
|
+
def _get_becwidget_ancestor(widget: QObject) -> BECConnector | None:
|
248
|
+
"""
|
249
|
+
Traverse up the parent chain to find the nearest BECConnector.
|
250
|
+
Returns None if none is found.
|
251
|
+
"""
|
252
|
+
|
253
|
+
parent = widget.parent()
|
254
|
+
while parent is not None:
|
255
|
+
if isinstance(parent, BECConnector):
|
256
|
+
return parent
|
257
|
+
parent = parent.parent()
|
258
|
+
return None
|
259
|
+
|
260
|
+
# Suppose clients register callbacks to receive updates
|
261
|
+
def add_registry_update_callback(self, cb: Callable) -> None:
|
262
|
+
"""
|
263
|
+
Add a callback to be called whenever the registry is updated.
|
264
|
+
The specified callback is called whenever the registry is updated.
|
265
|
+
|
266
|
+
Args:
|
267
|
+
cb (Callable): The callback to be added. It should accept a dictionary of all the
|
268
|
+
registered RPC objects as an argument.
|
269
|
+
"""
|
270
|
+
self._registry_update_callbacks.append(cb)
|
271
|
+
|
272
|
+
def shutdown(self): # TODO not sure if needed when cleanup is done at level of BECConnector
|
273
|
+
self.status = messages.BECStatus.IDLE
|
274
|
+
self._heartbeat_timer.stop()
|
275
|
+
self.emit_heartbeat()
|
276
|
+
logger.info("Succeded in shutting down CLI server")
|
277
|
+
self.client.shutdown()
|
@@ -0,0 +1,44 @@
|
|
1
|
+
from bec_lib.serialization import msgpack
|
2
|
+
from qtpy.QtCore import QPointF
|
3
|
+
|
4
|
+
|
5
|
+
def register_serializer_extension():
|
6
|
+
"""
|
7
|
+
Register the serializer extension for the BECConnector.
|
8
|
+
"""
|
9
|
+
if not module_is_registered("bec_widgets.utils.serialization"):
|
10
|
+
msgpack.register_object_hook(encode_qpointf, decode_qpointf)
|
11
|
+
|
12
|
+
|
13
|
+
def module_is_registered(module_name: str) -> bool:
|
14
|
+
"""
|
15
|
+
Check if the module is registered in the encoder.
|
16
|
+
|
17
|
+
Args:
|
18
|
+
module_name (str): The name of the module to check.
|
19
|
+
|
20
|
+
Returns:
|
21
|
+
bool: True if the module is registered, False otherwise.
|
22
|
+
"""
|
23
|
+
# pylint: disable=protected-access
|
24
|
+
for enc in msgpack._encoder:
|
25
|
+
if enc[0].__module__ == module_name:
|
26
|
+
return True
|
27
|
+
return False
|
28
|
+
|
29
|
+
|
30
|
+
def encode_qpointf(obj):
|
31
|
+
"""
|
32
|
+
Encode a QPointF object to a list of floats. As this is mostly used for sending
|
33
|
+
data to the client, it is not necessary to convert it back to a QPointF object.
|
34
|
+
"""
|
35
|
+
if isinstance(obj, QPointF):
|
36
|
+
return [obj.x(), obj.y()]
|
37
|
+
return obj
|
38
|
+
|
39
|
+
|
40
|
+
def decode_qpointf(obj):
|
41
|
+
"""
|
42
|
+
no-op function since QPointF is encoded as a list of floats.
|
43
|
+
"""
|
44
|
+
return obj
|
@@ -1,6 +1,10 @@
|
|
1
|
+
from bec_lib.logger import bec_logger
|
2
|
+
from PySide6.QtGui import QCloseEvent
|
1
3
|
from qtpy.QtWidgets import QDialog, QDialogButtonBox, QHBoxLayout, QPushButton, QVBoxLayout, QWidget
|
2
4
|
|
3
|
-
from bec_widgets.
|
5
|
+
from bec_widgets.utils.error_popups import SafeSlot
|
6
|
+
|
7
|
+
logger = bec_logger.logger
|
4
8
|
|
5
9
|
|
6
10
|
class SettingWidget(QWidget):
|
@@ -37,6 +41,15 @@ class SettingWidget(QWidget):
|
|
37
41
|
"""
|
38
42
|
pass
|
39
43
|
|
44
|
+
def cleanup(self):
|
45
|
+
"""
|
46
|
+
Cleanup the settings widget.
|
47
|
+
"""
|
48
|
+
|
49
|
+
def closeEvent(self, event: QCloseEvent) -> None:
|
50
|
+
self.cleanup()
|
51
|
+
return super().closeEvent(event)
|
52
|
+
|
40
53
|
|
41
54
|
class SettingsDialog(QDialog):
|
42
55
|
"""
|
@@ -99,8 +112,17 @@ class SettingsDialog(QDialog):
|
|
99
112
|
Accept the changes made in the settings widget and close the dialog.
|
100
113
|
"""
|
101
114
|
self.widget.accept_changes()
|
115
|
+
self.cleanup()
|
102
116
|
super().accept()
|
103
117
|
|
118
|
+
@SafeSlot()
|
119
|
+
def reject(self):
|
120
|
+
"""
|
121
|
+
Reject the changes made in the settings widget and close the dialog.
|
122
|
+
"""
|
123
|
+
self.cleanup()
|
124
|
+
super().reject()
|
125
|
+
|
104
126
|
@SafeSlot()
|
105
127
|
def apply_changes(self):
|
106
128
|
"""
|
@@ -114,7 +136,10 @@ class SettingsDialog(QDialog):
|
|
114
136
|
"""
|
115
137
|
self.button_box.close()
|
116
138
|
self.button_box.deleteLater()
|
139
|
+
self.widget.close()
|
140
|
+
self.widget.deleteLater()
|
117
141
|
|
118
142
|
def closeEvent(self, event):
|
143
|
+
logger.info("Closing settings dialog")
|
119
144
|
self.cleanup()
|
120
145
|
super().closeEvent(event)
|
@@ -16,7 +16,7 @@ from qtpy.QtWidgets import (
|
|
16
16
|
QWidget,
|
17
17
|
)
|
18
18
|
|
19
|
-
from bec_widgets.
|
19
|
+
from bec_widgets.utils.toolbar import MaterialIconAction, ModularToolBar
|
20
20
|
|
21
21
|
|
22
22
|
class SidePanel(QWidget):
|
@@ -35,7 +35,6 @@ class SidePanel(QWidget):
|
|
35
35
|
super().__init__(parent=parent)
|
36
36
|
|
37
37
|
self.setProperty("skip_settings", True)
|
38
|
-
self.setObjectName("SidePanel")
|
39
38
|
|
40
39
|
self._orientation = orientation
|
41
40
|
self._panel_max_width = panel_max_width
|
@@ -60,7 +59,7 @@ class SidePanel(QWidget):
|
|
60
59
|
self.main_layout.setContentsMargins(0, 0, 0, 0)
|
61
60
|
self.main_layout.setSpacing(0)
|
62
61
|
|
63
|
-
self.toolbar = ModularToolBar(target_widget=self, orientation="vertical")
|
62
|
+
self.toolbar = ModularToolBar(parent=self, target_widget=self, orientation="vertical")
|
64
63
|
|
65
64
|
self.container = QWidget()
|
66
65
|
self.container.layout = QVBoxLayout(self.container)
|
@@ -90,7 +89,7 @@ class SidePanel(QWidget):
|
|
90
89
|
self.main_layout.setContentsMargins(0, 0, 0, 0)
|
91
90
|
self.main_layout.setSpacing(0)
|
92
91
|
|
93
|
-
self.toolbar = ModularToolBar(target_widget=self, orientation="horizontal")
|
92
|
+
self.toolbar = ModularToolBar(parent=self, target_widget=self, orientation="horizontal")
|
94
93
|
|
95
94
|
self.container = QWidget()
|
96
95
|
self.container.layout = QVBoxLayout(self.container)
|
@@ -232,7 +231,14 @@ class SidePanel(QWidget):
|
|
232
231
|
self.stack_widget.setCurrentIndex(idx)
|
233
232
|
self.current_index = idx
|
234
233
|
|
235
|
-
def add_menu(
|
234
|
+
def add_menu(
|
235
|
+
self,
|
236
|
+
action_id: str,
|
237
|
+
icon_name: str,
|
238
|
+
tooltip: str,
|
239
|
+
widget: QWidget,
|
240
|
+
title: str | None = None,
|
241
|
+
):
|
236
242
|
"""
|
237
243
|
Add a menu to the side panel.
|
238
244
|
|
@@ -249,9 +255,10 @@ class SidePanel(QWidget):
|
|
249
255
|
container_layout.setContentsMargins(0, 0, 0, 0)
|
250
256
|
container_layout.setSpacing(5)
|
251
257
|
|
252
|
-
|
253
|
-
|
254
|
-
|
258
|
+
if title is not None:
|
259
|
+
title_label = QLabel(f"<b>{title}</b>")
|
260
|
+
title_label.setStyleSheet("font-size: 16px;")
|
261
|
+
container_layout.addWidget(title_label)
|
255
262
|
|
256
263
|
# Create a QScrollArea for the actual widget to ensure scrolling if the widget inside is too large
|
257
264
|
scroll_area = QScrollArea()
|
@@ -317,9 +324,9 @@ class ExampleApp(QMainWindow): # pragma: no cover
|
|
317
324
|
self.side_panel = SidePanel(self, orientation="left", panel_max_width=250)
|
318
325
|
self.layout.addWidget(self.side_panel)
|
319
326
|
|
320
|
-
from bec_widgets.widgets.plots.waveform.
|
327
|
+
from bec_widgets.widgets.plots.waveform.waveform import Waveform
|
321
328
|
|
322
|
-
self.plot =
|
329
|
+
self.plot = Waveform()
|
323
330
|
self.layout.addWidget(self.plot)
|
324
331
|
|
325
332
|
self.add_side_menus()
|
@@ -118,7 +118,7 @@ class IconAction(ToolBarAction):
|
|
118
118
|
def add_to_toolbar(self, toolbar: QToolBar, target: QWidget):
|
119
119
|
icon = QIcon()
|
120
120
|
icon.addFile(self.icon_path, size=QSize(20, 20))
|
121
|
-
self.action = QAction(icon, self.tooltip, target)
|
121
|
+
self.action = QAction(icon=icon, text=self.tooltip, parent=target)
|
122
122
|
self.action.setCheckable(self.checkable)
|
123
123
|
toolbar.addAction(self.action)
|
124
124
|
|
@@ -128,7 +128,7 @@ class QtIconAction(ToolBarAction):
|
|
128
128
|
super().__init__(icon_path=None, tooltip=tooltip, checkable=checkable)
|
129
129
|
self.standard_icon = standard_icon
|
130
130
|
self.icon = QApplication.style().standardIcon(standard_icon)
|
131
|
-
self.action = QAction(self.icon, self.tooltip, parent)
|
131
|
+
self.action = QAction(icon=self.icon, text=self.tooltip, parent=parent)
|
132
132
|
self.action.setCheckable(self.checkable)
|
133
133
|
|
134
134
|
def add_to_toolbar(self, toolbar, target):
|
@@ -173,7 +173,7 @@ class MaterialIconAction(ToolBarAction):
|
|
173
173
|
filled=self.filled,
|
174
174
|
color=self.color,
|
175
175
|
)
|
176
|
-
self.action = QAction(self.icon, self.tooltip, parent=parent)
|
176
|
+
self.action = QAction(icon=self.icon, text=self.tooltip, parent=parent)
|
177
177
|
self.action.setCheckable(self.checkable)
|
178
178
|
|
179
179
|
def add_to_toolbar(self, toolbar: QToolBar, target: QWidget):
|
@@ -212,12 +212,12 @@ class DeviceSelectionAction(ToolBarAction):
|
|
212
212
|
self.device_combobox.currentIndexChanged.connect(lambda: self.set_combobox_style("#ffa700"))
|
213
213
|
|
214
214
|
def add_to_toolbar(self, toolbar, target):
|
215
|
-
widget = QWidget()
|
215
|
+
widget = QWidget(parent=target)
|
216
216
|
layout = QHBoxLayout(widget)
|
217
217
|
layout.setContentsMargins(0, 0, 0, 0)
|
218
218
|
layout.setSpacing(0)
|
219
219
|
if self.label is not None:
|
220
|
-
label = QLabel(f"{self.label}")
|
220
|
+
label = QLabel(text=f"{self.label}", parent=target)
|
221
221
|
layout.addWidget(label)
|
222
222
|
if self.device_combobox is not None:
|
223
223
|
layout.addWidget(self.device_combobox)
|
@@ -279,31 +279,63 @@ class SwitchableToolBarAction(ToolBarAction):
|
|
279
279
|
self.main_button.setToolTip(default_action.tooltip)
|
280
280
|
self.main_button.clicked.connect(self._trigger_current_action)
|
281
281
|
menu = QMenu(self.main_button)
|
282
|
-
self.menu_actions = {}
|
283
282
|
for key, action_obj in self.actions.items():
|
284
|
-
menu_action = QAction(
|
283
|
+
menu_action = QAction(
|
284
|
+
icon=action_obj.get_icon(), text=action_obj.tooltip, parent=self.main_button
|
285
|
+
)
|
285
286
|
menu_action.setIconVisibleInMenu(True)
|
286
287
|
menu_action.setCheckable(self.checkable)
|
287
288
|
menu_action.setChecked(key == self.current_key)
|
288
289
|
menu_action.triggered.connect(lambda checked, k=key: self.set_default_action(k))
|
289
290
|
menu.addAction(menu_action)
|
290
|
-
self.menu_actions[key] = menu_action
|
291
291
|
self.main_button.setMenu(menu)
|
292
292
|
toolbar.addWidget(self.main_button)
|
293
293
|
|
294
294
|
def _trigger_current_action(self):
|
295
|
+
"""
|
296
|
+
Triggers the current action associated with the main button.
|
297
|
+
"""
|
295
298
|
action_obj = self.actions[self.current_key]
|
296
299
|
action_obj.action.trigger()
|
297
300
|
|
298
301
|
def set_default_action(self, key: str):
|
302
|
+
"""
|
303
|
+
Sets the default action for the split action.
|
304
|
+
|
305
|
+
Args:
|
306
|
+
key(str): The key of the action to set as default.
|
307
|
+
"""
|
299
308
|
self.current_key = key
|
300
309
|
new_action = self.actions[self.current_key]
|
301
310
|
self.main_button.setIcon(new_action.get_icon())
|
302
311
|
self.main_button.setToolTip(new_action.tooltip)
|
303
312
|
# Update check state of menu items
|
304
|
-
for k, menu_act in self.
|
305
|
-
menu_act.setChecked(
|
313
|
+
for k, menu_act in self.actions.items():
|
314
|
+
menu_act.action.setChecked(False)
|
306
315
|
new_action.action.trigger()
|
316
|
+
# Active action chosen from menu is always checked, uncheck through main button
|
317
|
+
if self.checkable:
|
318
|
+
new_action.action.setChecked(True)
|
319
|
+
self.main_button.setChecked(True)
|
320
|
+
|
321
|
+
def block_all_signals(self, block: bool = True):
|
322
|
+
"""
|
323
|
+
Blocks or unblocks all signals for the actions in the toolbar.
|
324
|
+
|
325
|
+
Args:
|
326
|
+
block (bool): Whether to block signals. Defaults to True.
|
327
|
+
"""
|
328
|
+
self.main_button.blockSignals(block)
|
329
|
+
for action in self.actions.values():
|
330
|
+
action.action.blockSignals(block)
|
331
|
+
|
332
|
+
def set_state_all(self, state: bool):
|
333
|
+
"""
|
334
|
+
Uncheck all actions in the toolbar.
|
335
|
+
"""
|
336
|
+
for action in self.actions.values():
|
337
|
+
action.action.setChecked(state)
|
338
|
+
self.main_button.setChecked(state)
|
307
339
|
|
308
340
|
def get_icon(self) -> QIcon:
|
309
341
|
return self.actions[self.current_key].get_icon()
|
@@ -318,11 +350,18 @@ class WidgetAction(ToolBarAction):
|
|
318
350
|
widget (QWidget): The widget to be added to the toolbar.
|
319
351
|
"""
|
320
352
|
|
321
|
-
def __init__(
|
353
|
+
def __init__(
|
354
|
+
self,
|
355
|
+
label: str | None = None,
|
356
|
+
widget: QWidget = None,
|
357
|
+
adjust_size: bool = True,
|
358
|
+
parent=None,
|
359
|
+
):
|
322
360
|
super().__init__(icon_path=None, tooltip=label, checkable=False)
|
323
361
|
self.label = label
|
324
362
|
self.widget = widget
|
325
363
|
self.container = None
|
364
|
+
self.adjust_size = adjust_size
|
326
365
|
|
327
366
|
def add_to_toolbar(self, toolbar: QToolBar, target: QWidget):
|
328
367
|
"""
|
@@ -332,18 +371,18 @@ class WidgetAction(ToolBarAction):
|
|
332
371
|
toolbar (QToolBar): The toolbar to add the widget to.
|
333
372
|
target (QWidget): The target widget for the action.
|
334
373
|
"""
|
335
|
-
self.container = QWidget()
|
374
|
+
self.container = QWidget(parent=target)
|
336
375
|
layout = QHBoxLayout(self.container)
|
337
376
|
layout.setContentsMargins(0, 0, 0, 0)
|
338
377
|
layout.setSpacing(0)
|
339
378
|
|
340
379
|
if self.label is not None:
|
341
|
-
label_widget = QLabel(f"{self.label}")
|
380
|
+
label_widget = QLabel(text=f"{self.label}", parent=target)
|
342
381
|
label_widget.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed)
|
343
382
|
label_widget.setAlignment(Qt.AlignVCenter | Qt.AlignRight)
|
344
383
|
layout.addWidget(label_widget)
|
345
384
|
|
346
|
-
if isinstance(self.widget, QComboBox):
|
385
|
+
if isinstance(self.widget, QComboBox) and self.adjust_size:
|
347
386
|
self.widget.setSizeAdjustPolicy(QComboBox.AdjustToContents)
|
348
387
|
|
349
388
|
size_policy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
@@ -400,7 +439,7 @@ class ExpandableMenuAction(ToolBarAction):
|
|
400
439
|
)
|
401
440
|
menu = QMenu(button)
|
402
441
|
for action_id, action in self.actions.items():
|
403
|
-
sub_action = QAction(action.tooltip, target)
|
442
|
+
sub_action = QAction(text=action.tooltip, parent=target)
|
404
443
|
sub_action.setIconVisibleInMenu(True)
|
405
444
|
if action.icon_path:
|
406
445
|
icon = QIcon()
|
@@ -484,7 +523,7 @@ class ModularToolBar(QToolBar):
|
|
484
523
|
orientation: Literal["horizontal", "vertical"] = "horizontal",
|
485
524
|
background_color: str = "rgba(0, 0, 0, 0)",
|
486
525
|
):
|
487
|
-
super().__init__(parent)
|
526
|
+
super().__init__(parent=parent)
|
488
527
|
|
489
528
|
self.widgets = defaultdict(dict)
|
490
529
|
self.background_color = background_color
|
@@ -821,13 +860,13 @@ class MainWindow(QMainWindow): # pragma: no cover
|
|
821
860
|
|
822
861
|
# For theme testing
|
823
862
|
|
824
|
-
self.dark_button = DarkModeButton(toolbar=True)
|
863
|
+
self.dark_button = DarkModeButton(parent=self, toolbar=True)
|
825
864
|
dark_mode_action = WidgetAction(label=None, widget=self.dark_button)
|
826
865
|
self.toolbar.add_action("dark_mode", dark_mode_action, self)
|
827
866
|
|
828
867
|
def add_bundles(self):
|
829
868
|
home_action = MaterialIconAction(
|
830
|
-
icon_name="home", tooltip="Home", checkable=
|
869
|
+
icon_name="home", tooltip="Home", checkable=False, parent=self
|
831
870
|
)
|
832
871
|
settings_action = MaterialIconAction(
|
833
872
|
icon_name="settings", tooltip="Settings", checkable=True, parent=self
|
@@ -844,6 +883,7 @@ class MainWindow(QMainWindow): # pragma: no cover
|
|
844
883
|
],
|
845
884
|
)
|
846
885
|
self.toolbar.add_bundle(main_actions_bundle, target_widget=self)
|
886
|
+
home_action.action.triggered.connect(lambda: self.switchable_action.set_state_all(False))
|
847
887
|
|
848
888
|
search_action = MaterialIconAction(
|
849
889
|
icon_name="search", tooltip="Search", checkable=False, parent=self
|
@@ -897,20 +937,20 @@ class MainWindow(QMainWindow): # pragma: no cover
|
|
897
937
|
|
898
938
|
def add_switchable_button_checkable(self):
|
899
939
|
action1 = MaterialIconAction(
|
900
|
-
icon_name="
|
940
|
+
icon_name="hdr_auto", tooltip="Action 1", checkable=True, parent=self
|
901
941
|
)
|
902
942
|
action2 = MaterialIconAction(
|
903
|
-
icon_name="
|
943
|
+
icon_name="hdr_auto", tooltip="Action 2", checkable=True, filled=True, parent=self
|
904
944
|
)
|
905
945
|
|
906
|
-
switchable_action = SwitchableToolBarAction(
|
946
|
+
self.switchable_action = SwitchableToolBarAction(
|
907
947
|
actions={"action1": action1, "action2": action2},
|
908
948
|
initial_action="action1",
|
909
949
|
tooltip="Switchable Action",
|
910
950
|
checkable=True,
|
911
951
|
parent=self,
|
912
952
|
)
|
913
|
-
self.toolbar.add_action("switchable_action", switchable_action, self)
|
953
|
+
self.toolbar.add_action("switchable_action", self.switchable_action, self)
|
914
954
|
|
915
955
|
action1.action.toggled.connect(
|
916
956
|
lambda checked: self.test_label.setText(f"Action 1 triggered, checked = {checked}")
|
@@ -931,16 +971,20 @@ class MainWindow(QMainWindow): # pragma: no cover
|
|
931
971
|
actions={"action1": action1, "action2": action2},
|
932
972
|
initial_action="action1",
|
933
973
|
tooltip="Switchable Action",
|
934
|
-
checkable=
|
974
|
+
checkable=False,
|
935
975
|
parent=self,
|
936
976
|
)
|
937
977
|
self.toolbar.add_action("switchable_action_no_toggle", switchable_action, self)
|
938
978
|
|
939
979
|
action1.action.triggered.connect(
|
940
|
-
lambda checked: self.test_label.setText(
|
980
|
+
lambda checked: self.test_label.setText(
|
981
|
+
f"Action 1 (non-checkable) triggered, checked = {checked}"
|
982
|
+
)
|
941
983
|
)
|
942
984
|
action2.action.triggered.connect(
|
943
|
-
lambda checked: self.test_label.setText(
|
985
|
+
lambda checked: self.test_label.setText(
|
986
|
+
f"Action 2 (non-checkable) triggered, checked = {checked}"
|
987
|
+
)
|
944
988
|
)
|
945
989
|
switchable_action.actions["action1"].action.setChecked(True)
|
946
990
|
|