bec-widgets 2.5.3__py3-none-any.whl → 2.6.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 (26) hide show
  1. .github/ISSUE_TEMPLATE/bug_report.yml +41 -0
  2. .gitlab/issue_templates/documentation_update_template.md → .github/ISSUE_TEMPLATE/documentation_update.md +10 -0
  3. .github/ISSUE_TEMPLATE/feature_request.md +3 -2
  4. .gitlab/merge_request_templates/default.md → .github/pull_request_template.md +8 -3
  5. .github/scripts/pr_issue_sync/pr_issue_sync.py +342 -0
  6. .github/scripts/pr_issue_sync/requirements.txt +2 -0
  7. .github/workflows/sync-issues-pr.yml +40 -0
  8. CHANGELOG.md +39 -0
  9. PKG-INFO +1 -1
  10. bec_widgets/cli/client.py +31 -1
  11. bec_widgets/examples/jupyter_console/jupyter_console_window.py +1 -1
  12. bec_widgets/widgets/containers/dock/dock_area.py +9 -4
  13. bec_widgets/widgets/plots/image/image.py +72 -8
  14. bec_widgets/widgets/plots/image/setting_widgets/__init__.py +0 -0
  15. bec_widgets/widgets/plots/image/setting_widgets/image_roi_tree.py +375 -0
  16. bec_widgets/widgets/plots/roi/image_roi.py +36 -14
  17. bec_widgets/widgets/plots/waveform/waveform.py +2 -0
  18. {bec_widgets-2.5.3.dist-info → bec_widgets-2.6.0.dist-info}/METADATA +1 -1
  19. {bec_widgets-2.5.3.dist-info → bec_widgets-2.6.0.dist-info}/RECORD +23 -20
  20. pyproject.toml +1 -1
  21. .github/ISSUE_TEMPLATE/bug_report.md +0 -26
  22. .gitlab/issue_templates/bug_report_template.md +0 -17
  23. .gitlab/issue_templates/feature_request_template.md +0 -40
  24. {bec_widgets-2.5.3.dist-info → bec_widgets-2.6.0.dist-info}/WHEEL +0 -0
  25. {bec_widgets-2.5.3.dist-info → bec_widgets-2.6.0.dist-info}/entry_points.txt +0 -0
  26. {bec_widgets-2.5.3.dist-info → bec_widgets-2.6.0.dist-info}/licenses/LICENSE +0 -0
@@ -8,13 +8,14 @@ from bec_lib import bec_logger
8
8
  from bec_lib.endpoints import MessageEndpoints
9
9
  from pydantic import Field, ValidationError, field_validator
10
10
  from qtpy.QtCore import QPointF, Signal
11
- from qtpy.QtWidgets import QWidget
11
+ from qtpy.QtWidgets import QDialog, QVBoxLayout, QWidget
12
12
 
13
13
  from bec_widgets.utils import ConnectionConfig
14
14
  from bec_widgets.utils.colors import Colors
15
15
  from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
16
16
  from bec_widgets.utils.toolbar import MaterialIconAction, SwitchableToolBarAction
17
17
  from bec_widgets.widgets.plots.image.image_item import ImageItem
18
+ from bec_widgets.widgets.plots.image.setting_widgets.image_roi_tree import ROIPropertyTree
18
19
  from bec_widgets.widgets.plots.image.toolbar_bundles.image_selection import (
19
20
  MonitorSelectionToolbarBundle,
20
21
  )
@@ -149,8 +150,7 @@ class Image(PlotBase):
149
150
  # Default Color map to plasma
150
151
  self.color_map = "plasma"
151
152
 
152
- # Headless controller keeps the canonical list.
153
- self._roi_manager_dialog = None
153
+ self.roi_manager_dialog = None
154
154
 
155
155
  ################################################################################
156
156
  # Widget Specific GUI interactions
@@ -266,6 +266,55 @@ class Image(PlotBase):
266
266
  lambda checked: self.enable_colorbar(checked, style="full")
267
267
  )
268
268
 
