bec-widgets 2.14.0__py3-none-any.whl → 2.15.1__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,41 @@
1
1
  # CHANGELOG
2
2
 
3
3
 
4
+ ## v2.15.1 (2025-06-16)
5
+
6
+ ### Bug Fixes
7
+
8
+ - **main_window**: Added expiration timer for scroll label for ClientInfoMessage
9
+ ([`187bf49`](https://github.com/bec-project/bec_widgets/commit/187bf493a5b18299a10939901b9ed7e308435092))
10
+
11
+ - **scroll_label**: Updating label during scrolling is done imminently, regardless scrolling
12
+ ([`1612933`](https://github.com/bec-project/bec_widgets/commit/1612933dd9689f2bf480ad81811c051201a9ff70))
13
+
14
+
15
+ ## v2.15.0 (2025-06-15)
16
+
17
+ ### Bug Fixes
18
+
19
+ - **main_window**: Central widget cleanup check to not remove None
20
+ ([`644be62`](https://github.com/bec-project/bec_widgets/commit/644be621f20cf09037da763f6217df9d1e4642bc))
21
+
22
+ ### Features
23
+
24
+ - **main_window**: Main window can display the messages from the send_client_info as a scrolling
25
+ horizontal text; closes #700
26
+ ([`0dec78a`](https://github.com/bec-project/bec_widgets/commit/0dec78afbaddbef98d20949d3a0ba4e0dc8529df))
27
+
28
+ ### Refactoring
29
+
30
+ - **main_window**: App id is displayed as QLabel instead of message
31
+ ([`57b9a57`](https://github.com/bec-project/bec_widgets/commit/57b9a57a631f267a8cb3622bf73035ffb15510e6))
32
+
33
+ ### Testing
34
+
35
+ - **main_window**: Becmainwindow tests extended
36
+ ([`30acc4c`](https://github.com/bec-project/bec_widgets/commit/30acc4c236bfbfed19f56512b264a52b4359e6c1))
37
+
38
+
4
39
  ## v2.14.0 (2025-06-13)
5
40
 
6
41
  ### 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.1
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,110 @@
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
+ """
29
+ Overridden to ensure that new text replaces the current one
30
+ immediately.
31
+ If the label was already scrolling (or in its delay phase),
32
+ the next message starts **without** the extra delay.
33
+ """
34
+ # Determine whether the widget was already in a scrolling cycle
35
+ was_scrolling = self._timer.isActive() or self._delay_timer.isActive()
36
+
37
+ super().setText(text)
38
+
39
+ fm = QFontMetrics(self.font())
40
+ self._text_width = fm.horizontalAdvance(text)
41
+ self._offset = 0
42
+
43
+ # Skip the delay when we were already scrolling
44
+ self._update_timer(skip_delay=was_scrolling)
45
+
46
+ def resizeEvent(self, event):
47
+ super().resizeEvent(event)
48
+ self._update_timer()
49
+
50
+ def _update_timer(self, *, skip_delay: bool = False):
51
+ """
52
+ Decide whether to start or stop scrolling.
53
+
54
+ If the text is wider than the visible area, start a single‑shot
55
+ delay timer (2s by default). Scrolling begins only after this
56
+ delay. Any change (resize or new text) restarts the logic.
57
+ """
58
+ needs_scroll = self._text_width > self.width()
59
+
60
+ if needs_scroll:
61
+ # Reset any running timers
62
+ if self._timer.isActive():
63
+ self._timer.stop()
64
+ if self._delay_timer.isActive():
65
+ self._delay_timer.stop()
66
+
67
+ self._offset = 0
68
+
69
+ # Start scrolling immediately when we should skip the delay,
70
+ # otherwise apply the configured delay_ms interval
71
+ if skip_delay:
72
+ self._timer.start()
73
+ else:
74
+ self._delay_timer.start()
75
+ else:
76
+ if self._delay_timer.isActive():
77
+ self._delay_timer.stop()
78
+ if self._timer.isActive():
79
+ self._timer.stop()
80
+ self.update()
81
+
82
+ def _scroll(self):
83
+ self._offset += self._step_px
84
+ if self._offset >= self._text_width:
85
+ self._offset = 0
86
+ self.update()
87
+
88
+ def paintEvent(self, event):
89
+ painter = QPainter(self)
90
+ painter.setRenderHint(QPainter.TextAntialiasing)
91
+ text = self.text()
92
+ if not text:
93
+ return
94
+ fm = QFontMetrics(self.font())
95
+ y = (self.height() + fm.ascent() - fm.descent()) // 2
96
+ if self._text_width <= self.width():
97
+ painter.drawText(0, y, text)
98
+ else:
99
+ x = -self._offset
100
+ gap = 50 # space between repeating text blocks
101
+ while x < self.width():
102
+ painter.drawText(x, y, text)
103
+ x += self._text_width + gap
104
+
105
+ def cleanup(self):
106
+ """Stop all timers to prevent memory leaks."""
107
+ if self._timer.isActive():
108
+ self._timer.stop()
109
+ if self._delay_timer.isActive():
110
+ 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, QTimer
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,77 @@ 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
+ # Timer to automatically clear client messages once they expire
84
+ self._client_info_expire_timer = QTimer(self)
85
+ self._client_info_expire_timer.setSingleShot(True)
86
+ self._client_info_expire_timer.timeout.connect(lambda: self._client_info_label.setText(""))
87
+
88
+ def _add_separator(self):
89
+ """
90
+ Add a vertically centred separator to the status bar.
91
+ """
92
+ status_bar = self.statusBar()
93
+
94
+ # The actual line
95
+ line = QFrame()
96
+ line.setFrameShape(QFrame.VLine)
97
+ line.setFrameShadow(QFrame.Sunken)
98
+ line.setFixedHeight(status_bar.sizeHint().height() - 2)
99
+
100
+ # Wrapper to center the line vertically -> work around for QFrame not being able to center itself
101
+ wrapper = QWidget()
102
+ vbox = QVBoxLayout(wrapper)
103
+ vbox.setContentsMargins(0, 0, 0, 0)
104
+ vbox.addStretch()
105
+ vbox.addWidget(line, alignment=Qt.AlignHCenter)
106
+ vbox.addStretch()
107
+ wrapper.setFixedWidth(line.sizeHint().width())
108
+
109
+ status_bar.addWidget(wrapper)
110
+
50
111
  def _init_bec_icon(self):
51
112
  icon = self.app.windowIcon()
52
113
  if icon.isNull():
53
- print("No icon is set, setting default icon")
54
114
  icon = QIcon()
55
115
  icon.addFile(
56
116
  os.path.join(MODULE_PATH, "assets", "app_icons", "bec_widgets_icon.png"),
57
117
  size=QSize(48, 48),
58
118
  )
59
119
  self.app.setWindowIcon(icon)
60
- else:
61
- print("An icon is set")
62
120
 
63
121
  def load_ui(self, ui_file):
64
122
  loader = UILoader(self)
65
123
  self.ui = loader.loader(ui_file)
66
124
  self.setCentralWidget(self.ui)
67
125
 
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
126
  def _fetch_theme(self) -> str:
81
127
  return self.app.theme.theme
82
128
 
@@ -164,8 +210,52 @@ class BECMainWindow(BECWidget, QMainWindow):
164
210
  help_menu.addAction(widgets_docs)
165
211
  help_menu.addAction(bug_report)
166
212
 
213
+ ################################################################################
214
+ # Status Bar Addons
215
+ ################################################################################
216
+ def display_app_id(self):
217
+ """
218
+ Display the app ID in the status bar.
219
+ """
220
+ if self.bec_dispatcher.cli_server is None:
221
+ status_message = "Not connected"
222
+ else:
223
+ # Get the server ID from the dispatcher
224
+ server_id = self.bec_dispatcher.cli_server.gui_id
225
+ status_message = f"App ID: {server_id}"
226
+ self._app_id_label.setText(status_message)
227
+
228
+ @SafeSlot(dict, dict)
229
+ def display_client_message(self, msg: dict, meta: dict):
230
+ """
231
+ Display a client message in the status bar.
232
+
233
+ Args:
234
+ msg(dict): The message to display, should contain:
235
+ meta(dict): Metadata about the message, usually empty.
236
+ """
237
+ # self._client_info_label.setText("")
238
+ message = msg.get("message", "")
239
+ expiration = msg.get("expire", 0) # 0 → never expire
240
+ self._client_info_label.setText(message)
241
+
242
+ # Restart the expiration timer if necessary
243
+ if hasattr(self, "_client_info_expire_timer") and self._client_info_expire_timer.isActive():
244
+ self._client_info_expire_timer.stop()
245
+ if expiration and expiration > 0:
246
+ self._client_info_expire_timer.start(int(expiration * 1000))
247
+
248
+ ################################################################################
249
+ # General and Cleanup Methods
250
+ ################################################################################
167
251
  @SafeSlot(str)
168
252
  def change_theme(self, theme: str):
253
+ """
254
+ Change the theme of the application.
255
+
256
+ Args:
257
+ theme(str): The theme to apply, either "light" or "dark".
258
+ """
169
259
  apply_theme(theme)
170
260
 
171
261
  def event(self, event):
@@ -175,8 +265,9 @@ class BECMainWindow(BECWidget, QMainWindow):
175
265
 
176
266
  def cleanup(self):
177
267
  central_widget = self.centralWidget()
178
- central_widget.close()
179
- central_widget.deleteLater()
268
+ if central_widget is not None:
269
+ central_widget.close()
270
+ central_widget.deleteLater()
180
271
  if not isinstance(central_widget, BECWidget):
181
272
  # if the central widget is not a BECWidget, we need to call the cleanup method
182
273
  # of all widgets whose parent is the current BECMainWindow
@@ -187,8 +278,22 @@ class BECMainWindow(BECWidget, QMainWindow):
187
278
  child.cleanup()
188
279
  child.close()
189
280
  child.deleteLater()
281
+
282
+ if hasattr(self, "_client_info_expire_timer") and self._client_info_expire_timer.isActive():
283
+ self._client_info_expire_timer.stop()
284
+ # Status bar widgets cleanup
285
+ self._client_info_label.cleanup()
190
286
  super().cleanup()
191
287
 
192
288
 
193
289
  class UILaunchWindow(BECMainWindow):
194
290
  RPC = True
291
+
292
+
293
+ if __name__ == "__main__":
294
+ import sys
295
+
296
+ app = QApplication(sys.argv)
297
+ main_window = UILaunchWindow()
298
+ main_window.show()
299
+ sys.exit(app.exec())
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bec_widgets
3
- Version: 2.14.0
3
+ Version: 2.15.1
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=BGd-zRzWE7KTMPqaa3CPc_JyROlDYD5DZ95PQMUjKzQ,301055
6
6
  LICENSE,sha256=Daeiu871NcAp8uYi4eB_qHgvypG-HX0ioRQyQxFwjeg,1531
7
- PKG-INFO,sha256=jremuY9vBAc7Gqo3FzN0bTR8fSa4zQGwMYTfzfmEYDg,1252
7
+ PKG-INFO,sha256=gTntcRGnhQPrg5vyZPQgQOjlhlKK-Y5r__82UIsiJXA,1252
8
8
  README.md,sha256=oY5Jc1uXehRASuwUJ0umin2vfkFh7tHF-LLruHTaQx0,3560
9
- pyproject.toml,sha256=2TeVnTuAoLHdPUi8NCUhJDAL3FStOjF7eYp3BDbL4i8,2827
9
+ pyproject.toml,sha256=HMdAYHc2bpPYaYYmgAWgbC3HiiptNXdnHyTMOQmdXg8,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=-qDkmL-DO6wJV1EnZdauGXzAh_nwRJJPWed1eSJR80Y,10425
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=RDiLWDkIrXFAdCXNMm6eE6U9h3zjfPhJDqVwdV8aSL8,3743
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.1.dist-info/METADATA,sha256=gTntcRGnhQPrg5vyZPQgQOjlhlKK-Y5r__82UIsiJXA,1252
413
+ bec_widgets-2.15.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
414
+ bec_widgets-2.15.1.dist-info/entry_points.txt,sha256=dItMzmwA1wizJ1Itx15qnfJ0ZzKVYFLVJ1voxT7K7D4,214
415
+ bec_widgets-2.15.1.dist-info/licenses/LICENSE,sha256=Daeiu871NcAp8uYi4eB_qHgvypG-HX0ioRQyQxFwjeg,1531
416
+ bec_widgets-2.15.1.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.1"
8
8
  description = "BEC Widgets"
9
9
  requires-python = ">=3.10"
10
10
  classifiers = [