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.
Files changed (196) hide show
  1. .gitlab-ci.yml +3 -5
  2. CHANGELOG.md +631 -0
  3. PKG-INFO +3 -3
  4. bec_widgets/__init__.py +4 -0
  5. bec_widgets/applications/bw_launch.py +23 -0
  6. bec_widgets/applications/launch_window.py +430 -0
  7. bec_widgets/assets/app_icons/auto_update.png +0 -0
  8. bec_widgets/assets/app_icons/ui_loader_tile.png +0 -0
  9. bec_widgets/cli/__init__.py +0 -1
  10. bec_widgets/cli/client.py +1779 -2064
  11. bec_widgets/cli/client_utils.py +346 -174
  12. bec_widgets/cli/generate_cli.py +143 -37
  13. bec_widgets/cli/rpc/rpc_base.py +152 -21
  14. bec_widgets/cli/rpc/rpc_register.py +113 -6
  15. bec_widgets/cli/rpc/rpc_widget_handler.py +13 -11
  16. bec_widgets/cli/server.py +125 -239
  17. bec_widgets/examples/jupyter_console/jupyter_console_window.py +97 -145
  18. bec_widgets/examples/plugin_example_pyside/tictactoetaskmenu.py +1 -1
  19. bec_widgets/utils/bec_connector.py +190 -21
  20. bec_widgets/utils/bec_designer.py +7 -0
  21. bec_widgets/utils/bec_dispatcher.py +71 -4
  22. bec_widgets/utils/bec_plugin_helper.py +89 -0
  23. bec_widgets/utils/bec_signal_proxy.py +1 -1
  24. bec_widgets/utils/bec_widget.py +26 -10
  25. bec_widgets/utils/colors.py +1 -1
  26. bec_widgets/{qt_utils → utils}/compact_popup.py +2 -0
  27. bec_widgets/utils/container_utils.py +37 -12
  28. bec_widgets/utils/crosshair.py +25 -8
  29. bec_widgets/utils/entry_validator.py +3 -1
  30. bec_widgets/{qt_utils → utils}/error_popups.py +18 -0
  31. bec_widgets/{qt_utils → utils}/expandable_frame.py +2 -2
  32. bec_widgets/utils/forms_from_types/forms.py +182 -0
  33. bec_widgets/{widgets/editors/scan_metadata/_metadata_widgets.py → utils/forms_from_types/items.py} +41 -30
  34. bec_widgets/utils/generate_designer_plugin.py +40 -36
  35. bec_widgets/utils/linear_region_selector.py +2 -0
  36. bec_widgets/utils/name_utils.py +16 -0
  37. bec_widgets/{qt_utils → utils}/palette_viewer.py +2 -2
  38. bec_widgets/utils/plot_indicator_items.py +2 -5
  39. bec_widgets/utils/plugin_utils.py +47 -1
  40. bec_widgets/{qt_utils → utils}/round_frame.py +14 -14
  41. bec_widgets/utils/rpc_server.py +277 -0
  42. bec_widgets/utils/serialization.py +44 -0
  43. bec_widgets/{qt_utils → utils}/settings_dialog.py +26 -1
  44. bec_widgets/{qt_utils → utils}/side_panel.py +17 -10
  45. bec_widgets/{qt_utils → utils}/toolbar.py +69 -25
  46. bec_widgets/utils/ui_loader.py +8 -8
  47. bec_widgets/utils/widget_io.py +166 -25
  48. bec_widgets/widgets/containers/auto_update/auto_updates.py +364 -0
  49. bec_widgets/widgets/containers/dock/dock.py +157 -49
  50. bec_widgets/widgets/containers/dock/dock_area.py +186 -138
  51. bec_widgets/widgets/containers/layout_manager/layout_manager.py +2 -1
  52. bec_widgets/widgets/containers/main_window/addons/web_links.py +15 -0
  53. bec_widgets/widgets/containers/main_window/main_window.py +189 -41
  54. bec_widgets/widgets/control/buttons/button_abort/button_abort.py +3 -4
  55. bec_widgets/widgets/control/buttons/button_reset/button_reset.py +3 -4
  56. bec_widgets/widgets/control/buttons/button_resume/button_resume.py +3 -3
  57. bec_widgets/widgets/control/buttons/stop_button/stop_button.py +18 -7
  58. bec_widgets/widgets/control/device_control/position_indicator/position_indicator.py +22 -3
  59. bec_widgets/widgets/control/device_control/positioner_box/_base/positioner_box_base.py +31 -13
  60. bec_widgets/widgets/control/device_control/positioner_box/positioner_box/positioner_box.py +3 -1
  61. bec_widgets/widgets/control/device_control/positioner_box/positioner_box/positioner_box.ui +27 -4
  62. bec_widgets/widgets/control/device_control/positioner_box/positioner_box_2d/positioner_box_2d.py +5 -2
  63. bec_widgets/widgets/control/device_control/positioner_box/positioner_box_2d/positioner_box_2d.ui +97 -31
  64. bec_widgets/widgets/control/device_control/positioner_box/positioner_control_line/positioner_control_line.ui +11 -4
  65. bec_widgets/widgets/control/device_control/positioner_group/positioner_group.py +2 -3
  66. bec_widgets/widgets/control/device_input/base_classes/device_input_base.py +29 -4
  67. bec_widgets/widgets/control/device_input/base_classes/device_signal_input_base.py +1 -0
  68. bec_widgets/widgets/control/device_input/device_combobox/device_combobox.py +2 -2
  69. bec_widgets/widgets/control/device_input/device_line_edit/device_line_edit.py +2 -2
  70. bec_widgets/widgets/control/device_input/signal_combobox/signal_combobox.py +1 -2
  71. bec_widgets/widgets/control/device_input/signal_line_edit/signal_line_edit.py +1 -2
  72. bec_widgets/widgets/control/scan_control/scan_control.py +7 -5
  73. bec_widgets/widgets/control/scan_control/scan_group_box.py +28 -5
  74. bec_widgets/widgets/dap/dap_combo_box/dap_combo_box.py +1 -2
  75. bec_widgets/widgets/dap/lmfit_dialog/lmfit_dialog.py +3 -4
  76. bec_widgets/widgets/dap/lmfit_dialog/lmfit_dialog_vertical.ui +14 -8
  77. bec_widgets/widgets/editors/console/console.py +1 -1
  78. bec_widgets/widgets/editors/{scan_metadata/additional_metadata_table.py → dict_backed_table.py} +29 -6
  79. bec_widgets/widgets/editors/scan_metadata/__init__.py +0 -7
  80. bec_widgets/widgets/editors/scan_metadata/_util.py +1 -1
  81. bec_widgets/widgets/{plots/motor_map/register_bec_motor_map_widget.py → editors/scan_metadata/register_scan_metadata.py} +2 -4
  82. bec_widgets/widgets/editors/scan_metadata/scan_metadata.py +42 -136
  83. bec_widgets/widgets/editors/scan_metadata/scan_metadata.pyproject +1 -0
  84. bec_widgets/widgets/{plots/multi_waveform/bec_multi_waveform_widget_plugin.py → editors/scan_metadata/scan_metadata_plugin.py} +9 -9
  85. bec_widgets/widgets/editors/text_box/text_box.py +2 -3
  86. bec_widgets/widgets/editors/website/website.py +2 -2
  87. bec_widgets/widgets/games/minesweeper.py +3 -2
  88. bec_widgets/widgets/plots/image/image.py +960 -0
  89. bec_widgets/widgets/plots/image/image.pyproject +1 -0
  90. bec_widgets/widgets/plots/image/image_item.py +279 -0
  91. bec_widgets/widgets/plots/{motor_map/bec_motor_map_widget_plugin.py → image/image_plugin.py} +11 -13
  92. bec_widgets/widgets/{containers/figure/plots → plots}/image/image_processor.py +31 -64
  93. bec_widgets/widgets/plots/image/{register_bec_image_widget.py → register_image.py} +2 -2
  94. bec_widgets/widgets/plots/image/toolbar_bundles/image_selection.py +59 -0
  95. bec_widgets/widgets/plots/image/toolbar_bundles/processing.py +79 -0
  96. bec_widgets/widgets/plots/motor_map/motor_map.py +832 -0
  97. bec_widgets/widgets/plots/motor_map/motor_map.pyproject +1 -0
  98. bec_widgets/widgets/plots/motor_map/motor_map_plugin.py +54 -0
  99. bec_widgets/widgets/plots/{multi_waveform/register_bec_multi_waveform_widget.py → motor_map/register_motor_map.py} +2 -4
  100. bec_widgets/widgets/plots/motor_map/settings/motor_map_settings.py +129 -0
  101. bec_widgets/widgets/plots/motor_map/settings/motor_map_settings.ui +120 -0
  102. bec_widgets/widgets/plots/motor_map/toolbar_bundles/motor_selection.py +70 -0
  103. bec_widgets/widgets/plots/multi_waveform/multi_waveform.py +508 -0
  104. bec_widgets/widgets/plots/multi_waveform/multi_waveform.pyproject +1 -0
  105. bec_widgets/widgets/plots/multi_waveform/multi_waveform_plugin.py +54 -0
  106. bec_widgets/widgets/plots/multi_waveform/register_multi_waveform.py +15 -0
  107. bec_widgets/widgets/plots/multi_waveform/settings/control_panel.py +144 -0
  108. bec_widgets/widgets/plots/multi_waveform/settings/multi_waveform_controls.ui +164 -0
  109. bec_widgets/widgets/plots/multi_waveform/toolbar_bundles/monitor_selection.py +65 -0
  110. bec_widgets/widgets/{plots_next_gen → plots}/plot_base.py +321 -40
  111. bec_widgets/widgets/plots/{waveform/register_bec_waveform_widget.py → scatter_waveform/register_scatter_waveform.py} +3 -3
  112. bec_widgets/widgets/plots/scatter_waveform/scatter_curve.py +197 -0
  113. bec_widgets/widgets/plots/scatter_waveform/scatter_waveform.py +553 -0
  114. bec_widgets/widgets/plots/scatter_waveform/scatter_waveform.pyproject +1 -0
  115. bec_widgets/widgets/plots/{image/bec_image_widget_plugin.py → scatter_waveform/scatter_waveform_plugin.py} +9 -13
  116. bec_widgets/widgets/plots/scatter_waveform/settings/scatter_curve_setting.py +138 -0
  117. bec_widgets/widgets/plots/scatter_waveform/settings/scatter_curve_settings_horizontal.ui +195 -0
  118. bec_widgets/widgets/plots/scatter_waveform/settings/scatter_curve_settings_vertical.ui +204 -0
  119. bec_widgets/widgets/{plots_next_gen → plots}/setting_menus/axis_settings.py +8 -8
  120. bec_widgets/widgets/{plots_next_gen → plots}/toolbar_bundles/mouse_interactions.py +4 -18
  121. bec_widgets/widgets/{plots_next_gen → plots}/toolbar_bundles/plot_export.py +14 -3
  122. bec_widgets/widgets/{plots_next_gen → plots}/toolbar_bundles/roi_bundle.py +6 -1
  123. bec_widgets/widgets/{plots_next_gen → plots}/toolbar_bundles/save_state.py +2 -2
  124. bec_widgets/widgets/{containers/figure/plots/waveform/waveform_curve.py → plots/waveform/curve.py} +119 -49
  125. bec_widgets/widgets/plots/waveform/register_waveform.py +15 -0
  126. bec_widgets/widgets/plots/waveform/settings/curve_settings/curve_setting.py +125 -0
  127. bec_widgets/widgets/plots/waveform/settings/curve_settings/curve_tree.py +576 -0
  128. bec_widgets/widgets/plots/waveform/utils/__init__.py +0 -0
  129. bec_widgets/widgets/plots/waveform/utils/roi_manager.py +84 -0
  130. bec_widgets/widgets/plots/waveform/waveform.py +1794 -0
  131. bec_widgets/widgets/plots/waveform/waveform.pyproject +1 -0
  132. bec_widgets/widgets/plots/waveform/{bec_waveform_widget_plugin.py → waveform_plugin.py} +9 -13
  133. bec_widgets/widgets/progress/bec_progressbar/bec_progressbar.py +1 -2
  134. bec_widgets/widgets/progress/ring_progress_bar/ring.py +11 -10
  135. bec_widgets/widgets/progress/ring_progress_bar/ring_progress_bar.py +24 -14
  136. bec_widgets/widgets/services/bec_queue/bec_queue.py +13 -11
  137. bec_widgets/widgets/services/bec_status_box/bec_status_box.py +3 -4
  138. bec_widgets/widgets/services/device_browser/device_browser.py +5 -2
  139. bec_widgets/widgets/services/device_browser/device_item/device_item.py +1 -1
  140. bec_widgets/widgets/utility/logpanel/logpanel.py +36 -17
  141. bec_widgets/widgets/utility/spinbox/decimal_spinbox.py +3 -3
  142. bec_widgets/widgets/utility/visual/color_button/color_button.py +1 -1
  143. bec_widgets/widgets/utility/visual/colormap_widget/colormap_widget.py +4 -6
  144. bec_widgets/widgets/utility/visual/dark_mode_button/dark_mode_button.py +4 -8
  145. {bec_widgets-1.25.1.dist-info → bec_widgets-2.0.0.dist-info}/METADATA +3 -3
  146. {bec_widgets-1.25.1.dist-info → bec_widgets-2.0.0.dist-info}/RECORD +168 -153
  147. pyproject.toml +3 -3
  148. bec_widgets/applications/alignment/alignment_1d/alignment_1d.py +0 -198
  149. bec_widgets/applications/alignment/alignment_1d/alignment_1d.ui +0 -615
  150. bec_widgets/applications/bec_app.py +0 -84
  151. bec_widgets/cli/auto_updates.py +0 -168
  152. bec_widgets/widgets/containers/figure/__init__.py +0 -1
  153. bec_widgets/widgets/containers/figure/figure.py +0 -796
  154. bec_widgets/widgets/containers/figure/plots/axis_settings.py +0 -91
  155. bec_widgets/widgets/containers/figure/plots/axis_settings.ui +0 -256
  156. bec_widgets/widgets/containers/figure/plots/image/image.py +0 -772
  157. bec_widgets/widgets/containers/figure/plots/image/image_item.py +0 -337
  158. bec_widgets/widgets/containers/figure/plots/motor_map/motor_map.py +0 -525
  159. bec_widgets/widgets/containers/figure/plots/multi_waveform/multi_waveform.py +0 -340
  160. bec_widgets/widgets/containers/figure/plots/plot_base.py +0 -505
  161. bec_widgets/widgets/containers/figure/plots/waveform/waveform.py +0 -1563
  162. bec_widgets/widgets/plots/image/bec_image_widget.pyproject +0 -1
  163. bec_widgets/widgets/plots/image/image_widget.py +0 -515
  164. bec_widgets/widgets/plots/motor_map/bec_motor_map_widget.pyproject +0 -1
  165. bec_widgets/widgets/plots/motor_map/motor_map_dialog/motor_map_settings.py +0 -56
  166. bec_widgets/widgets/plots/motor_map/motor_map_dialog/motor_map_settings.ui +0 -108
  167. bec_widgets/widgets/plots/motor_map/motor_map_widget.py +0 -234
  168. bec_widgets/widgets/plots/multi_waveform/bec_multi_waveform_widget.pyproject +0 -1
  169. bec_widgets/widgets/plots/multi_waveform/multi_waveform_controls.ui +0 -99
  170. bec_widgets/widgets/plots/multi_waveform/multi_waveform_widget.py +0 -536
  171. bec_widgets/widgets/plots/waveform/bec_waveform_widget.pyproject +0 -1
  172. bec_widgets/widgets/plots/waveform/waveform_popups/curve_dialog/curve_dialog.py +0 -336
  173. bec_widgets/widgets/plots/waveform/waveform_popups/curve_dialog/curve_dialog.ui +0 -372
  174. bec_widgets/widgets/plots/waveform/waveform_popups/dap_summary_dialog/dap_summary_dialog.py +0 -25
  175. bec_widgets/widgets/plots/waveform/waveform_widget.py +0 -751
  176. /bec_widgets/{qt_utils → utils}/collapsible_panel_manager.py +0 -0
  177. /bec_widgets/{applications/alignment → utils/forms_from_types}/__init__.py +0 -0
  178. /bec_widgets/{qt_utils → utils}/redis_message_waiter.py +0 -0
  179. /bec_widgets/{applications/alignment/alignment_1d → widgets/containers/auto_update}/__init__.py +0 -0
  180. /bec_widgets/{qt_utils → widgets/containers/main_window/addons}/__init__.py +0 -0
  181. /bec_widgets/widgets/{containers/figure/plots → plots/image/toolbar_bundles}/__init__.py +0 -0
  182. /bec_widgets/widgets/{containers/figure/plots/image → plots/motor_map/settings}/__init__.py +0 -0
  183. /bec_widgets/widgets/{containers/figure/plots/motor_map → plots/motor_map/toolbar_bundles}/__init__.py +0 -0
  184. /bec_widgets/widgets/{containers/figure/plots/multi_waveform → plots/multi_waveform/settings}/__init__.py +0 -0
  185. /bec_widgets/widgets/{containers/figure/plots/waveform → plots/multi_waveform/toolbar_bundles}/__init__.py +0 -0
  186. /bec_widgets/widgets/plots/{motor_map/motor_map_dialog → scatter_waveform}/__init__.py +0 -0
  187. /bec_widgets/widgets/plots/{waveform/waveform_popups → scatter_waveform/settings}/__init__.py +0 -0
  188. /bec_widgets/widgets/plots/{waveform/waveform_popups/curve_dialog → setting_menus}/__init__.py +0 -0
  189. /bec_widgets/widgets/{plots_next_gen → plots}/setting_menus/axis_settings_horizontal.ui +0 -0
  190. /bec_widgets/widgets/{plots_next_gen → plots}/setting_menus/axis_settings_vertical.ui +0 -0
  191. /bec_widgets/widgets/plots/{waveform/waveform_popups/dap_summary_dialog → toolbar_bundles}/__init__.py +0 -0
  192. /bec_widgets/widgets/{plots_next_gen/setting_menus → plots/waveform/settings}/__init__.py +0 -0
  193. /bec_widgets/widgets/{plots_next_gen/toolbar_bundles → plots/waveform/settings/curve_settings}/__init__.py +0 -0
  194. {bec_widgets-1.25.1.dist-info → bec_widgets-2.0.0.dist-info}/WHEEL +0 -0
  195. {bec_widgets-1.25.1.dist-info → bec_widgets-2.0.0.dist-info}/entry_points.txt +0 -0
  196. {bec_widgets-1.25.1.dist-info → bec_widgets-2.0.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,576 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from typing import TYPE_CHECKING
5
+
6
+ from bec_lib.logger import bec_logger
7
+ from bec_qthemes._icon.material_icons import material_icon
8
+ from qtpy.QtGui import QColor
9
+ from qtpy.QtWidgets import (
10
+ QColorDialog,
11
+ QComboBox,
12
+ QHBoxLayout,
13
+ QLabel,
14
+ QLineEdit,
15
+ QPushButton,
16
+ QSizePolicy,
17
+ QSpinBox,
18
+ QToolButton,
19
+ QTreeWidget,
20
+ QTreeWidgetItem,
21
+ QVBoxLayout,
22
+ QWidget,
23
+ )
24
+
25
+ from bec_widgets.utils import ConnectionConfig, EntryValidator
26
+ from bec_widgets.utils.bec_widget import BECWidget
27
+ from bec_widgets.utils.colors import Colors
28
+ from bec_widgets.utils.toolbar import MaterialIconAction, ModularToolBar
29
+ from bec_widgets.widgets.control.device_input.device_line_edit.device_line_edit import (
30
+ DeviceLineEdit,
31
+ )
32
+ from bec_widgets.widgets.dap.dap_combo_box.dap_combo_box import DapComboBox
33
+ from bec_widgets.widgets.plots.waveform.curve import CurveConfig, DeviceSignal
34
+ from bec_widgets.widgets.utility.visual.colormap_widget.colormap_widget import BECColorMapWidget
35
+
36
+ if TYPE_CHECKING: # pragma: no cover
37
+ from bec_widgets.widgets.plots.waveform.waveform import Waveform
38
+
39
+
40
+ logger = bec_logger.logger
41
+
42
+
43
+ class ColorButton(QPushButton):
44
+ """A QPushButton subclass that displays a color.
45
+
46
+ The background is set to the given color and the button text is the hex code.
47
+ The text color is chosen automatically (black if the background is light, white if dark)
48
+ to guarantee good readability.
49
+ """
50
+
51
+ def __init__(self, color="#000000", parent=None):
52
+ """Initialize the color button.
53
+
54
+ Args:
55
+ color (str): The initial color in hex format (e.g., '#000000').
56
+ parent: Optional QWidget parent.
57
+ """
58
+ super().__init__(parent)
59
+ self.set_color(color)
60
+
61
+ def set_color(self, color):
62
+ """Set the button's color and update its appearance.
63
+
64
+ Args:
65
+ color (str or QColor): The new color to assign.
66
+ """
67
+ if isinstance(color, QColor):
68
+ self._color = color.name()
69
+ else:
70
+ self._color = color
71
+ self._update_appearance()
72
+
73
+ def color(self):
74
+ """Return the current color in hex."""
75
+ return self._color
76
+
77
+ def _update_appearance(self):
78
+ """Update the button style based on the background color's brightness."""
79
+ c = QColor(self._color)
80
+ brightness = c.lightnessF()
81
+ text_color = "#000000" if brightness > 0.5 else "#FFFFFF"
82
+ self.setStyleSheet(f"background-color: {self._color}; color: {text_color};")
83
+ self.setText(self._color)
84
+
85
+
86
+ class CurveRow(QTreeWidgetItem):
87
+ DELETE_BUTTON_COLOR = "#CC181E"
88
+ """A unified row that can represent either a device or a DAP curve.
89
+
90
+ Columns:
91
+ 0: Actions (delete or "Add DAP" if source=device)
92
+ 1..2: DeviceLineEdit and QLineEdit if source=device, or "Model" label and DapComboBox if source=dap
93
+ 3: ColorButton
94
+ 4: Style QComboBox
95
+ 5: Pen width QSpinBox
96
+ 6: Symbol size QSpinBox
97
+ """
98
+
99
+ def __init__(
100
+ self,
101
+ tree: QTreeWidget,
102
+ parent_item: QTreeWidgetItem | None = None,
103
+ config: CurveConfig | None = None,
104
+ device_manager=None,
105
+ ):
106
+ if parent_item:
107
+ super().__init__(parent_item)
108
+ else:
109
+ # A top-level device row.
110
+ super().__init__(tree)
111
+
112
+ self.tree = tree
113
+ self.parent_item = parent_item
114
+ self.curve_tree = tree.parent() # The CurveTree widget
115
+ self.curve_tree.all_items.append(self) # Track stable ordering
116
+
117
+ # BEC user input
118
+ self.device_edit = None
119
+ self.dap_combo = None
120
+
121
+ self.dev = device_manager
122
+ self.entry_validator = EntryValidator(self.dev)
123
+
124
+ self.config = config or CurveConfig()
125
+ self.source = self.config.source
126
+ self.dap_rows = []
127
+
128
+ # Create column 0 (Actions)
129
+ self._init_actions()
130
+ # Create columns 1..2, depending on source
131
+ self._init_source_ui()
132
+ # Create columns 3..6 (color, style, width, symbol)
133
+ self._init_style_controls()
134
+
135
+ def _init_actions(self):
136
+ """Create the actions widget in column 0, including a delete button and maybe 'Add DAP'."""
137
+ self.actions_widget = QWidget()
138
+ actions_layout = QHBoxLayout(self.actions_widget)
139
+ actions_layout.setContentsMargins(0, 0, 0, 0)
140
+ actions_layout.setSpacing(0)
141
+
142
+ # Delete button
143
+ self.delete_button = QToolButton()
144
+ delete_icon = material_icon(
145
+ "delete",
146
+ size=(20, 20),
147
+ convert_to_pixmap=False,
148
+ filled=False,
149
+ color=self.DELETE_BUTTON_COLOR,
150
+ )
151
+ self.delete_button.setIcon(delete_icon)
152
+ self.delete_button.clicked.connect(lambda: self.remove_self())
153
+ actions_layout.addWidget(self.delete_button)
154
+
155
+ # If device row, add "Add DAP" button
156
+ if self.source == "device":
157
+ self.add_dap_button = QPushButton("DAP")
158
+ self.add_dap_button.clicked.connect(lambda: self.add_dap_row())
159
+ actions_layout.addWidget(self.add_dap_button)
160
+
161
+ self.tree.setItemWidget(self, 0, self.actions_widget)
162
+
163
+ def _init_source_ui(self):
164
+ """Create columns 1 and 2. For device rows, we have device/entry edits; for dap rows, label/model combo."""
165
+ if self.source == "device":
166
+ # Device row: columns 1..2 are device line edits
167
+ self.device_edit = DeviceLineEdit(parent=self.tree)
168
+ self.entry_edit = QLineEdit(parent=self.tree) # TODO in future will be signal line edit
169
+ if self.config.signal:
170
+ self.device_edit.setText(self.config.signal.name or "")
171
+ self.entry_edit.setText(self.config.signal.entry or "")
172
+
173
+ self.tree.setItemWidget(self, 1, self.device_edit)
174
+ self.tree.setItemWidget(self, 2, self.entry_edit)
175
+
176
+ else:
177
+ # DAP row: column1= "Model" label, column2= DapComboBox
178
+ self.label_widget = QLabel("Model")
179
+ self.tree.setItemWidget(self, 1, self.label_widget)
180
+ self.dap_combo = DapComboBox(parent=self.tree)
181
+ self.dap_combo.populate_fit_model_combobox()
182
+ # If config.signal has a dap
183
+ if self.config.signal and self.config.signal.dap:
184
+ dap_value = self.config.signal.dap
185
+ idx = self.dap_combo.fit_model_combobox.findText(dap_value)
186
+ if idx >= 0:
187
+ self.dap_combo.fit_model_combobox.setCurrentIndex(idx)
188
+ else:
189
+ self.dap_combo.select_fit_model("GaussianModel") # default
190
+
191
+ self.tree.setItemWidget(self, 2, self.dap_combo)
192
+
193
+ def _init_style_controls(self):
194
+ """Create columns 3..6: color button, style combo, width spin, symbol spin."""
195
+ # Color in col 3
196
+ self.color_button = ColorButton(self.config.color)
197
+ self.color_button.clicked.connect(lambda: self._select_color(self.color_button))
198
+ self.tree.setItemWidget(self, 3, self.color_button)
199
+
200
+ # Style in col 4
201
+ self.style_combo = QComboBox()
202
+ self.style_combo.addItems(["solid", "dash", "dot", "dashdot"])
203
+ idx = self.style_combo.findText(self.config.pen_style)
204
+ if idx >= 0:
205
+ self.style_combo.setCurrentIndex(idx)
206
+ self.tree.setItemWidget(self, 4, self.style_combo)
207
+
208
+ # Pen width in col 5
209
+ self.width_spin = QSpinBox()
210
+ self.width_spin.setRange(1, 20)
211
+ self.width_spin.setValue(self.config.pen_width)
212
+ self.tree.setItemWidget(self, 5, self.width_spin)
213
+
214
+ # Symbol size in col 6
215
+ self.symbol_spin = QSpinBox()
216
+ self.symbol_spin.setRange(1, 20)
217
+ self.symbol_spin.setValue(self.config.symbol_size)
218
+ self.tree.setItemWidget(self, 6, self.symbol_spin)
219
+
220
+ def _select_color(self, button):
221
+ """
222
+ Selects a new color using a color dialog and applies it to the specified button. Updates
223
+ related configuration properties based on the chosen color.
224
+
225
+ Args:
226
+ button: The button widget whose color is being modified.
227
+ """
228
+ current_color = QColor(button.color())
229
+ chosen_color = QColorDialog.getColor(current_color, self.tree, "Select Curve Color")
230
+ if chosen_color.isValid():
231
+ button.set_color(chosen_color)
232
+ self.config.color = chosen_color.name()
233
+ self.config.symbol_color = chosen_color.name()
234
+
235
+ def add_dap_row(self):
236
+ """Create a new DAP row as a child. Only valid if source='device'."""
237
+ if self.source != "device":
238
+ return
239
+ curve_tree = self.tree.parent()
240
+ parent_label = self.config.label
241
+
242
+ # Inherit device name/entry
243
+ dev_name = ""
244
+ dev_entry = ""
245
+ if self.config.signal:
246
+ dev_name = self.config.signal.name
247
+ dev_entry = self.config.signal.entry
248
+
249
+ # Create a new config for the DAP row
250
+ dap_cfg = CurveConfig(
251
+ widget_class="Curve",
252
+ source="dap",
253
+ parent_label=parent_label,
254
+ signal=DeviceSignal(name=dev_name, entry=dev_entry),
255
+ )
256
+ new_dap = CurveRow(self.tree, parent_item=self, config=dap_cfg, device_manager=self.dev)
257
+ # Expand device row to show new child
258
+ self.tree.expandItem(self)
259
+
260
+ # Give the new row a color from the buffer:
261
+ curve_tree._ensure_color_buffer_size()
262
+ idx = len(curve_tree.all_items) - 1
263
+ new_col = curve_tree.color_buffer[idx]
264
+ new_dap.color_button.set_color(new_col)
265
+ new_dap.config.color = new_col
266
+ new_dap.config.symbol_color = new_col
267
+
268
+ def remove_self(self):
269
+ """Remove this row from the tree and from the parent's item list."""
270
+ # Recursively remove all child rows first
271
+ for i in reversed(range(self.childCount())):
272
+ child = self.child(i)
273
+ if isinstance(child, CurveRow):
274
+ child.remove_self()
275
+
276
+ # Clean up the widget references if they still exist
277
+ if getattr(self, "device_edit", None) is not None:
278
+ self.device_edit.close()
279
+ self.device_edit.deleteLater()
280
+ self.device_edit = None
281
+
282
+ if getattr(self, "dap_combo", None) is not None:
283
+ self.dap_combo.close()
284
+ self.dap_combo.deleteLater()
285
+ self.dap_combo = None
286
+
287
+ # Remove the item from the tree widget
288
+ index = self.tree.indexOfTopLevelItem(self)
289
+ if index != -1:
290
+ self.tree.takeTopLevelItem(index)
291
+ elif self.parent_item:
292
+ self.parent_item.removeChild(self)
293
+
294
+ # Finally, remove self from the registration list in the curve tree
295
+ curve_tree = self.tree.parent()
296
+ if self in curve_tree.all_items:
297
+ curve_tree.all_items.remove(self)
298
+
299
+ def export_data(self) -> dict:
300
+ """Collect data from the GUI widgets, update config, and return as a dict.
301
+
302
+ Returns:
303
+ dict: The serialized config based on the GUI state.
304
+ """
305
+ if self.source == "device":
306
+ # Gather device name/entry
307
+ device_name = ""
308
+ device_entry = ""
309
+ if hasattr(self, "device_edit"):
310
+ device_name = self.device_edit.text()
311
+ if hasattr(self, "entry_edit"):
312
+ device_entry = self.entry_validator.validate_signal(
313
+ name=device_name, entry=self.entry_edit.text()
314
+ )
315
+ self.entry_edit.setText(device_entry)
316
+ self.config.signal = DeviceSignal(name=device_name, entry=device_entry)
317
+ self.config.source = "device"
318
+ self.config.label = f"{device_name}-{device_entry}"
319
+ else:
320
+ # DAP logic
321
+ parent_conf_dict = {}
322
+ if self.parent_item:
323
+ parent_conf_dict = self.parent_item.export_data()
324
+ parent_conf = CurveConfig(**parent_conf_dict)
325
+ dev_name = ""
326
+ dev_entry = ""
327
+ if parent_conf.signal:
328
+ dev_name = parent_conf.signal.name
329
+ dev_entry = parent_conf.signal.entry
330
+ # Dap from the DapComboBox
331
+ new_dap = "GaussianModel"
332
+ if hasattr(self, "dap_combo"):
333
+ new_dap = self.dap_combo.fit_model_combobox.currentText()
334
+ self.config.signal = DeviceSignal(name=dev_name, entry=dev_entry, dap=new_dap)
335
+ self.config.source = "dap"
336
+ self.config.parent_label = parent_conf.label
337
+ self.config.label = f"{parent_conf.label}-{new_dap}"
338
+
339
+ # Common style fields
340
+ self.config.color = self.color_button.color()
341
+ self.config.symbol_color = self.color_button.color()
342
+ self.config.pen_style = self.style_combo.currentText()
343
+ self.config.pen_width = self.width_spin.value()
344
+ self.config.symbol_size = self.symbol_spin.value()
345
+
346
+ return self.config.model_dump()
347
+
348
+ def closeEvent(self, event) -> None:
349
+ logger.info(f"CurveRow closeEvent: {self.config.label}")
350
+ return super().closeEvent(event)
351
+
352
+
353
+ class CurveTree(BECWidget, QWidget):
354
+ """A tree widget that manages device and DAP curves."""
355
+
356
+ PLUGIN = False
357
+ RPC = False
358
+
359
+ def __init__(
360
+ self,
361
+ parent: QWidget | None = None,
362
+ config: ConnectionConfig | None = None,
363
+ client=None,
364
+ gui_id: str | None = None,
365
+ waveform: Waveform | None = None,
366
+ **kwargs,
367
+ ) -> None:
368
+ if config is None:
369
+ config = ConnectionConfig(widget_class=self.__class__.__name__)
370
+ super().__init__(parent=parent, client=client, gui_id=gui_id, config=config, **kwargs)
371
+
372
+ self.waveform = waveform
373
+ if self.waveform and hasattr(self.waveform, "color_palette"):
374
+ self.color_palette = self.waveform.color_palette
375
+ else:
376
+ self.color_palette = "plasma"
377
+
378
+ self.get_bec_shortcuts()
379
+
380
+ self.color_buffer = []
381
+ self.all_items = []
382
+ self.layout = QVBoxLayout(self)
383
+ self._init_toolbar()
384
+ self._init_tree()
385
+ self.refresh_from_waveform()
386
+
387
+ def _init_toolbar(self):
388
+ """Initialize the toolbar with actions: add, send, refresh, expand, collapse, renormalize."""
389
+ self.toolbar = ModularToolBar(parent=self, target_widget=self, orientation="horizontal")
390
+ add = MaterialIconAction(
391
+ icon_name="add", tooltip="Add new curve", checkable=False, parent=self
392
+ )
393
+ expand = MaterialIconAction(
394
+ icon_name="unfold_more", tooltip="Expand All DAP", checkable=False, parent=self
395
+ )
396
+ collapse = MaterialIconAction(
397
+ icon_name="unfold_less", tooltip="Collapse All DAP", checkable=False, parent=self
398
+ )
399
+
400
+ self.toolbar.add_action("add", add, self)
401
+ self.toolbar.add_action("expand_all", expand, self)
402
+ self.toolbar.add_action("collapse_all", collapse, self)
403
+
404
+ # Add colormap widget (not updating waveform's color_palette until Send is pressed)
405
+ self.spacer = QWidget()
406
+ self.spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
407
+ self.toolbar.addWidget(self.spacer)
408
+
409
+ # Renormalize colors button
410
+ renorm_action = MaterialIconAction(
411
+ icon_name="palette", tooltip="Normalize All Colors", checkable=False, parent=self
412
+ )
413
+ self.toolbar.add_action("renormalize_colors", renorm_action, self)
414
+ renorm_action.action.triggered.connect(lambda checked: self.renormalize_colors())
415
+
416
+ self.colormap_widget = BECColorMapWidget(cmap=self.color_palette or "plasma")
417
+ self.toolbar.addWidget(self.colormap_widget)
418
+ self.colormap_widget.colormap_changed_signal.connect(self.handle_colormap_changed)
419
+
420
+ add.action.triggered.connect(lambda checked: self.add_new_curve())
421
+ expand.action.triggered.connect(lambda checked: self.expand_all_daps())
422
+ collapse.action.triggered.connect(lambda checked: self.collapse_all_daps())
423
+
424
+ self.layout.addWidget(self.toolbar)
425
+
426
+ def _init_tree(self):
427
+ """Initialize the QTreeWidget with 7 columns and compact widths."""
428
+ self.tree = QTreeWidget()
429
+ self.tree.setColumnCount(7)
430
+ self.tree.setHeaderLabels(["Actions", "Name", "Entry", "Color", "Style", "Width", "Symbol"])
431
+ self.tree.setColumnWidth(0, 90)
432
+ self.tree.setColumnWidth(1, 100)
433
+ self.tree.setColumnWidth(2, 100)
434
+ self.tree.setColumnWidth(3, 70)
435
+ self.tree.setColumnWidth(4, 80)
436
+ self.tree.setColumnWidth(5, 40)
437
+ self.tree.setColumnWidth(6, 40)
438
+ self.layout.addWidget(self.tree)
439
+
440
+ def _init_color_buffer(self, size: int):
441
+ """
442
+ Initializes the color buffer with a calculated set of colors based on the golden
443
+ angle sequence.
444
+
445
+ Args:
446
+ size (int): The number of colors to be generated for the color buffer.
447
+ """
448
+ self.color_buffer = Colors.golden_angle_color(
449
+ colormap=self.colormap_widget.colormap, num=size, format="HEX"
450
+ )
451
+
452
+ def _ensure_color_buffer_size(self):
453
+ """
454
+ Ensures that the color buffer size meets the required number of items.
455
+ """
456
+ current_count = len(self.color_buffer)
457
+ color_list = Colors.golden_angle_color(
458
+ colormap=self.color_palette, num=max(10, current_count + 1), format="HEX"
459
+ )
460
+ self.color_buffer = color_list
461
+
462
+ def handle_colormap_changed(self, new_cmap: str):
463
+ """
464
+ Handles the updating of the color palette when the colormap is changed.
465
+
466
+ Args:
467
+ new_cmap: The new colormap to be set as the color palette.
468
+ """
469
+ self.color_palette = new_cmap
470
+
471
+ def renormalize_colors(self):
472
+ """Overwrite all existing rows with new colors from the buffer in their creation order."""
473
+ total = len(self.all_items)
474
+ self._ensure_color_buffer_size()
475
+ for idx, item in enumerate(self.all_items):
476
+ if hasattr(item, "color_button"):
477
+ new_col = self.color_buffer[idx]
478
+ item.color_button.set_color(new_col)
479
+ if hasattr(item, "config"):
480
+ item.config.color = new_col
481
+ item.config.symbol_color = new_col
482
+
483
+ def add_new_curve(self, name: str = None, entry: str = None):
484
+ """Add a new device-type CurveRow with an assigned colormap color.
485
+
486
+ Args:
487
+ name (str, optional): Device name.
488
+ entry (str, optional): Device entry.
489
+ style (str, optional): Pen style. Defaults to "solid".
490
+ width (int, optional): Pen width. Defaults to 4.
491
+ symbol_size (int, optional): Symbol size. Defaults to 7.
492
+
493
+ Returns:
494
+ CurveRow: The newly created top-level row.
495
+ """
496
+ cfg = CurveConfig(
497
+ widget_class="Curve",
498
+ parent_id=self.waveform.gui_id,
499
+ source="device",
500
+ signal=DeviceSignal(name=name or "", entry=entry or ""),
501
+ )
502
+ new_row = CurveRow(self.tree, parent_item=None, config=cfg, device_manager=self.dev)
503
+
504
+ # Assign color from the buffer ONLY to this new curve.
505
+ total_items = len(self.all_items)
506
+ self._ensure_color_buffer_size()
507
+ color_idx = total_items - 1 # new row is last
508
+ new_col = self.color_buffer[color_idx]
509
+ new_row.color_button.set_color(new_col)
510
+ new_row.config.color = new_col
511
+ new_row.config.symbol_color = new_col
512
+
513
+ return new_row
514
+
515
+ def send_curve_json(self):
516
+ """Send the current tree's config as JSON to the waveform, updating wavefrom.color_palette as well."""
517
+ if self.waveform is not None:
518
+ self.waveform.color_palette = self.color_palette
519
+ data = self.export_all_curves()
520
+ json_data = json.dumps(data, indent=2)
521
+ if self.waveform is not None:
522
+ self.waveform.curve_json = json_data
523
+
524
+ def export_all_curves(self) -> list:
525
+ """Recursively export data from each row.
526
+
527
+ Returns:
528
+ list: A list of exported config dicts for every row (device and DAP).
529
+ """
530
+ curves = []
531
+ for i in range(self.tree.topLevelItemCount()):
532
+ item = self.tree.topLevelItem(i)
533
+ if isinstance(item, CurveRow):
534
+ curves.append(item.export_data())
535
+ for j in range(item.childCount()):
536
+ child = item.child(j)
537
+ if isinstance(child, CurveRow):
538
+ curves.append(child.export_data())
539
+ return curves
540
+
541
+ def expand_all_daps(self):
542
+ """Expand all top-level rows to reveal child DAP rows."""
543
+ for i in range(self.tree.topLevelItemCount()):
544
+ item = self.tree.topLevelItem(i)
545
+ self.tree.expandItem(item)
546
+
547
+ def collapse_all_daps(self):
548
+ """Collapse all top-level rows, hiding child DAP rows."""
549
+ for i in range(self.tree.topLevelItemCount()):
550
+ item = self.tree.topLevelItem(i)
551
+ self.tree.collapseItem(item)
552
+
553
+ def refresh_from_waveform(self):
554
+ """Clear the tree and rebuild from the waveform's existing curves if any, else add sample rows."""
555
+ if self.waveform is None:
556
+ return
557
+ self.tree.clear()
558
+ self.all_items = []
559
+
560
+ device_curves = [c for c in self.waveform.curves if c.config.source == "device"]
561
+ dap_curves = [c for c in self.waveform.curves if c.config.source == "dap"]
562
+ for dev in device_curves:
563
+ dr = CurveRow(self.tree, parent_item=None, config=dev.config, device_manager=self.dev)
564
+ for dap in dap_curves:
565
+ if dap.config.parent_label == dev.config.label:
566
+ CurveRow(self.tree, parent_item=dr, config=dap.config, device_manager=self.dev)
567
+
568
+ def cleanup(self):
569
+ """Cleanup the widget."""
570
+ all_items = list(self.all_items)
571
+ for item in all_items:
572
+ item.remove_self()
573
+
574
+ def closeEvent(self, event):
575
+ self.cleanup()
576
+ return super().closeEvent(event)
File without changes
@@ -0,0 +1,84 @@
1
+ import pyqtgraph as pg
2
+ from qtpy.QtCore import QObject, Signal, Slot
3
+
4
+ from bec_widgets.utils.colors import get_accent_colors
5
+ from bec_widgets.utils.linear_region_selector import LinearRegionWrapper
6
+
7
+
8
+ class WaveformROIManager(QObject):
9
+ """
10
+ A reusable helper class that manages a single linear ROI region on a given plot item.
11
+ It provides signals to notify about region changes and active state.
12
+ """
13
+
14
+ roi_changed = Signal(tuple) # Emitted when the ROI (left, right) changes
15
+ roi_active = Signal(bool) # Emitted when ROI is enabled or disabled
16
+
17
+ def __init__(self, plot_item: pg.PlotItem, parent=None):
18
+ super().__init__(parent)
19
+ self._plot_item = plot_item
20
+ self._roi_wrapper: LinearRegionWrapper | None = None
21
+ self._roi_region: tuple[float, float] | None = None
22
+ self._accent_colors = get_accent_colors()
23
+
24
+ @property
25
+ def roi_region(self) -> tuple[float, float] | None:
26
+ return self._roi_region
27
+
28
+ @roi_region.setter
29
+ def roi_region(self, value: tuple[float, float] | None):
30
+ self._roi_region = value
31
+ if self._roi_wrapper is not None and value is not None:
32
+ self._roi_wrapper.linear_region_selector.setRegion(value)
33
+
34
+ @Slot(bool)
35
+ def toggle_roi(self, enabled: bool) -> None:
36
+ if enabled:
37
+ self._enable_roi()
38
+ else:
39
+ self._disable_roi()
40
+
41
+ @Slot(tuple)
42
+ def select_roi(self, region: tuple[float, float]):
43
+ # If ROI not present, enabling it
44
+ if self._roi_wrapper is None:
45
+ self.toggle_roi(True)
46
+ self.roi_region = region
47
+
48
+ def _enable_roi(self):
49
+ if self._roi_wrapper is not None:
50
+ # Already enabled
51
+ return
52
+ color = self._accent_colors.default
53
+ color.setAlpha(int(0.2 * 255))
54
+ hover_color = self._accent_colors.default
55
+ hover_color.setAlpha(int(0.35 * 255))
56
+
57
+ self._roi_wrapper = LinearRegionWrapper(
58
+ self._plot_item, color=color, hover_color=hover_color, parent=self
59
+ )
60
+ self._roi_wrapper.add_region_selector()
61
+ self._roi_wrapper.region_changed.connect(self._on_region_changed)
62
+
63
+ # If we already had a region, apply it
64
+ if self._roi_region is not None:
65
+ self._roi_wrapper.linear_region_selector.setRegion(self._roi_region)
66
+ else:
67
+ self._roi_region = self._roi_wrapper.linear_region_selector.getRegion()
68
+
69
+ self.roi_active.emit(True)
70
+
71
+ def _disable_roi(self):
72
+ if self._roi_wrapper is not None:
73
+ self._roi_wrapper.region_changed.disconnect(self._on_region_changed)
74
+ self._roi_wrapper.cleanup()
75
+ self._roi_wrapper.deleteLater()
76
+ self._roi_wrapper = None
77
+
78
+ self._roi_region = None
79
+ self.roi_active.emit(False)
80
+
81
+ @Slot(tuple)
82
+ def _on_region_changed(self, region: tuple[float, float]):
83
+ self._roi_region = region
84
+ self.roi_changed.emit(region)