269
+ ########################################
270
+ # ROI Gui Manager
271
+ def add_side_menus(self):
272
+ super().add_side_menus()
273
+
274
+ roi_mgr = ROIPropertyTree(parent=self, image_widget=self)
275
+ self.side_panel.add_menu(
276
+ action_id="roi_mgr",
277
+ icon_name="view_list",
278
+ tooltip="ROI Manager",
279
+ widget=roi_mgr,
280
+ title="ROI Manager",
281
+ )
282
+
283
+ def add_popups(self):
284
+ super().add_popups() # keep Axis Settings
285
+
286
+ roi_action = MaterialIconAction(
287
+ icon_name="view_list", tooltip="ROI Manager", checkable=True, parent=self
288
+ )
289
+ # self.popup_bundle.add_action("roi_mgr", roi_action)
290
+ self.toolbar.add_action_to_bundle(
291
+ bundle_id="popup_bundle", action_id="roi_mgr", action=roi_action, target_widget=self
292
+ )
293
+ self.toolbar.widgets["roi_mgr"].action.triggered.connect(self.show_roi_manager_popup)
294
+
295
+ def show_roi_manager_popup(self):
296
+ roi_action = self.toolbar.widgets["roi_mgr"].action
297
+ if self.roi_manager_dialog is None or not self.roi_manager_dialog.isVisible():
298
+ self.roi_mgr = ROIPropertyTree(parent=self, image_widget=self)
299
+ self.roi_manager_dialog = QDialog(modal=False)
300
+ self.roi_manager_dialog.layout = QVBoxLayout(self.roi_manager_dialog)
301
+ self.roi_manager_dialog.layout.addWidget(self.roi_mgr)
302
+ self.roi_manager_dialog.finished.connect(self._roi_mgr_closed)
303
+ self.roi_manager_dialog.show()
304
+ roi_action.setChecked(True)
305
+ else:
306
+ self.roi_manager_dialog.raise_()
307
+ self.roi_manager_dialog.activateWindow()
308
+ roi_action.setChecked(True)
309
+
310
+ def _roi_mgr_closed(self):
311
+ self.roi_mgr.close()
312
+ self.roi_mgr.deleteLater()
313
+ self.roi_manager_dialog.close()
314
+ self.roi_manager_dialog.deleteLater()
315
+ self.roi_manager_dialog = None
316
+ self.toolbar.widgets["roi_mgr"].action.setChecked(False)
317
+
269
318
  def enable_colorbar(
270
319
  self,
271
320
  enabled: bool,
@@ -324,7 +373,7 @@ class Image(PlotBase):
324
373
  self,
325
374
  kind: Literal["rect", "circle"] = "rect",
326
375
  name: str | None = None,
327
- line_width: int | None = 10,
376
+ line_width: int | None = 5,
328
377
  pos: tuple[float, float] | None = (10, 10),
329
378
  size: tuple[float, float] | None = (50, 50),
330
379
  **pg_kwargs,
@@ -369,6 +418,7 @@ class Image(PlotBase):
369
418
  # Add to plot and controller (controller assigns color)
370
419
  self.plot_item.addItem(roi)
371
420
  self.roi_controller.add_roi(roi)
421
+ roi.add_scale_handle()
372
422
  return roi
373
423
 
374
424
  def remove_roi(self, roi: int | str):
@@ -1031,6 +1081,11 @@ class Image(PlotBase):
1031
1081
  self._color_bar.deleteLater()
1032
1082
  self._color_bar = None
1033
1083
 
1084
+ # Popup cleanup
1085
+ if self.roi_manager_dialog is not None:
1086
+ self.roi_manager_dialog.reject()
1087
+ self.roi_manager_dialog = None
1088
+
1034
1089
  # Toolbar cleanup
1035
1090
  self.toolbar.widgets["monitor"].widget.close()
1036
1091
  self.toolbar.widgets["monitor"].widget.deleteLater()
@@ -1041,10 +1096,19 @@ class Image(PlotBase):
1041
1096
  if __name__ == "__main__": # pragma: no cover
1042
1097
  import sys
1043
1098
 
1044
- from qtpy.QtWidgets import QApplication
1099
+ from qtpy.QtWidgets import QApplication, QHBoxLayout
1045
1100
 
1046
1101
  app = QApplication(sys.argv)
1047
- widget = Image(popups=True)
1048
- widget.show()
1049
- widget.resize(1000, 800)
1102
+ win = QWidget()
1103
+ win.setWindowTitle("Image Demo")
1104
+ ml = QHBoxLayout(win)
1105
+
1106
+ image_popup = Image(popups=True)
1107
+ image_side_panel = Image(popups=False)
1108
+
1109
+ ml.addWidget(image_popup)
1110
+ ml.addWidget(image_side_panel)
1111
+
1112
+ win.resize(1500, 800)
1113
+ win.show()
1050
1114
  sys.exit(app.exec_())
@@ -0,0 +1,375 @@
1
+ from __future__ import annotations
2
+
3
+ import math
4
+ from typing import TYPE_CHECKING
5
+
6
+ from bec_qthemes import material_icon
7
+ from qtpy.QtCore import QEvent, Qt
8
+ from qtpy.QtGui import QColor
9
+ from qtpy.QtWidgets import (
10
+ QColorDialog,
11
+ QHeaderView,
12
+ QSpinBox,
13
+ QToolButton,
14
+ QTreeWidget,
15
+ QTreeWidgetItem,
16
+ QVBoxLayout,
17
+ QWidget,
18
+ )
19
+
20
+ from bec_widgets import BECWidget
21
+ from bec_widgets.utils import BECDispatcher, ConnectionConfig
22
+ from bec_widgets.utils.toolbar import MaterialIconAction, ModularToolBar
23
+ from bec_widgets.widgets.plots.roi.image_roi import (
24
+ BaseROI,
25
+ CircularROI,
26
+ RectangularROI,
27
+ ROIController,
28
+ )
29
+ from bec_widgets.widgets.utility.visual.color_button_native.color_button_native import (
30
+ ColorButtonNative,
31
+ )
32
+ from bec_widgets.widgets.utility.visual.colormap_widget.colormap_widget import BECColorMapWidget
33
+
34
+ if TYPE_CHECKING:
35
+ from bec_widgets.widgets.plots.image.image import Image
36
+
37
+
38
+ class ROIPropertyTree(BECWidget, QWidget):
39
+ """
40
+ Two-column tree: [ROI] [Properties]
41
+
42
+ - Top-level: ROI name (editable) + color button.
43
+ - Children: type, line-width (spin box), coordinates (auto-updating).
44
+
45
+ Args:
46
+ image_widget (Image): The main Image widget that displays the ImageItem.
47
+ Provides ``plot_item`` and owns an ROIController already.
48
+ controller (ROIController, optional): Optionally pass an external controller.
49
+ If None, the manager uses ``image_widget.roi_controller``.
50
+ parent (QWidget, optional): Parent widget. Defaults to None.
51
+ """
52
+
53
+ PLUGIN = False
54
+ RPC = False
55
+
56
+ COL_ACTION, COL_ROI, COL_PROPS = range(3)
57
+ DELETE_BUTTON_COLOR = "#CC181E"
58
+
59
+ def __init__(
60
+ self,
61
+ *,
62
+ parent: QWidget = None,
63
+ image_widget: Image,
64
+ controller: ROIController | None = None,
65
+ ):
66
+
67
+ super().__init__(
68
+ parent=parent, config=ConnectionConfig(widget_class=self.__class__.__name__)
69
+ )
70
+
71
+ if controller is None:
72
+ # Use the controller already belonging to the Image widget
73
+ controller = getattr(image_widget, "roi_controller", None)
74
+ if controller is None:
75
+ controller = ROIController()
76
+ image_widget.roi_controller = controller
77
+
78
+ self.image_widget = image_widget
79
+ self.plot = image_widget.plot_item
80
+ self.controller = controller
81
+ self.roi_items: dict[BaseROI, QTreeWidgetItem] = {}
82
+
83
+ self.layout = QVBoxLayout(self)
84
+ self._init_toolbar()
85
+ self._init_tree()
86
+
87
+ # connect controller
88
+ self.controller.roiAdded.connect(self._on_roi_added)
89
+ self.controller.roiRemoved.connect(self._on_roi_removed)
90
+ self.controller.cleared.connect(self.tree.clear)
91
+
92
+ # initial load
93
+ for r in self.controller.rois:
94
+ self._on_roi_added(r)
95
+
96
+ self.tree.collapseAll()
97
+
98
+ # --------------------------------------------------------------------- UI
99
+ def _init_toolbar(self):
100
+ tb = ModularToolBar(self, self, orientation="horizontal")
101
+ # --- ROI draw actions (toggleable) ---
102
+ self.add_rect_action = MaterialIconAction("add_box", "Add Rect ROI", True, self)
103
+ self.add_circle_action = MaterialIconAction("add_circle", "Add Circle ROI", True, self)
104
+ tb.add_action("Add Rect ROI", self.add_rect_action, self)
105
+ tb.add_action("Add Circle ROI", self.add_circle_action, self)
106
+
107
+ # Expand/Collapse toggle
108
+ self.expand_toggle = MaterialIconAction(
109
+ "unfold_more", "Expand/Collapse", checkable=True, parent=self # icon when collapsed
110
+ )
111
+ tb.add_action("Expand/Collapse", self.expand_toggle, self)
112
+
113
+ def _exp_toggled(on: bool):
114
+ if on:
115
+ # switched to expanded state
116
+ self.tree.expandAll()
117
+ new_icon = material_icon("unfold_less", size=(20, 20), convert_to_pixmap=False)
118
+ else:
119
+ # collapsed state
120
+ self.tree.collapseAll()
121
+ new_icon = material_icon("unfold_more", size=(20, 20), convert_to_pixmap=False)
122
+ self.expand_toggle.action.setIcon(new_icon)
123
+
124
+ self.expand_toggle.action.toggled.connect(_exp_toggled)
125
+
126
+ self.expand_toggle.action.setChecked(False)
127
+ # colormap widget
128
+ self.cmap = BECColorMapWidget(cmap=self.controller.colormap)
129
+ tb.addWidget(QWidget()) # spacer
130
+ tb.addWidget(self.cmap)
131
+ self.cmap.colormap_changed_signal.connect(self.controller.set_colormap)
132
+ self.layout.addWidget(tb)
133
+ self.controller.paletteChanged.connect(lambda cmap: setattr(self.cmap, "colormap", cmap))
134
+
135
+ # ROI drawing state
136
+ self._roi_draw_mode = None # 'rect' | 'circle' | None
137
+ self._roi_start_pos = None # QPointF in image coords
138
+ self._temp_roi = None # live ROI being resized while dragging
139
+
140
+ # toggle handlers
141
+ self.add_rect_action.action.toggled.connect(
142
+ lambda on: self._set_roi_draw_mode("rect" if on else None)
143
+ )
144
+ self.add_circle_action.action.toggled.connect(
145
+ lambda on: self._set_roi_draw_mode("circle" if on else None)
146
+ )
147
+ # capture mouse events on the plot scene
148
+ self.plot.scene().installEventFilter(self)
149
+
150
+ def _init_tree(self):
151
+ self.tree = QTreeWidget()
152
+ self.tree.setColumnCount(3)
153
+ self.tree.setHeaderLabels(["Actions", "ROI", "Properties"])
154
+ self.tree.header().setSectionResizeMode(self.COL_ACTION, QHeaderView.ResizeToContents)
155
+ self.tree.headerItem().setText(self.COL_ACTION, "Actions") # blank header text
156
+ self.tree.itemChanged.connect(self._on_item_edited)
157
+ self.layout.addWidget(self.tree)
158
+
159
+ ################################################################################
160
+ # Helper functions
161
+ ################################################################################
162
+
163
+ # --------------------------------------------------------------------- formatting
164
+ @staticmethod
165
+ def _format_coord_text(value) -> str:
166
+ """
167
+ Consistently format a coordinate value for display.
168
+ """
169
+ if isinstance(value, (tuple, list)):
170
+ return "(" + ", ".join(f"{v:.2f}" for v in value) + ")"
171
+ if isinstance(value, (int, float)):
172
+ return f"{value:.2f}"
173
+ return str(value)
174
+
175
+ def _set_roi_draw_mode(self, mode: str | None):
176
+ # Ensure only the selected action is toggled on
177
+ if mode == "rect":
178
+ self.add_rect_action.action.setChecked(True)
179
+ self.add_circle_action.action.setChecked(False)
180
+ elif mode == "circle":
181
+ self.add_rect_action.action.setChecked(False)
182
+ self.add_circle_action.action.setChecked(True)
183
+ else:
184
+ self.add_rect_action.action.setChecked(False)
185
+ self.add_circle_action.action.setChecked(False)
186
+ self._roi_draw_mode = mode
187
+ self._roi_start_pos = None
188
+ # remove any unfinished temp ROI
189
+ if self._temp_roi is not None:
190
+ self.plot.removeItem(self._temp_roi)
191
+ self._temp_roi = None
192
+
193
+ def eventFilter(self, obj, event):
194
+ if self._roi_draw_mode is None:
195
+ return super().eventFilter(obj, event)
196
+ if event.type() == QEvent.GraphicsSceneMousePress and event.button() == Qt.LeftButton:
197
+ self._roi_start_pos = self.plot.vb.mapSceneToView(event.scenePos())
198
+ if self._roi_draw_mode == "rect":
199
+ self._temp_roi = RectangularROI(
200
+ pos=[self._roi_start_pos.x(), self._roi_start_pos.y()],
201
+ size=[5, 5],
202
+ parent_image=self.image_widget,
203
+ resize_handles=False,
204
+ )
205
+ if self._roi_draw_mode == "circle":
206
+ self._temp_roi = CircularROI(
207
+ pos=[self._roi_start_pos.x() - 2.5, self._roi_start_pos.y() - 2.5],
208
+ size=[5, 5],
209
+ parent_image=self.image_widget,
210
+ )
211
+ self.plot.addItem(self._temp_roi)
212
+ return True
213
+ elif event.type() == QEvent.GraphicsSceneMouseMove and self._temp_roi is not None:
214
+ pos = self.plot.vb.mapSceneToView(event.scenePos())
215
+ dx = pos.x() - self._roi_start_pos.x()
216
+ dy = pos.y() - self._roi_start_pos.y()
217
+
218
+ if self._roi_draw_mode == "rect":
219
+ self._temp_roi.setSize([dx, dy])
220
+ if self._roi_draw_mode == "circle":
221
+ r = max(
222
+ 1, math.hypot(dx, dy)
223
+ ) # radius never smaller than 1 for safety of handle mapping, otherwise SEGFAULT
224
+ d = 2 * r # diameter
225
+ self._temp_roi.setPos(self._roi_start_pos.x() - r, self._roi_start_pos.y() - r)
226
+ self._temp_roi.setSize([d, d])
227
+ return True
228
+ elif (
229
+ event.type() == QEvent.GraphicsSceneMouseRelease
230
+ and event.button() == Qt.LeftButton
231
+ and self._temp_roi is not None
232
+ ):
233
+ # finalize ROI
234
+ final_roi = self._temp_roi
235
+ self._temp_roi = None
236
+ self._set_roi_draw_mode(None)
237
+ # register via controller
238
+ final_roi.add_scale_handle()
239
+ self.controller.add_roi(final_roi)
240
+ return True
241
+ return super().eventFilter(obj, event)
242
+
243
+ # --------------------------------------------------------- controller slots
244
+ def _on_roi_added(self, roi: BaseROI):
245
+ # parent row with blank action column, name in ROI column
246
+ parent = QTreeWidgetItem(self.tree, ["", "", ""])
247
+ parent.setText(self.COL_ROI, roi.label)
248
+ parent.setFlags(parent.flags() | Qt.ItemIsEditable)
249
+ # --- delete button in actions column ---
250
+ del_btn = QToolButton()
251
+ delete_icon = material_icon(
252
+ "delete",
253
+ size=(20, 20),
254
+ convert_to_pixmap=False,
255
+ filled=False,
256
+ color=self.DELETE_BUTTON_COLOR,
257
+ )
258
+ del_btn.setIcon(delete_icon)
259
+ self.tree.setItemWidget(parent, self.COL_ACTION, del_btn)
260
+ del_btn.clicked.connect(lambda _=None, r=roi: self._delete_roi(r))
261
+ # color button
262
+ color_btn = ColorButtonNative(parent=self, color=roi.line_color)
263
+ self.tree.setItemWidget(parent, self.COL_PROPS, color_btn)
264
+ color_btn.clicked.connect(lambda: self._pick_color(roi, color_btn))
265
+
266
+ # child rows (3 columns: action, ROI, properties)
267
+ QTreeWidgetItem(parent, ["", "Type", roi.__class__.__name__])
268
+ width_item = QTreeWidgetItem(parent, ["", "Line width", ""])
269
+ width_spin = QSpinBox()
270
+ width_spin.setRange(1, 50)
271
+ width_spin.setValue(roi.line_width)
272
+ self.tree.setItemWidget(width_item, self.COL_PROPS, width_spin)
273
+ width_spin.valueChanged.connect(lambda v, r=roi: setattr(r, "line_width", v))
274
+
275
+ # --- Step 2: Insert separate coordinate rows (one per value)
276
+ coord_rows = {}
277
+ coords = roi.get_coordinates(typed=True)
278
+
279
+ for key, value in coords.items():
280
+ # Human-readable label: “center x” from “center_x”, etc.
281
+ label = key.replace("_", " ").title()
282
+ val_text = self._format_coord_text(value)
283
+ row = QTreeWidgetItem(parent, ["", label, val_text])
284
+ coord_rows[key] = row
285
+
286
+ # keep dict refs
287
+ self.roi_items[roi] = parent
288
+
289
+ # --- Step 3: Update coordinates on ROI movement
290
+ def _update_coords():
291
+ c_dict = roi.get_coordinates(typed=True)
292
+ for k, row in coord_rows.items():
293
+ if k in c_dict:
294
+ val = c_dict[k]
295
+ row.setText(self.COL_PROPS, self._format_coord_text(val))
296
+
297
+ if isinstance(roi, RectangularROI):
298
+ roi.edgesChanged.connect(_update_coords)
299
+ else:
300
+ roi.centerChanged.connect(_update_coords)
301
+
302
+ # sync width edits back to spinbox
303
+ roi.penChanged.connect(lambda r=roi, sp=width_spin: sp.setValue(r.line_width))
304
+ roi.nameChanged.connect(lambda n, itm=parent: itm.setText(self.COL_ROI, n))
305
+
306
+ # color changes
307
+ roi.penChanged.connect(lambda r=roi, b=color_btn: b.set_color(r.line_color))
308
+
309
+ for c in range(3):
310
+ self.tree.resizeColumnToContents(c)
311
+
312
+ def _on_roi_removed(self, roi: BaseROI):
313
+ item = self.roi_items.pop(roi, None)
314
+ if item:
315
+ idx = self.tree.indexOfTopLevelItem(item)
316
+ self.tree.takeTopLevelItem(idx)
317
+
318
+ # ---------------------------------------------------------- event handlers
319
+ def _pick_color(self, roi: BaseROI, btn: "ColorButtonNative"):
320
+ clr = QColorDialog.getColor(QColor(roi.line_color), self, "Select ROI Color")
321
+ if clr.isValid():
322
+ roi.line_color = clr.name()
323
+ btn.set_color(clr)
324
+
325
+ def _on_item_edited(self, item: QTreeWidgetItem, col: int):
326
+ if col != self.COL_ROI:
327
+ return
328
+ # find which roi
329
+ for r, it in self.roi_items.items():
330
+ if it is item:
331
+ r.label = item.text(self.COL_ROI)
332
+ break
333
+
334
+ def _delete_roi(self, roi):
335
+ self.controller.remove_roi(roi)
336
+
337
+ def cleanup(self):
338
+ self.cmap.close()
339
+ self.cmap.deleteLater()
340
+ super().cleanup()
341
+
342
+
343
+ # Demo
344
+ if __name__ == "__main__": # pragma: no cover
345
+ import sys
346
+
347
+ import numpy as np
348
+ from qtpy.QtWidgets import QApplication, QHBoxLayout, QVBoxLayout
349
+
350
+ from bec_widgets.widgets.plots.image.image import Image
351
+
352
+ app = QApplication(sys.argv)
353
+
354
+ bec_dispatcher = BECDispatcher(gui_id="roi_tree_demo")
355
+ client = bec_dispatcher.client
356
+ client.start()
357
+
358
+ image_widget = Image(popups=False)
359
+ image_widget.main_image.set_data(np.random.normal(size=(200, 200)))
360
+
361
+ win = QWidget()
362
+ win.setWindowTitle("Modular ROI Demo")
363
+ ml = QHBoxLayout(win)
364
+
365
+ # Add the image widget on the left
366
+ ml.addWidget(image_widget)
367
+
368
+ # ROI manager linked to that image
369
+ mgr = ROIPropertyTree(parent=image_widget, image_widget=image_widget)
370
+ mgr.setFixedWidth(350)
371
+ ml.addWidget(mgr)
372
+
373
+ win.resize(1500, 600)
374
+ win.show()
375
+ sys.exit(app.exec_())
@@ -113,6 +113,7 @@ class BaseROI(BECConnector):
113
113
  "line_width.setter",
114
114
  "get_coordinates",
115
115
  "get_data_from_image",
116
+ "set_position",
116
117
  ]
