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.
- .github/ISSUE_TEMPLATE/bug_report.yml +41 -0
- .gitlab/issue_templates/documentation_update_template.md → .github/ISSUE_TEMPLATE/documentation_update.md +10 -0
- .github/ISSUE_TEMPLATE/feature_request.md +3 -2
- .gitlab/merge_request_templates/default.md → .github/pull_request_template.md +8 -3
- .github/scripts/pr_issue_sync/pr_issue_sync.py +342 -0
- .github/scripts/pr_issue_sync/requirements.txt +2 -0
- .github/workflows/sync-issues-pr.yml +40 -0
- CHANGELOG.md +39 -0
- PKG-INFO +1 -1
- bec_widgets/cli/client.py +31 -1
- bec_widgets/examples/jupyter_console/jupyter_console_window.py +1 -1
- bec_widgets/widgets/containers/dock/dock_area.py +9 -4
- bec_widgets/widgets/plots/image/image.py +72 -8
- bec_widgets/widgets/plots/image/setting_widgets/__init__.py +0 -0
- bec_widgets/widgets/plots/image/setting_widgets/image_roi_tree.py +375 -0
- bec_widgets/widgets/plots/roi/image_roi.py +36 -14
- bec_widgets/widgets/plots/waveform/waveform.py +2 -0
- {bec_widgets-2.5.3.dist-info → bec_widgets-2.6.0.dist-info}/METADATA +1 -1
- {bec_widgets-2.5.3.dist-info → bec_widgets-2.6.0.dist-info}/RECORD +23 -20
- pyproject.toml +1 -1
- .github/ISSUE_TEMPLATE/bug_report.md +0 -26
- .gitlab/issue_templates/bug_report_template.md +0 -17
- .gitlab/issue_templates/feature_request_template.md +0 -40
- {bec_widgets-2.5.3.dist-info → bec_widgets-2.6.0.dist-info}/WHEEL +0 -0
- {bec_widgets-2.5.3.dist-info → bec_widgets-2.6.0.dist-info}/entry_points.txt +0 -0
- {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
|
-
|
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 =
|
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
|
-
|
1048
|
-
|
1049
|
-
|
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_())
|
File without changes
|
@@ -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 =
|
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
|
-
|
346
|
-
|
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 =
|
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 =
|
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 =
|
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
|
-
|
742
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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)
|