bec-widgets 2.13.2__py3-none-any.whl → 2.15.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.
- CHANGELOG.md +37 -0
- PKG-INFO +1 -1
- bec_widgets/cli/client.py +123 -1
- bec_widgets/widgets/containers/main_window/addons/scroll_label.py +89 -0
- bec_widgets/widgets/containers/main_window/main_window.py +94 -20
- bec_widgets/widgets/plots/image/image_base.py +12 -1
- bec_widgets/widgets/plots/image/setting_widgets/image_roi_tree.py +42 -22
- bec_widgets/widgets/plots/roi/image_roi.py +86 -0
- {bec_widgets-2.13.2.dist-info → bec_widgets-2.15.0.dist-info}/METADATA +1 -1
- {bec_widgets-2.13.2.dist-info → bec_widgets-2.15.0.dist-info}/RECORD +14 -13
- pyproject.toml +1 -1
- {bec_widgets-2.13.2.dist-info → bec_widgets-2.15.0.dist-info}/WHEEL +0 -0
- {bec_widgets-2.13.2.dist-info → bec_widgets-2.15.0.dist-info}/entry_points.txt +0 -0
- {bec_widgets-2.13.2.dist-info → bec_widgets-2.15.0.dist-info}/licenses/LICENSE +0 -0
CHANGELOG.md
CHANGED
@@ -1,6 +1,43 @@
|
|
1
1
|
# CHANGELOG
|
2
2
|
|
3
3
|
|
4
|
+
## v2.15.0 (2025-06-15)
|
5
|
+
|
6
|
+
### Bug Fixes
|
7
|
+
|
8
|
+
- **main_window**: Central widget cleanup check to not remove None
|
9
|
+
([`644be62`](https://github.com/bec-project/bec_widgets/commit/644be621f20cf09037da763f6217df9d1e4642bc))
|
10
|
+
|
11
|
+
### Features
|
12
|
+
|
13
|
+
- **main_window**: Main window can display the messages from the send_client_info as a scrolling
|
14
|
+
horizontal text; closes #700
|
15
|
+
([`0dec78a`](https://github.com/bec-project/bec_widgets/commit/0dec78afbaddbef98d20949d3a0ba4e0dc8529df))
|
16
|
+
|
17
|
+
### Refactoring
|
18
|
+
|
19
|
+
- **main_window**: App id is displayed as QLabel instead of message
|
20
|
+
([`57b9a57`](https://github.com/bec-project/bec_widgets/commit/57b9a57a631f267a8cb3622bf73035ffb15510e6))
|
21
|
+
|
22
|
+
### Testing
|
23
|
+
|
24
|
+
- **main_window**: Becmainwindow tests extended
|
25
|
+
([`30acc4c`](https://github.com/bec-project/bec_widgets/commit/30acc4c236bfbfed19f56512b264a52b4359e6c1))
|
26
|
+
|
27
|
+
|
28
|
+
## v2.14.0 (2025-06-13)
|
29
|
+
|
30
|
+
### Features
|
31
|
+
|
32
|
+
- **image_roi**: Added EllipticalROI
|
33
|
+
([`af8db0b`](https://github.com/bec-project/bec_widgets/commit/af8db0bede32dd10ad72671a8c2978ca884f4994))
|
34
|
+
|
35
|
+
### Refactoring
|
36
|
+
|
37
|
+
- **image_roi_tree**: Shape switch logic adjusted to reduce code repetition
|
38
|
+
([`f0d48a0`](https://github.com/bec-project/bec_widgets/commit/f0d48a05085bb8c628e516d4a976d776ee63c7c3))
|
39
|
+
|
40
|
+
|
4
41
|
## v2.13.2 (2025-06-13)
|
5
42
|
|
6
43
|
### Bug Fixes
|
PKG-INFO
CHANGED
bec_widgets/cli/client.py
CHANGED
@@ -1044,6 +1044,128 @@ class DeviceLineEdit(RPCBase):
|
|
1044
1044
|
"""
|
1045
1045
|
|
1046
1046
|
|
1047
|
+
class EllipticalROI(RPCBase):
|
1048
|
+
"""Elliptical Region of Interest with centre/width/height tracking and auto-labelling."""
|
1049
|
+
|
1050
|
+
@property
|
1051
|
+
@rpc_call
|
1052
|
+
def label(self) -> "str":
|
1053
|
+
"""
|
1054
|
+
Gets the display name of this ROI.
|
1055
|
+
|
1056
|
+
Returns:
|
1057
|
+
str: The current name of the ROI.
|
1058
|
+
"""
|
1059
|
+
|
1060
|
+
@label.setter
|
1061
|
+
@rpc_call
|
1062
|
+
def label(self) -> "str":
|
1063
|
+
"""
|
1064
|
+
Gets the display name of this ROI.
|
1065
|
+
|
1066
|
+
Returns:
|
1067
|
+
str: The current name of the ROI.
|
1068
|
+
"""
|
1069
|
+
|
1070
|
+
@property
|
1071
|
+
@rpc_call
|
1072
|
+
def movable(self) -> "bool":
|
1073
|
+
"""
|
1074
|
+
Gets whether this ROI is movable.
|
1075
|
+
|
1076
|
+
Returns:
|
1077
|
+
bool: True if the ROI can be moved, False otherwise.
|
1078
|
+
"""
|
1079
|
+
|
1080
|
+
@movable.setter
|
1081
|
+
@rpc_call
|
1082
|
+
def movable(self) -> "bool":
|
1083
|
+
"""
|
1084
|
+
Gets whether this ROI is movable.
|
1085
|
+
|
1086
|
+
Returns:
|
1087
|
+
bool: True if the ROI can be moved, False otherwise.
|
1088
|
+
"""
|
1089
|
+
|
1090
|
+
@property
|
1091
|
+
@rpc_call
|
1092
|
+
def line_color(self) -> "str":
|
1093
|
+
"""
|
1094
|
+
Gets the current line color of the ROI.
|
1095
|
+
|
1096
|
+
Returns:
|
1097
|
+
str: The current line color as a string (e.g., hex color code).
|
1098
|
+
"""
|
1099
|
+
|
1100
|
+
@line_color.setter
|
1101
|
+
@rpc_call
|
1102
|
+
def line_color(self) -> "str":
|
1103
|
+
"""
|
1104
|
+
Gets the current line color of the ROI.
|
1105
|
+
|
1106
|
+
Returns:
|
1107
|
+
str: The current line color as a string (e.g., hex color code).
|
1108
|
+
"""
|
1109
|
+
|
1110
|
+
@property
|
1111
|
+
@rpc_call
|
1112
|
+
def line_width(self) -> "int":
|
1113
|
+
"""
|
1114
|
+
Gets the current line width of the ROI.
|
1115
|
+
|
1116
|
+
Returns:
|
1117
|
+
int: The current line width in pixels.
|
1118
|
+
"""
|
1119
|
+
|
1120
|
+
@line_width.setter
|
1121
|
+
@rpc_call
|
1122
|
+
def line_width(self) -> "int":
|
1123
|
+
"""
|
1124
|
+
Gets the current line width of the ROI.
|
1125
|
+
|
1126
|
+
Returns:
|
1127
|
+
int: The current line width in pixels.
|
1128
|
+
"""
|
1129
|
+
|
1130
|
+
@rpc_call
|
1131
|
+
def get_coordinates(self, typed: "bool | None" = None) -> "dict | tuple":
|
1132
|
+
"""
|
1133
|
+
Return the ellipse's centre and size.
|
1134
|
+
|
1135
|
+
Args:
|
1136
|
+
typed (bool | None): If True returns dict; otherwise tuple.
|
1137
|
+
"""
|
1138
|
+
|
1139
|
+
@rpc_call
|
1140
|
+
def get_data_from_image(
|
1141
|
+
self, image_item: "pg.ImageItem | None" = None, returnMappedCoords: "bool" = False, **kwargs
|
1142
|
+
):
|
1143
|
+
"""
|
1144
|
+
Wrapper around `pyqtgraph.ROI.getArrayRegion`.
|
1145
|
+
|
1146
|
+
Args:
|
1147
|
+
image_item (pg.ImageItem or None): The ImageItem to sample. If None, auto-detects
|
1148
|
+
the first `ImageItem` in the same GraphicsScene as this ROI.
|
1149
|
+
returnMappedCoords (bool): If True, also returns the coordinate array generated by
|
1150
|
+
*getArrayRegion*.
|
1151
|
+
**kwargs: Additional keyword arguments passed to *getArrayRegion* or *affineSlice*,
|
1152
|
+
such as `axes`, `order`, `shape`, etc.
|
1153
|
+
|
1154
|
+
Returns:
|
1155
|
+
ndarray: Pixel data inside the ROI, or (data, coords) if *returnMappedCoords* is True.
|
1156
|
+
"""
|
1157
|
+
|
1158
|
+
@rpc_call
|
1159
|
+
def set_position(self, x: "float", y: "float"):
|
1160
|
+
"""
|
1161
|
+
Sets the position of the ROI.
|
1162
|
+
|
1163
|
+
Args:
|
1164
|
+
x (float): The x-coordinate of the new position.
|
1165
|
+
y (float): The y-coordinate of the new position.
|
1166
|
+
"""
|
1167
|
+
|
1168
|
+
|
1047
1169
|
class Image(RPCBase):
|
1048
1170
|
"""Image widget for displaying 2D data."""
|
1049
1171
|
|
@@ -1529,7 +1651,7 @@ class Image(RPCBase):
|
|
1529
1651
|
@rpc_call
|
1530
1652
|
def add_roi(
|
1531
1653
|
self,
|
1532
|
-
kind: "Literal['rect', 'circle']" = "rect",
|
1654
|
+
kind: "Literal['rect', 'circle', 'ellipse']" = "rect",
|
1533
1655
|
name: "str | None" = None,
|
1534
1656
|
line_width: "int | None" = 5,
|
1535
1657
|
pos: "tuple[float, float] | None" = (10, 10),
|
@@ -0,0 +1,89 @@
|
|
1
|
+
from qtpy.QtCore import QTimer
|
2
|
+
from qtpy.QtGui import QFontMetrics, QPainter
|
3
|
+
from qtpy.QtWidgets import QLabel
|
4
|
+
|
5
|
+
|
6
|
+
class ScrollLabel(QLabel):
|
7
|
+
"""A QLabel that scrolls its text horizontally across the widget."""
|
8
|
+
|
9
|
+
def __init__(self, parent=None, speed_ms=30, step_px=1, delay_ms=2000):
|
10
|
+
super().__init__(parent=parent)
|
11
|
+
self._offset = 0
|
12
|
+
self._text_width = 0
|
13
|
+
|
14
|
+
# scrolling timer (runs continuously once started)
|
15
|
+
self._timer = QTimer(self)
|
16
|
+
self._timer.setInterval(speed_ms)
|
17
|
+
self._timer.timeout.connect(self._scroll)
|
18
|
+
|
19
|
+
# delay‑before‑scroll timer (single‑shot)
|
20
|
+
self._delay_timer = QTimer(self)
|
21
|
+
self._delay_timer.setSingleShot(True)
|
22
|
+
self._delay_timer.setInterval(delay_ms)
|
23
|
+
self._delay_timer.timeout.connect(self._timer.start)
|
24
|
+
|
25
|
+
self._step_px = step_px
|
26
|
+
|
27
|
+
def setText(self, text):
|
28
|
+
super().setText(text)
|
29
|
+
fm = QFontMetrics(self.font())
|
30
|
+
self._text_width = fm.horizontalAdvance(text)
|
31
|
+
self._offset = 0
|
32
|
+
self._update_timer()
|
33
|
+
|
34
|
+
def resizeEvent(self, event):
|
35
|
+
super().resizeEvent(event)
|
36
|
+
self._update_timer()
|
37
|
+
|
38
|
+
def _update_timer(self):
|
39
|
+
"""
|
40
|
+
Decide whether to start or stop scrolling.
|
41
|
+
|
42
|
+
If the text is wider than the visible area, start a single‑shot
|
43
|
+
delay timer (2s by default). Scrolling begins only after this
|
44
|
+
delay. Any change (resize or new text) restarts the logic.
|
45
|
+
"""
|
46
|
+
needs_scroll = self._text_width > self.width()
|
47
|
+
|
48
|
+
if needs_scroll:
|
49
|
+
if self._timer.isActive():
|
50
|
+
self._timer.stop()
|
51
|
+
self._offset = 0
|
52
|
+
if not self._delay_timer.isActive():
|
53
|
+
self._delay_timer.start()
|
54
|
+
else:
|
55
|
+
if self._delay_timer.isActive():
|
56
|
+
self._delay_timer.stop()
|
57
|
+
if self._timer.isActive():
|
58
|
+
self._timer.stop()
|
59
|
+
self.update()
|
60
|
+
|
61
|
+
def _scroll(self):
|
62
|
+
self._offset += self._step_px
|
63
|
+
if self._offset >= self._text_width:
|
64
|
+
self._offset = 0
|
65
|
+
self.update()
|
66
|
+
|
67
|
+
def paintEvent(self, event):
|
68
|
+
painter = QPainter(self)
|
69
|
+
painter.setRenderHint(QPainter.TextAntialiasing)
|
70
|
+
text = self.text()
|
71
|
+
if not text:
|
72
|
+
return
|
73
|
+
fm = QFontMetrics(self.font())
|
74
|
+
y = (self.height() + fm.ascent() - fm.descent()) // 2
|
75
|
+
if self._text_width <= self.width():
|
76
|
+
painter.drawText(0, y, text)
|
77
|
+
else:
|
78
|
+
x = -self._offset
|
79
|
+
gap = 50 # space between repeating text blocks
|
80
|
+
while x < self.width():
|
81
|
+
painter.drawText(x, y, text)
|
82
|
+
x += self._text_width + gap
|
83
|
+
|
84
|
+
def cleanup(self):
|
85
|
+
"""Stop all timers to prevent memory leaks."""
|
86
|
+
if self._timer.isActive():
|
87
|
+
self._timer.stop()
|
88
|
+
if self._delay_timer.isActive():
|
89
|
+
self._delay_timer.stop()
|
@@ -1,16 +1,17 @@
|
|
1
1
|
import os
|
2
2
|
|
3
|
-
from
|
3
|
+
from bec_lib.endpoints import MessageEndpoints
|
4
|
+
from qtpy.QtCore import QEvent, QSize, Qt
|
4
5
|
from qtpy.QtGui import QAction, QActionGroup, QIcon
|
5
|
-
from qtpy.QtWidgets import QApplication, QMainWindow, QStyle
|
6
|
+
from qtpy.QtWidgets import QApplication, QFrame, QLabel, QMainWindow, QStyle, QVBoxLayout, QWidget
|
6
7
|
|
7
8
|
import bec_widgets
|
8
9
|
from bec_widgets.utils import UILoader
|
9
10
|
from bec_widgets.utils.bec_widget import BECWidget
|
10
11
|
from bec_widgets.utils.colors import apply_theme
|
11
|
-
from bec_widgets.utils.container_utils import WidgetContainerUtils
|
12
12
|
from bec_widgets.utils.error_popups import SafeSlot
|
13
13
|
from bec_widgets.utils.widget_io import WidgetHierarchy
|
14
|
+
from bec_widgets.widgets.containers.main_window.addons.scroll_label import ScrollLabel
|
14
15
|
from bec_widgets.widgets.containers.main_window.addons.web_links import BECWebLinksMixin
|
15
16
|
|
16
17
|
MODULE_PATH = os.path.dirname(bec_widgets.__file__)
|
@@ -36,6 +37,14 @@ class BECMainWindow(BECWidget, QMainWindow):
|
|
36
37
|
self._init_ui()
|
37
38
|
self._connect_to_theme_change()
|
38
39
|
|
40
|
+
# Connections to BEC Notifications
|
41
|
+
self.bec_dispatcher.connect_slot(
|
42
|
+
self.display_client_message, MessageEndpoints.client_info()
|
43
|
+
)
|
44
|
+
|
45
|
+
################################################################################
|
46
|
+
# MainWindow Elements Initialization
|
47
|
+
################################################################################
|
39
48
|
def _init_ui(self):
|
40
49
|
|
41
50
|
# Set the icon
|
@@ -43,40 +52,72 @@ class BECMainWindow(BECWidget, QMainWindow):
|
|
43
52
|
|
44
53
|
# Set Menu and Status bar
|
45
54
|
self._setup_menu_bar()
|
55
|
+
self._init_status_bar_widgets()
|
46
56
|
|
47
57
|
# BEC Specific UI
|
48
58
|
self.display_app_id()
|
49
59
|
|
60
|
+
def _init_status_bar_widgets(self):
|
61
|
+
"""
|
62
|
+
Prepare the BEC specific widgets in the status bar.
|
63
|
+
"""
|
64
|
+
status_bar = self.statusBar()
|
65
|
+
|
66
|
+
# Left: App‑ID label
|
67
|
+
self._app_id_label = QLabel()
|
68
|
+
self._app_id_label.setAlignment(
|
69
|
+
Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter
|
70
|
+
)
|
71
|
+
status_bar.addWidget(self._app_id_label)
|
72
|
+
|
73
|
+
# Add a separator after the app ID label
|
74
|
+
self._add_separator()
|
75
|
+
|
76
|
+
# Centre: Client‑info label (stretch=1 so it expands)
|
77
|
+
self._client_info_label = ScrollLabel()
|
78
|
+
self._client_info_label.setAlignment(
|
79
|
+
Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter
|
80
|
+
)
|
81
|
+
status_bar.addWidget(self._client_info_label, 1)
|
82
|
+
|
83
|
+
def _add_separator(self):
|
84
|
+
"""
|
85
|
+
Add a vertically centred separator to the status bar.
|
86
|
+
"""
|
87
|
+
status_bar = self.statusBar()
|
88
|
+
|
89
|
+
# The actual line
|
90
|
+
line = QFrame()
|
91
|
+
line.setFrameShape(QFrame.VLine)
|
92
|
+
line.setFrameShadow(QFrame.Sunken)
|
93
|
+
line.setFixedHeight(status_bar.sizeHint().height() - 2)
|
94
|
+
|
95
|
+
# Wrapper to center the line vertically -> work around for QFrame not being able to center itself
|
96
|
+
wrapper = QWidget()
|
97
|
+
vbox = QVBoxLayout(wrapper)
|
98
|
+
vbox.setContentsMargins(0, 0, 0, 0)
|
99
|
+
vbox.addStretch()
|
100
|
+
vbox.addWidget(line, alignment=Qt.AlignHCenter)
|
101
|
+
vbox.addStretch()
|
102
|
+
wrapper.setFixedWidth(line.sizeHint().width())
|
103
|
+
|
104
|
+
status_bar.addWidget(wrapper)
|
105
|
+
|
50
106
|
def _init_bec_icon(self):
|
51
107
|
icon = self.app.windowIcon()
|
52
108
|
if icon.isNull():
|
53
|
-
print("No icon is set, setting default icon")
|
54
109
|
icon = QIcon()
|
55
110
|
icon.addFile(
|
56
111
|
os.path.join(MODULE_PATH, "assets", "app_icons", "bec_widgets_icon.png"),
|
57
112
|
size=QSize(48, 48),
|
58
113
|
)
|
59
114
|
self.app.setWindowIcon(icon)
|
60
|
-
else:
|
61
|
-
print("An icon is set")
|
62
115
|
|
63
116
|
def load_ui(self, ui_file):
|
64
117
|
loader = UILoader(self)
|
65
118
|
self.ui = loader.loader(ui_file)
|
66
119
|
self.setCentralWidget(self.ui)
|
67
120
|
|
68
|
-
def display_app_id(self):
|
69
|
-
"""
|
70
|
-
Display the app ID in the status bar.
|
71
|
-
"""
|
72
|
-
if self.bec_dispatcher.cli_server is None:
|
73
|
-
status_message = "Not connected"
|
74
|
-
else:
|
75
|
-
# Get the server ID from the dispatcher
|
76
|
-
server_id = self.bec_dispatcher.cli_server.gui_id
|
77
|
-
status_message = f"App ID: {server_id}"
|
78
|
-
self.statusBar().showMessage(status_message)
|
79
|
-
|
80
121
|
def _fetch_theme(self) -> str:
|
81
122
|
return self.app.theme.theme
|
82
123
|
|
@@ -164,8 +205,37 @@ class BECMainWindow(BECWidget, QMainWindow):
|
|
164
205
|
help_menu.addAction(widgets_docs)
|
165
206
|
help_menu.addAction(bug_report)
|
166
207
|
|
208
|
+
################################################################################
|
209
|
+
# Status Bar Addons
|
210
|
+
################################################################################
|
211
|
+
def display_app_id(self):
|
212
|
+
"""
|
213
|
+
Display the app ID in the status bar.
|
214
|
+
"""
|
215
|
+
if self.bec_dispatcher.cli_server is None:
|
216
|
+
status_message = "Not connected"
|
217
|
+
else:
|
218
|
+
# Get the server ID from the dispatcher
|
219
|
+
server_id = self.bec_dispatcher.cli_server.gui_id
|
220
|
+
status_message = f"App ID: {server_id}"
|
221
|
+
self._app_id_label.setText(status_message)
|
222
|
+
|
223
|
+
@SafeSlot(dict, dict)
|
224
|
+
def display_client_message(self, msg: dict, meta: dict):
|
225
|
+
message = msg.get("message", "")
|
226
|
+
self._client_info_label.setText(message)
|
227
|
+
|
228
|
+
################################################################################
|
229
|
+
# General and Cleanup Methods
|
230
|
+
################################################################################
|
167
231
|
@SafeSlot(str)
|
168
232
|
def change_theme(self, theme: str):
|
233
|
+
"""
|
234
|
+
Change the theme of the application.
|
235
|
+
|
236
|
+
Args:
|
237
|
+
theme(str): The theme to apply, either "light" or "dark".
|
238
|
+
"""
|
169
239
|
apply_theme(theme)
|
170
240
|
|
171
241
|
def event(self, event):
|
@@ -175,8 +245,9 @@ class BECMainWindow(BECWidget, QMainWindow):
|
|
175
245
|
|
176
246
|
def cleanup(self):
|
177
247
|
central_widget = self.centralWidget()
|
178
|
-
central_widget
|
179
|
-
|
248
|
+
if central_widget is not None:
|
249
|
+
central_widget.close()
|
250
|
+
central_widget.deleteLater()
|
180
251
|
if not isinstance(central_widget, BECWidget):
|
181
252
|
# if the central widget is not a BECWidget, we need to call the cleanup method
|
182
253
|
# of all widgets whose parent is the current BECMainWindow
|
@@ -187,6 +258,9 @@ class BECMainWindow(BECWidget, QMainWindow):
|
|
187
258
|
child.cleanup()
|
188
259
|
child.close()
|
189
260
|
child.deleteLater()
|
261
|
+
|
262
|
+
# Status bar widgets cleanup
|
263
|
+
self._client_info_label.cleanup()
|
190
264
|
super().cleanup()
|
191
265
|
|
192
266
|
|
@@ -24,6 +24,7 @@ from bec_widgets.widgets.plots.plot_base import PlotBase
|
|
24
24
|
from bec_widgets.widgets.plots.roi.image_roi import (
|
25
25
|
BaseROI,
|
26
26
|
CircularROI,
|
27
|
+
EllipticalROI,
|
27
28
|
RectangularROI,
|
28
29
|
ROIController,
|
29
30
|
)
|
@@ -554,7 +555,7 @@ class ImageBase(PlotBase):
|
|
554
555
|
|
555
556
|
def add_roi(
|
556
557
|
self,
|
557
|
-
kind: Literal["rect", "circle"] = "rect",
|
558
|
+
kind: Literal["rect", "circle", "ellipse"] = "rect",
|
558
559
|
name: str | None = None,
|
559
560
|
line_width: int | None = 5,
|
560
561
|
pos: tuple[float, float] | None = (10, 10),
|
@@ -599,6 +600,16 @@ class ImageBase(PlotBase):
|
|
599
600
|
movable=movable,
|
600
601
|
**pg_kwargs,
|
601
602
|
)
|
603
|
+
elif kind == "ellipse":
|
604
|
+
roi = EllipticalROI(
|
605
|
+
pos=pos,
|
606
|
+
size=size,
|
607
|
+
parent_image=self,
|
608
|
+
line_width=line_width,
|
609
|
+
label=name,
|
610
|
+
movable=movable,
|
611
|
+
**pg_kwargs,
|
612
|
+
)
|
602
613
|
else:
|
603
614
|
raise ValueError("kind must be 'rect' or 'circle'")
|
604
615
|
|
@@ -24,6 +24,7 @@ from bec_widgets.utils.toolbar import MaterialIconAction, ModularToolBar
|
|
24
24
|
from bec_widgets.widgets.plots.roi.image_roi import (
|
25
25
|
BaseROI,
|
26
26
|
CircularROI,
|
27
|
+
EllipticalROI,
|
27
28
|
RectangularROI,
|
28
29
|
ROIController,
|
29
30
|
)
|
@@ -121,11 +122,21 @@ class ROIPropertyTree(BECWidget, QWidget):
|
|
121
122
|
# --------------------------------------------------------------------- UI
|
122
123
|
def _init_toolbar(self):
|
123
124
|
tb = ModularToolBar(self, self, orientation="horizontal")
|
125
|
+
self._draw_actions: dict[str, MaterialIconAction] = {}
|
124
126
|
# --- ROI draw actions (toggleable) ---
|
125
127
|
self.add_rect_action = MaterialIconAction("add_box", "Add Rect ROI", True, self)
|
126
|
-
self.add_circle_action = MaterialIconAction("add_circle", "Add Circle ROI", True, self)
|
127
128
|
tb.add_action("Add Rect ROI", self.add_rect_action, self)
|
129
|
+
self._draw_actions["rect"] = self.add_rect_action
|
130
|
+
self.add_circle_action = MaterialIconAction("add_circle", "Add Circle ROI", True, self)
|
128
131
|
tb.add_action("Add Circle ROI", self.add_circle_action, self)
|
132
|
+
self._draw_actions["circle"] = self.add_circle_action
|
133
|
+
# --- Ellipse ROI draw action ---
|
134
|
+
self.add_ellipse_action = MaterialIconAction("vignette", "Add Ellipse ROI", True, self)
|
135
|
+
tb.add_action("Add Ellipse ROI", self.add_ellipse_action, self)
|
136
|
+
self._draw_actions["ellipse"] = self.add_ellipse_action
|
137
|
+
|
138
|
+
for mode, act in self._draw_actions.items():
|
139
|
+
act.action.toggled.connect(lambda on, m=mode: self._on_draw_action_toggled(m, on))
|
129
140
|
|
130
141
|
# Expand/Collapse toggle
|
131
142
|
self.expand_toggle = MaterialIconAction(
|
@@ -174,17 +185,9 @@ class ROIPropertyTree(BECWidget, QWidget):
|
|
174
185
|
self.controller.paletteChanged.connect(lambda cmap: setattr(self.cmap, "colormap", cmap))
|
175
186
|
|
176
187
|
# ROI drawing state
|
177
|
-
self._roi_draw_mode = None # 'rect' | 'circle' | None
|
188
|
+
self._roi_draw_mode = None # 'rect' | 'circle' | 'ellipse' | None
|
178
189
|
self._roi_start_pos = None # QPointF in image coords
|
179
190
|
self._temp_roi = None # live ROI being resized while dragging
|
180
|
-
|
181
|
-
# toggle handlers
|
182
|
-
self.add_rect_action.action.toggled.connect(
|
183
|
-
lambda on: self._set_roi_draw_mode("rect" if on else None)
|
184
|
-
)
|
185
|
-
self.add_circle_action.action.toggled.connect(
|
186
|
-
lambda on: self._set_roi_draw_mode("circle" if on else None)
|
187
|
-
)
|
188
191
|
# capture mouse events on the plot scene
|
189
192
|
self.plot.scene().installEventFilter(self)
|
190
193
|
|
@@ -214,16 +217,12 @@ class ROIPropertyTree(BECWidget, QWidget):
|
|
214
217
|
return str(value)
|
215
218
|
|
216
219
|
def _set_roi_draw_mode(self, mode: str | None):
|
217
|
-
#
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
self.add_circle_action.action.setChecked(True)
|
224
|
-
else:
|
225
|
-
self.add_rect_action.action.setChecked(False)
|
226
|
-
self.add_circle_action.action.setChecked(False)
|
220
|
+
# Update toolbar actions so that only the selected mode is checked
|
221
|
+
for m, act in self._draw_actions.items():
|
222
|
+
act.action.blockSignals(True)
|
223
|
+
act.action.setChecked(m == mode)
|
224
|
+
act.action.blockSignals(False)
|
225
|
+
|
227
226
|
self._roi_draw_mode = mode
|
228
227
|
self._roi_start_pos = None
|
229
228
|
# remove any unfinished temp ROI
|
@@ -231,6 +230,15 @@ class ROIPropertyTree(BECWidget, QWidget):
|
|
231
230
|
self.plot.removeItem(self._temp_roi)
|
232
231
|
self._temp_roi = None
|
233
232
|
|
233
|
+
def _on_draw_action_toggled(self, mode: str, checked: bool):
|
234
|
+
if checked:
|
235
|
+
# Activate selected mode
|
236
|
+
self._set_roi_draw_mode(mode)
|
237
|
+
else:
|
238
|
+
# If the active mode is being unchecked, clear mode
|
239
|
+
if self._roi_draw_mode == mode:
|
240
|
+
self._set_roi_draw_mode(None)
|
241
|
+
|
234
242
|
def eventFilter(self, obj, event):
|
235
243
|
if self._roi_draw_mode is None:
|
236
244
|
return super().eventFilter(obj, event)
|
@@ -243,12 +251,18 @@ class ROIPropertyTree(BECWidget, QWidget):
|
|
243
251
|
parent_image=self.image_widget,
|
244
252
|
resize_handles=False,
|
245
253
|
)
|
246
|
-
|
254
|
+
elif self._roi_draw_mode == "circle":
|
247
255
|
self._temp_roi = CircularROI(
|
248
256
|
pos=[self._roi_start_pos.x() - 2.5, self._roi_start_pos.y() - 2.5],
|
249
257
|
size=[5, 5],
|
250
258
|
parent_image=self.image_widget,
|
251
259
|
)
|
260
|
+
elif self._roi_draw_mode == "ellipse":
|
261
|
+
self._temp_roi = EllipticalROI(
|
262
|
+
pos=[self._roi_start_pos.x() - 2.5, self._roi_start_pos.y() - 2.5],
|
263
|
+
size=[5, 5],
|
264
|
+
parent_image=self.image_widget,
|
265
|
+
)
|
252
266
|
self.plot.addItem(self._temp_roi)
|
253
267
|
return True
|
254
268
|
elif event.type() == QEvent.GraphicsSceneMouseMove and self._temp_roi is not None:
|
@@ -258,13 +272,19 @@ class ROIPropertyTree(BECWidget, QWidget):
|
|
258
272
|
|
259
273
|
if self._roi_draw_mode == "rect":
|
260
274
|
self._temp_roi.setSize([dx, dy])
|
261
|
-
|
275
|
+
elif self._roi_draw_mode == "circle":
|
262
276
|
r = max(
|
263
277
|
1, math.hypot(dx, dy)
|
264
278
|
) # radius never smaller than 1 for safety of handle mapping, otherwise SEGFAULT
|
265
279
|
d = 2 * r # diameter
|
266
280
|
self._temp_roi.setPos(self._roi_start_pos.x() - r, self._roi_start_pos.y() - r)
|
267
281
|
self._temp_roi.setSize([d, d])
|
282
|
+
elif self._roi_draw_mode == "ellipse":
|
283
|
+
# Safeguard: enforce a minimum ellipse width/height of 2 px
|
284
|
+
min_dim = 2.0
|
285
|
+
w = dx if abs(dx) >= min_dim else math.copysign(min_dim, dx or 1.0)
|
286
|
+
h = dy if abs(dy) >= min_dim else math.copysign(min_dim, dy or 1.0)
|
287
|
+
self._temp_roi.setSize([w, h])
|
268
288
|
return True
|
269
289
|
elif (
|
270
290
|
event.type() == QEvent.GraphicsSceneMouseRelease
|
@@ -750,6 +750,92 @@ class CircularROI(BaseROI, pg.CircleROI):
|
|
750
750
|
return None
|
751
751
|
|
752
752
|
|
753
|
+
class EllipticalROI(BaseROI, pg.EllipseROI):
|
754
|
+
"""
|
755
|
+
Elliptical Region of Interest with centre/width/height tracking and auto-labelling.
|
756
|
+
|
757
|
+
Mirrors the behaviour of ``CircularROI`` but supports independent
|
758
|
+
horizontal and vertical radii.
|
759
|
+
"""
|
760
|
+
|
761
|
+
centerChanged = Signal(float, float, float, float) # cx, cy, width, height
|
762
|
+
centerReleased = Signal(float, float, float, float)
|
763
|
+
|
764
|
+
def __init__(
|
765
|
+
self,
|
766
|
+
*,
|
767
|
+
pos,
|
768
|
+
size,
|
769
|
+
pen=None,
|
770
|
+
config: ConnectionConfig | None = None,
|
771
|
+
gui_id: str | None = None,
|
772
|
+
parent_image: Image | None = None,
|
773
|
+
label: str | None = None,
|
774
|
+
line_color: str | None = None,
|
775
|
+
line_width: int = 5,
|
776
|
+
movable: bool = True,
|
777
|
+
**extra_pg,
|
778
|
+
):
|
779
|
+
super().__init__(
|
780
|
+
config=config,
|
781
|
+
gui_id=gui_id,
|
782
|
+
parent_image=parent_image,
|
783
|
+
label=label,
|
784
|
+
line_color=line_color,
|
785
|
+
line_width=line_width,
|
786
|
+
pos=pos,
|
787
|
+
size=size,
|
788
|
+
pen=pen,
|
789
|
+
movable=movable,
|
790
|
+
**extra_pg,
|
791
|
+
)
|
792
|
+
|
793
|
+
self.sigRegionChanged.connect(self._on_region_changed)
|
794
|
+
self._adorner = LabelAdorner(self)
|
795
|
+
self.hoverPen = fn.mkPen(color=(255, 0, 0), width=3, style=QtCore.Qt.DashLine)
|
796
|
+
self.handleHoverPen = fn.mkPen("lime", width=4)
|
797
|
+
|
798
|
+
def add_scale_handle(self):
|
799
|
+
"""Add scale handles to the elliptical ROI."""
|
800
|
+
self._addHandles() # delegates to pg.EllipseROI
|
801
|
+
|
802
|
+
def _on_region_changed(self):
|
803
|
+
w = abs(self.state["size"][0])
|
804
|
+
h = abs(self.state["size"][1])
|
805
|
+
cx = self.pos().x() + w / 2
|
806
|
+
cy = self.pos().y() + h / 2
|
807
|
+
self.centerChanged.emit(cx, cy, w, h)
|
808
|
+
self.parent_plot_item.vb.update()
|
809
|
+
|
810
|
+
def mouseDragEvent(self, ev):
|
811
|
+
super().mouseDragEvent(ev)
|
812
|
+
if ev.isFinish():
|
813
|
+
w = abs(self.state["size"][0])
|
814
|
+
h = abs(self.state["size"][1])
|
815
|
+
cx = self.pos().x() + w / 2
|
816
|
+
cy = self.pos().y() + h / 2
|
817
|
+
self.centerReleased.emit(cx, cy, w, h)
|
818
|
+
|
819
|
+
def get_coordinates(self, typed: bool | None = None) -> dict | tuple:
|
820
|
+
"""
|
821
|
+
Return the ellipse's centre and size.
|
822
|
+
|
823
|
+
Args:
|
824
|
+
typed (bool | None): If True returns dict; otherwise tuple.
|
825
|
+
"""
|
826
|
+
if typed is None:
|
827
|
+
typed = self.description
|
828
|
+
|
829
|
+
w, h = map(abs, self.state["size"]) # raw diameters
|
830
|
+
major, minor = (w, h) if w >= h else (h, w)
|
831
|
+
cx = self.pos().x() + w / 2
|
832
|
+
cy = self.pos().y() + h / 2
|
833
|
+
|
834
|
+
if typed:
|
835
|
+
return {"center_x": cx, "center_y": cy, "major_axis": major, "minor_axis": minor}
|
836
|
+
return (cx, cy, major, minor)
|
837
|
+
|
838
|
+
|
753
839
|
class ROIController(QObject):
|
754
840
|
"""Manages a collection of ROIs (Regions of Interest) with palette-assigned colors.
|
755
841
|
|
@@ -2,11 +2,11 @@
|
|
2
2
|
.gitlab-ci.yml,sha256=1nMYldzVk0tFkBWYTcUjumOrdSADASheWOAc0kOFDYs,9509
|
3
3
|
.pylintrc,sha256=eeY8YwSI74oFfq6IYIbCqnx3Vk8ZncKaatv96n_Y8Rs,18544
|
4
4
|
.readthedocs.yaml,sha256=ivqg3HTaOxNbEW3bzWh9MXAkrekuGoNdj0Mj3SdRYuw,639
|
5
|
-
CHANGELOG.md,sha256=
|
5
|
+
CHANGELOG.md,sha256=n_bLGhoWteR3IlmPcheOc1NmwwuA9YT-U8wH9qdqvTc,300622
|
6
6
|
LICENSE,sha256=Daeiu871NcAp8uYi4eB_qHgvypG-HX0ioRQyQxFwjeg,1531
|
7
|
-
PKG-INFO,sha256=
|
7
|
+
PKG-INFO,sha256=UG0NtEIGEXUQb9P-vMk67mOQHLGP06_1SRQ3Hdf8D0U,1252
|
8
8
|
README.md,sha256=oY5Jc1uXehRASuwUJ0umin2vfkFh7tHF-LLruHTaQx0,3560
|
9
|
-
pyproject.toml,sha256=
|
9
|
+
pyproject.toml,sha256=A3AL_HaiNQUaOAmO1eBS6lIrkDjfJb8DIP-BwuqMeLw,2827
|
10
10
|
.git_hooks/pre-commit,sha256=n3RofIZHJl8zfJJIUomcMyYGFi_rwq4CC19z0snz3FI,286
|
11
11
|
.github/pull_request_template.md,sha256=F_cJXzooWMFgMGtLK-7KeGcQt0B4AYFse5oN0zQ9p6g,801
|
12
12
|
.github/ISSUE_TEMPLATE/bug_report.yml,sha256=WdRnt7HGxvsIBLzhkaOWNfg8IJQYa_oV9_F08Ym6znQ,1081
|
@@ -36,7 +36,7 @@ bec_widgets/assets/app_icons/bec_widgets_icon.png,sha256=K8dgGwIjalDh9PRHUsSQBqg
|
|
36
36
|
bec_widgets/assets/app_icons/ui_loader_tile.png,sha256=qSK3XHqvnAVGV9Q0ulORcGFbXJ9LDq2uz8l9uTtMsNk,1812476
|
37
37
|
bec_widgets/assets/app_icons/widget_launch_tile.png,sha256=bWsICHFfSe9-ESUj3AwlE95dDOea-f6M-s9fBapsxB4,2252911
|
38
38
|
bec_widgets/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
39
|
-
bec_widgets/cli/client.py,sha256=
|
39
|
+
bec_widgets/cli/client.py,sha256=Nlc9RodoiykU846Dy_HGfZmtI-fhwwCSwxU0dpu_Qg4,102491
|
40
40
|
bec_widgets/cli/client_utils.py,sha256=F2hyt--jL53bN8NoWifNUMqwwx5FbpS6I1apERdTRzM,18114
|
41
41
|
bec_widgets/cli/generate_cli.py,sha256=K_wMxo2XBUn92SnY3dSrlyUn8ax6Y20QBGCuP284DsQ,10986
|
42
42
|
bec_widgets/cli/server.py,sha256=h7QyBOOGjyrP_fxJIIOSEMc4E06cLG0JyaofjNV6oCA,5671
|
@@ -121,8 +121,9 @@ bec_widgets/widgets/containers/dock/register_dock_area.py,sha256=L7BL4qknCjtqsDP
|
|
121
121
|
bec_widgets/widgets/containers/layout_manager/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
122
122
|
bec_widgets/widgets/containers/layout_manager/layout_manager.py,sha256=V7s8mtB3VLPstyGVaR9YKcoTVlfMMOYNpIJUsw2WQVc,35198
|
123
123
|
bec_widgets/widgets/containers/main_window/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
124
|
-
bec_widgets/widgets/containers/main_window/main_window.py,sha256=
|
124
|
+
bec_widgets/widgets/containers/main_window/main_window.py,sha256=vp3KiHg0uCA2z9JeeV1ArFNYAdrWRWqDKhct8AOab4g,9169
|
125
125
|
bec_widgets/widgets/containers/main_window/addons/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
126
|
+
bec_widgets/widgets/containers/main_window/addons/scroll_label.py,sha256=ERZVE5J4uRyuizf9nbvnHAJZo2xGaVmgLBlrJaYpVSg,2943
|
126
127
|
bec_widgets/widgets/containers/main_window/addons/web_links.py,sha256=d5OgzgI9zb-NAC0pOGanOtJX3nZoe4x8QuQTw-_hK_8,434
|
127
128
|
bec_widgets/widgets/control/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
128
129
|
bec_widgets/widgets/control/buttons/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
@@ -259,14 +260,14 @@ bec_widgets/widgets/plots/plot_base.py,sha256=GETUsx51BE_Tuop8bC-KiFVrkR82TJ5S0c
|
|
259
260
|
bec_widgets/widgets/plots/image/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
260
261
|
bec_widgets/widgets/plots/image/image.py,sha256=wrI9C2xNEoQmCWoKjlSdRHroWHffIg_DFT4fNDTgAyE,20299
|
261
262
|
bec_widgets/widgets/plots/image/image.pyproject,sha256=_sRCIu4MNgToaB4D7tUMWq3xKX6T2VoRS3UzGNIseHQ,23
|
262
|
-
bec_widgets/widgets/plots/image/image_base.py,sha256=
|
263
|
+
bec_widgets/widgets/plots/image/image_base.py,sha256=q8b2kvUXsD6-6ld4r3TSMJEKYS158VzK-r88SIdB9Os,36549
|
263
264
|
bec_widgets/widgets/plots/image/image_item.py,sha256=rkL1o35Pgs1zhvv2wpSG1gt_bjP5kO4Z1oy6d2q-Yls,8577
|
264
265
|
bec_widgets/widgets/plots/image/image_plugin.py,sha256=R0Hzh2GgYlfZLPZwOMgqLKKIA5DxEnTcSrbI7zTe-tI,1204
|
265
266
|
bec_widgets/widgets/plots/image/image_processor.py,sha256=0qsQIyB__xxBwNIoXhFbB0wSiB8n7n_oX4cvfFsUzOs,4304
|
266
267
|
bec_widgets/widgets/plots/image/image_roi_plot.py,sha256=5CWy_eC-GS2ZLJTj2ItrVCoKhPN2g3fx6L4ktf5yVFQ,1191
|
267
268
|
bec_widgets/widgets/plots/image/register_image.py,sha256=0rvFyAMGRZcknc7nMVwsMGSfY8L2j9cdtQhbO5TMzeM,455
|
268
269
|
bec_widgets/widgets/plots/image/setting_widgets/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
269
|
-
bec_widgets/widgets/plots/image/setting_widgets/image_roi_tree.py,sha256=
|
270
|
+
bec_widgets/widgets/plots/image/setting_widgets/image_roi_tree.py,sha256=atfW9xOMqYtdmdnkch4P5cNCSFmx9nz7-o8PjOYed8I,17481
|
270
271
|
bec_widgets/widgets/plots/image/toolbar_bundles/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
271
272
|
bec_widgets/widgets/plots/image/toolbar_bundles/image_selection.py,sha256=ezs2TWZCz-3npbIFEiYuHNiuSvJptJznTEZWD_CX3po,4705
|
272
273
|
bec_widgets/widgets/plots/image/toolbar_bundles/processing.py,sha256=99hgd1q86ZUhQYTTsFCk3Ml8oAEeZJy-WqdmsMO4bzA,3367
|
@@ -291,7 +292,7 @@ bec_widgets/widgets/plots/multi_waveform/settings/multi_waveform_controls.ui,sha
|
|
291
292
|
bec_widgets/widgets/plots/multi_waveform/toolbar_bundles/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
292
293
|
bec_widgets/widgets/plots/multi_waveform/toolbar_bundles/monitor_selection.py,sha256=lGreAkRBd-A4X_wqYZiKyGDmb_3uzLunjSju9A2PjWw,2532
|
293
294
|
bec_widgets/widgets/plots/roi/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
294
|
-
bec_widgets/widgets/plots/roi/image_roi.py,sha256
|
295
|
+
bec_widgets/widgets/plots/roi/image_roi.py,sha256=-i-HWsBgP6Yt0Tnc2I5s21X0flsJ5zBDxjkoi55mJhU,37542
|
295
296
|
bec_widgets/widgets/plots/scatter_waveform/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
296
297
|
bec_widgets/widgets/plots/scatter_waveform/register_scatter_waveform.py,sha256=KttVjlAK3PfP9tyMfLnqEm6kphap8NZyqyaRry8oebY,514
|
297
298
|
bec_widgets/widgets/plots/scatter_waveform/scatter_curve.py,sha256=nCyZ_6EunS1m5XkLB-CwfBV9L4IX04D9SpHlHc8zG_I,6763
|
@@ -408,8 +409,8 @@ bec_widgets/widgets/utility/visual/dark_mode_button/dark_mode_button.py,sha256=O
|
|
408
409
|
bec_widgets/widgets/utility/visual/dark_mode_button/dark_mode_button.pyproject,sha256=Lbi9zb6HNlIq14k6hlzR-oz6PIFShBuF7QxE6d87d64,34
|
409
410
|
bec_widgets/widgets/utility/visual/dark_mode_button/dark_mode_button_plugin.py,sha256=CzChz2SSETYsR8-36meqWnsXCT-FIy_J_xeU5coWDY8,1350
|
410
411
|
bec_widgets/widgets/utility/visual/dark_mode_button/register_dark_mode_button.py,sha256=rMpZ1CaoucwobgPj1FuKTnt07W82bV1GaSYdoqcdMb8,521
|
411
|
-
bec_widgets-2.
|
412
|
-
bec_widgets-2.
|
413
|
-
bec_widgets-2.
|
414
|
-
bec_widgets-2.
|
415
|
-
bec_widgets-2.
|
412
|
+
bec_widgets-2.15.0.dist-info/METADATA,sha256=UG0NtEIGEXUQb9P-vMk67mOQHLGP06_1SRQ3Hdf8D0U,1252
|
413
|
+
bec_widgets-2.15.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
414
|
+
bec_widgets-2.15.0.dist-info/entry_points.txt,sha256=dItMzmwA1wizJ1Itx15qnfJ0ZzKVYFLVJ1voxT7K7D4,214
|
415
|
+
bec_widgets-2.15.0.dist-info/licenses/LICENSE,sha256=Daeiu871NcAp8uYi4eB_qHgvypG-HX0ioRQyQxFwjeg,1531
|
416
|
+
bec_widgets-2.15.0.dist-info/RECORD,,
|
pyproject.toml
CHANGED
File without changes
|
File without changes
|
File without changes
|