117
118
 
118
119
  def __init__(
@@ -125,7 +126,7 @@ class BaseROI(BECConnector):
125
126
  # ROI-specific
126
127
  label: str | None = None,
127
128
  line_color: str | None = None,
128
- line_width: int = 10,
129
+ line_width: int = 5,
129
130
  # all remaining pg.*ROI kwargs (pos, size, pen, …)
130
131
  **pg_kwargs,
131
132
  ):
@@ -333,7 +334,22 @@ class BaseROI(BECConnector):
333
334
  def add_scale_handle(self):
334
335
  return
335
336
 
337
+ def set_position(self, x: float, y: float):
338
+ """
339
+ Sets the position of the ROI.
340
+
341
+ Args:
342
+ x (float): The x-coordinate of the new position.
343
+ y (float): The y-coordinate of the new position.
344
+ """
345
+ self.setPos(x, y)
346
+
336
347
  def remove(self):
348
+ # Delegate to controller first so that GUI managers stay in sync
349
+ controller = getattr(self.parent_image, "roi_controller", None)
350
+ if controller and self in controller.rois:
351
+ controller.remove_roi(self)
352
+ return # controller will call back into this method once deregistered
337
353
  handles = self.handles
338
354
  for i in range(len(handles)):
339
355
  try:
@@ -342,9 +358,8 @@ class BaseROI(BECConnector):
342
358
  continue
