bec-widgets 2.14.0__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 CHANGED
@@ -1,6 +1,30 @@
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
+
4
28
  ## v2.14.0 (2025-06-13)
5
29
 
6
30
  ### Features
PKG-INFO CHANGED
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bec_widgets
3
- Version: 2.14.0
3
+ Version: 2.15.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
@@ -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 qtpy.QtCore import QEvent, QSize
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.close()
179
- central_widget.deleteLater()
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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bec_widgets
3
- Version: 2.14.0
3
+ Version: 2.15.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
@@ -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=8Q0rbZR9aGHaASYpr06r4ZzGjuVL3ANdmGddlS-VCaQ,299790
5
+ CHANGELOG.md,sha256=n_bLGhoWteR3IlmPcheOc1NmwwuA9YT-U8wH9qdqvTc,300622
6
6
  LICENSE,sha256=Daeiu871NcAp8uYi4eB_qHgvypG-HX0ioRQyQxFwjeg,1531
7
- PKG-INFO,sha256=jremuY9vBAc7Gqo3FzN0bTR8fSa4zQGwMYTfzfmEYDg,1252
7
+ PKG-INFO,sha256=UG0NtEIGEXUQb9P-vMk67mOQHLGP06_1SRQ3Hdf8D0U,1252
8
8
  README.md,sha256=oY5Jc1uXehRASuwUJ0umin2vfkFh7tHF-LLruHTaQx0,3560
9
- pyproject.toml,sha256=2TeVnTuAoLHdPUi8NCUhJDAL3FStOjF7eYp3BDbL4i8,2827
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
@@ -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=1cqsQ4uo63RBuQnZzpw1cWf10b9VgQvXgq4UQPEnhKo,6336
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
@@ -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.14.0.dist-info/METADATA,sha256=jremuY9vBAc7Gqo3FzN0bTR8fSa4zQGwMYTfzfmEYDg,1252
412
- bec_widgets-2.14.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
413
- bec_widgets-2.14.0.dist-info/entry_points.txt,sha256=dItMzmwA1wizJ1Itx15qnfJ0ZzKVYFLVJ1voxT7K7D4,214
414
- bec_widgets-2.14.0.dist-info/licenses/LICENSE,sha256=Daeiu871NcAp8uYi4eB_qHgvypG-HX0ioRQyQxFwjeg,1531
415
- bec_widgets-2.14.0.dist-info/RECORD,,
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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "bec_widgets"
7
- version = "2.14.0"
7
+ version = "2.15.0"
8
8
  description = "BEC Widgets"
9
9
  requires-python = ">=3.10"
10
10
  classifiers = [