343
359
  self.rpc_register.remove_rpc(self)
344
360
  self.parent_image.plot_item.removeItem(self)
345
- if hasattr(self.parent_image, "roi_controller"):
346
- self.parent_image.roi_controller._rois.remove(self)
347
- self.parent_image.roi_controller._rebuild_color_buffer()
361
+ viewBox = self.parent_plot_item.vb
362
+ viewBox.update()
348
363
 
349
364
 
350
365
  class RectangularROI(BaseROI, pg.RectROI):
@@ -378,7 +393,7 @@ class RectangularROI(BaseROI, pg.RectROI):
378
393
  # ROI specifics
379
394
  label: str | None = None,
380
395
  line_color: str | None = None,
381
- line_width: int = 10,
396
+ line_width: int = 5,
382
397
  resize_handles: bool = True,
383
398
  **extra_pg,
384
399
  ):
@@ -414,8 +429,6 @@ class RectangularROI(BaseROI, pg.RectROI):
414
429
 
415
430
  self.sigRegionChanged.connect(self._on_region_changed)
416
431
  self.adorner = LabelAdorner(roi=self)
417
- if resize_handles:
418
- self.add_scale_handle()
419
432
  self.hoverPen = fn.mkPen(color=(255, 0, 0), width=3, style=QtCore.Qt.DashLine)
420
433
  self.handleHoverPen = fn.mkPen("lime", width=4)
421
434
 
@@ -440,6 +453,11 @@ class RectangularROI(BaseROI, pg.RectROI):
440
453
  self.addScaleHandle([0, 0.5], [1, 0.5]) # left edge
441
454
  self.addScaleHandle([1, 0.5], [0, 0.5]) # right edge
442
455
 
456
+ self.handlePen = fn.mkPen("#ffff00", width=5) # bright yellow outline
457
+ self.handleHoverPen = fn.mkPen("#00ffff", width=4) # cyan, thicker when hovered
458
+ self.handleBrush = (200, 200, 0, 120) # semi-transparent fill
459
+ self.handleHoverBrush = (0, 255, 255, 160)
460
+
443
461
  def _on_region_changed(self):
444
462
  """
445
463
  Handles ROI region change events.
@@ -544,7 +562,7 @@ class CircularROI(BaseROI, pg.CircleROI):
544
562
  parent_image: Image | None = None,
545
563
  label: str | None = None,
546
564
  line_color: str | None = None,
547
- line_width: int = 10,
565
+ line_width: int = 5,
548
566
  **extra_pg,
549
567
  ):
550
568
  """
@@ -725,7 +743,7 @@ class ROIController(QObject):
725
743
  roi.line_color = color
726
744
  # ensure line width default is at least 3 if not previously set
727
745
  if getattr(roi, "line_width", 0) < 1:
728
- roi.line_width = 10
746
+ roi.line_width = 5
729
747
  self.roiAdded.emit(roi)
730
748
 
731
749
  def remove_roi(self, roi: BaseROI):
@@ -738,8 +756,12 @@ class ROIController(QObject):
738
756
  Args:
739
757
  roi (BaseROI): The ROI instance to remove.
740
758
  """
741
- rois = self._rois
742
- if roi not in rois:
759
+ if roi in self._rois:
760
+ self.roiRemoved.emit(roi)
761
+ self._rois.remove(roi)
762
+ roi.remove()
763
+ self._rebuild_color_buffer()
764
+ else:
743
765
  roi.remove()
744
766
 
745
767
  def get_roi(self, index: int) -> BaseROI | None:
@@ -782,7 +804,7 @@ class ROIController(QObject):
782
804
  """
783
805
  roi = self.get_roi(index)
784
806
  if roi is not None:
785
- roi.remove()
807
+ self.remove_roi(roi)
786
808
 
787
809
  def remove_roi_by_name(self, name: str):
788
810
  """
@@ -793,7 +815,7 @@ class ROIController(QObject):
793
815
  """
794
816
  roi = self.get_roi_by_name(name)
795
817
  if roi is not None:
796
- roi.remove()
818
+ self.remove_roi(roi)
797
819
 
798
820
  def clear(self):
799
821
  """
@@ -803,7 +825,7 @@ class ROIController(QObject):
803
825
  the cleared signal to notify listeners that all ROIs have been removed.
804
826
  """
805
827
  for roi in list(self._rois):
806
- roi.remove()
828
+ self.remove_roi(roi)
807
829
  self.cleared.emit()
808
830
 
809
831
  def renormalize_colors(self):
@@ -414,6 +414,8 @@ class Waveform(PlotBase):
414
414
  """
415
415
  Slot for when the axis settings dialog is closed.
416
416
  """
417
+ self.dap_summary.close()
418
+ self.dap_summary.deleteLater()
417
419
  self.dap_summary_dialog.deleteLater()
418
420
  self.dap_summary_dialog = None
419
421
  self.toolbar.widgets["fit_params"].action.setChecked(False)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bec_widgets
3
- Version: 2.5.3
3
+ Version: 2.6.0
4
4
  Summary: BEC Widgets
5
5
  Project-URL: Bug Tracker, https://gitlab.psi.ch/bec/bec_widgets/issues
6
6
  Project-URL: Homepage, https://gitlab.psi.ch/bec/bec_widgets