bec-widgets 2.16.2__py3-none-any.whl → 2.18.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. CHANGELOG.md +88 -0
  2. PKG-INFO +3 -3
  3. bec_widgets/applications/launch_window.py +1 -1
  4. bec_widgets/cli/client.py +24 -0
  5. bec_widgets/tests/utils.py +15 -3
  6. bec_widgets/utils/filter_io.py +125 -9
  7. bec_widgets/widgets/containers/main_window/main_window.py +122 -7
  8. bec_widgets/widgets/control/device_input/base_classes/device_input_base.py +16 -16
  9. bec_widgets/widgets/control/device_input/base_classes/device_signal_input_base.py +22 -12
  10. bec_widgets/widgets/control/device_input/device_combobox/device_combobox.py +2 -0
  11. bec_widgets/widgets/control/device_input/signal_combobox/signal_combobox.py +9 -5
  12. bec_widgets/widgets/plots/waveform/settings/curve_settings/curve_setting.py +7 -2
  13. bec_widgets/widgets/plots/waveform/settings/curve_settings/curve_tree.py +62 -19
  14. bec_widgets/widgets/plots/waveform/waveform.py +0 -1
  15. bec_widgets/widgets/progress/bec_progressbar/bec_progressbar.py +135 -21
  16. bec_widgets/widgets/progress/scan_progressbar/__init__.py +0 -0
  17. bec_widgets/widgets/progress/scan_progressbar/register_scan_progress_bar.py +17 -0
  18. bec_widgets/widgets/progress/scan_progressbar/scan_progress_bar.pyproject +1 -0
  19. bec_widgets/widgets/progress/scan_progressbar/scan_progress_bar_plugin.py +54 -0
  20. bec_widgets/widgets/progress/scan_progressbar/scan_progressbar.py +320 -0
  21. bec_widgets/widgets/progress/scan_progressbar/scan_progressbar.ui +141 -0
  22. bec_widgets/widgets/progress/scan_progressbar/scan_progressbar_one_line.ui +124 -0
  23. {bec_widgets-2.16.2.dist-info → bec_widgets-2.18.0.dist-info}/METADATA +3 -3
  24. {bec_widgets-2.16.2.dist-info → bec_widgets-2.18.0.dist-info}/RECORD +28 -21
  25. pyproject.toml +6 -6
  26. {bec_widgets-2.16.2.dist-info → bec_widgets-2.18.0.dist-info}/WHEEL +0 -0
  27. {bec_widgets-2.16.2.dist-info → bec_widgets-2.18.0.dist-info}/entry_points.txt +0 -0
  28. {bec_widgets-2.16.2.dist-info → bec_widgets-2.18.0.dist-info}/licenses/LICENSE +0 -0
CHANGELOG.md CHANGED
@@ -1,6 +1,94 @@
1
1
  # CHANGELOG
2
2
 
3
3
 
4
+ ## v2.18.0 (2025-06-22)
5
+
6
+ ### Bug Fixes
7
+
8
+ - Make settings dialog resizable
9
+ ([`5a564a5`](https://github.com/bec-project/bec_widgets/commit/5a564a5f3f3229e6407ea52a59d3e63319dc214a))
10
+
11
+ - **curve settings**: Add initial size hint
12
+ ([`a9708f6`](https://github.com/bec-project/bec_widgets/commit/a9708f6d8f15c42b142488da1e392a8f3179932a))
13
+
14
+ ### Features
15
+
16
+ - **curve settings**: Add combobox selection for device and signal
17
+ ([`eea5f7e`](https://github.com/bec-project/bec_widgets/commit/eea5f7ebbd2b477b3ed19c7efcc76390dd391f26))
18
+
19
+ - **device combobox**: Emit reset event if validation fails
20
+ ([`4c2c0c5`](https://github.com/bec-project/bec_widgets/commit/4c2c0c5525d593d8ec7fd554336cb11adbe32de2))
21
+
22
+ - **FilterIO**: Add support for item data
23
+ ([`8e8acd6`](https://github.com/bec-project/bec_widgets/commit/8e8acd672c0deb8dcd928886fb574452ac956de7))
24
+
25
+ - **signal combobox**: Add reset_selection slot
26
+ ([`b51de1a`](https://github.com/bec-project/bec_widgets/commit/b51de1a00e4b17c44cab23e5097391c6fa8ea0e2))
27
+
28
+ ### Refactoring
29
+
30
+ - **device input**: Refactor to SafeProperty and SafeSlot
31
+ ([`6e2f2ce`](https://github.com/bec-project/bec_widgets/commit/6e2f2cea91ba3af33e9891532506f4b0b65b90c8))
32
+
33
+
34
+ ## v2.17.0 (2025-06-22)
35
+
36
+ ### Bug Fixes
37
+
38
+ - **bec_progressbar**: Layout and sizing adjustments
39
+ ([`b02c870`](https://github.com/bec-project/bec_widgets/commit/b02c870dbfecb4bc6921ec4c915dac0e67beb9b4))
40
+
41
+ - **launch_window**: Number of remaining connections increase to 2 to include the ScanProgressBar
42
+ ([`3bbb8da`](https://github.com/bec-project/bec_widgets/commit/3bbb8daa24348613f62bde667a446d37dcec8fb0))
43
+
44
+ - **main_window**: Labels and sizing of scan progress adopted
45
+ ([`aca6efb`](https://github.com/bec-project/bec_widgets/commit/aca6efb567528eb3c68521a59b4f9903a5616c6f))
46
+
47
+ - **scan_progressbar**: Cleanup adjusted
48
+ ([`e8ae972`](https://github.com/bec-project/bec_widgets/commit/e8ae9725fa86b7db52a147ca5a2acc62fa2ccf43))
49
+
50
+ - **scan_progressbar**: Mapping of bec progress states to the progressbar enums
51
+ ([`88b42e4`](https://github.com/bec-project/bec_widgets/commit/88b42e49e30a0aa0edc2de4d970408f4be5bde6b))
52
+
53
+ ### Build System
54
+
55
+ - Update min dependency of bec to 3.42.4
56
+ ([`a4274ff`](https://github.com/bec-project/bec_widgets/commit/a4274ff8cd9f3e73a61b2eaf902c172c028d21b0))
57
+
58
+ ### Features
59
+
60
+ - **main_window**: Added scan progress bar to BECMainWindow status bar
61
+ ([`497e394`](https://github.com/bec-project/bec_widgets/commit/497e394deb5cfe36c8fc4f769fef26f109fd1c1f))
62
+
63
+ - **main_window**: Timer to show hide scan progress when it is relevant only
64
+ ([`9ff1706`](https://github.com/bec-project/bec_widgets/commit/9ff170660edd9e03f99eccee60b5e20fc1cf5a8d))
65
+
66
+ - **progressbar**: Added padding as designer property
67
+ ([`a451625`](https://github.com/bec-project/bec_widgets/commit/a451625a5ab804ca8259f9c9f83c4f9ebbea4a5b))
68
+
69
+ - **progressbar**: State setting and dynamic corner radius
70
+ ([`d3a9e09`](https://github.com/bec-project/bec_widgets/commit/d3a9e0903a263d735ecab3a2ad9319c9d5e86092))
71
+
72
+ - **scan_progressbar**: Added oneline design for compact applications
73
+ ([`d5ca7b8`](https://github.com/bec-project/bec_widgets/commit/d5ca7b84337cf60aa66f961d357ae66994f53c7a))
74
+
75
+ - **scan_progressbar**: Added progressbar with hooks to scan progress and device progress
76
+ ([`c4b8538`](https://github.com/bec-project/bec_widgets/commit/c4b85381a41e4742567680864668ee83d498b1d1))
77
+
78
+ ### Refactoring
79
+
80
+ - **progressbar**: Change slot / property to safeslot / safeproperty
81
+ ([`92d0ffe`](https://github.com/bec-project/bec_widgets/commit/92d0ffee65babc718fafd60131d0a4f291e5ca2b))
82
+
83
+ ### Testing
84
+
85
+ - **scan progress**: Add test for queue update logic
86
+ ([`b2a46e2`](https://github.com/bec-project/bec_widgets/commit/b2a46e284d45e97dd9853d1a3c8e95de7e530267))
87
+
88
+ - **scan_progress**: Tests extended
89
+ ([`6c04eac`](https://github.com/bec-project/bec_widgets/commit/6c04eac18c887526b333f58fc1118c3b4029abd8))
90
+
91
+
4
92
  ## v2.16.2 (2025-06-20)
5
93
 
6
94
  ### Bug Fixes
PKG-INFO CHANGED
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bec_widgets
3
- Version: 2.16.2
3
+ Version: 2.18.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
@@ -9,8 +9,8 @@ Classifier: Development Status :: 3 - Alpha
9
9
  Classifier: Programming Language :: Python :: 3
10
10
  Classifier: Topic :: Scientific/Engineering
11
11
  Requires-Python: >=3.10
12
- Requires-Dist: bec-ipython-client<=4.0,>=3.38
13
- Requires-Dist: bec-lib<=4.0,>=3.38
12
+ Requires-Dist: bec-ipython-client<=4.0,>=3.42.4
13
+ Requires-Dist: bec-lib<=4.0,>=3.42.4
14
14
  Requires-Dist: bec-qthemes>=0.7,~=0.7
15
15
  Requires-Dist: black~=25.0
16
16
  Requires-Dist: isort>=5.13.2,~=5.13
@@ -542,7 +542,7 @@ class LaunchWindow(BECMainWindow):
542
542
  remaining_connections = [
543
543
  connection for connection in connections.values() if connection.parent_id != self.gui_id
544
544
  ]
545
- return len(remaining_connections) <= 1
545
+ return len(remaining_connections) <= 2
546
546
 
547
547
  def _turn_off_the_lights(self, connections: dict):
548
548
  """
bec_widgets/cli/client.py CHANGED
@@ -474,6 +474,20 @@ class BECProgressBar(RPCBase):
474
474
  >>> progressbar.label_template = "$value / $percentage %"
475
475
  """
476
476
 
477
+ @property
478
+ @rpc_call
479
+ def state(self):
480
+ """
481
+ None
482
+ """
483
+
484
+ @state.setter
485
+ @rpc_call
486
+ def state(self):
487
+ """
488
+ None
489
+ """
490
+
477
491
  @rpc_call
478
492
  def _get_label(self) -> str:
479
493
  """
@@ -3245,6 +3259,16 @@ class ScanControl(RPCBase):
3245
3259
  """
3246
3260
 
3247
3261
 
3262
+ class ScanProgressBar(RPCBase):
3263
+ """Widget to display a progress bar that is hooked up to the scan progress of a scan."""
3264
+
3265
+ @rpc_call
3266
+ def remove(self):
3267
+ """
3268
+ Cleanup the BECConnector
3269
+ """
3270
+
3271
+
3248
3272
  class ScatterCurve(RPCBase):
3249
3273
  """Scatter curve item for the scatter waveform widget."""
3250
3274
 
@@ -96,9 +96,21 @@ class FakePositioner(BECPositioner):
96
96
  }
97
97
  self._info = {
98
98
  "signals": {
99
- "readback": {"kind_str": "hinted"}, # hinted
100
- "setpoint": {"kind_str": "normal"}, # normal
101
- "velocity": {"kind_str": "config"}, # config
99
+ "readback": {
100
+ "kind_str": "hinted",
101
+ "component_name": "readback",
102
+ "obj_name": self.name,
103
+ }, # hinted
104
+ "setpoint": {
105
+ "kind_str": "normal",
106
+ "component_name": "setpoint",
107
+ "obj_name": f"{self.name}_setpoint",
108
+ }, # normal
109
+ "velocity": {
110
+ "kind_str": "config",
111
+ "component_name": "velocity",
112
+ "obj_name": f"{self.name}_velocity",
113
+ }, # config
102
114
  }
103
115
  }
104
116
  self.signals = {
@@ -8,6 +8,8 @@ from bec_lib.logger import bec_logger
8
8
  from qtpy.QtCore import QStringListModel
9
9
  from qtpy.QtWidgets import QComboBox, QCompleter, QLineEdit
10
10
 
11
+ from bec_widgets.utils.ophyd_kind_util import Kind
12
+
11
13
  logger = bec_logger.logger
12
14
 
13
15
 
@@ -15,11 +17,13 @@ class WidgetFilterHandler(ABC):
15
17
  """Abstract base class for widget filter handlers"""
16
18
 
17
19
  @abstractmethod
18
- def set_selection(self, widget, selection: list) -> None:
20
+ def set_selection(self, widget, selection: list[str | tuple]) -> None:
19
21
  """Set the filtered_selection for the widget
20
22
 
21
23
  Args:
22
- selection (list): Filtered selection of items
24
+ widget: Widget instance
25
+ selection (list[str | tuple]): Filtered selection of items.
26
+ If tuple, it contains (text, data) pairs.
23
27
  """
24
28
 
25
29
  @abstractmethod
@@ -34,17 +38,37 @@ class WidgetFilterHandler(ABC):
34
38
  bool: True if the input text is in the filtered selection
35
39
  """
36
40
 
41
+ @abstractmethod
42
+ def update_with_kind(
43
+ self, kind: Kind, signal_filter: set, device_info: dict, device_name: str
44
+ ) -> list[str | tuple]:
45
+ """Update the selection based on the kind of signal.
46
+
47
+ Args:
48
+ kind (Kind): The kind of signal to filter.
49
+ signal_filter (set): Set of signal kinds to filter.
50
+ device_info (dict): Dictionary containing device information.
51
+ device_name (str): Name of the device.
52
+
53
+ Returns:
54
+ list[str | tuple]: A list of filtered signals based on the kind.
55
+ """
56
+ # This method should be implemented in subclasses or extended as needed
57
+
37
58
 
38
59
  class LineEditFilterHandler(WidgetFilterHandler):
39
60
  """Handler for QLineEdit widget"""
40
61
 
41
- def set_selection(self, widget: QLineEdit, selection: list) -> None:
62
+ def set_selection(self, widget: QLineEdit, selection: list[str | tuple]) -> None:
42
63
  """Set the selection for the widget to the completer model
43
64
 
44
65
  Args:
45
66
  widget (QLineEdit): The QLineEdit widget
46
- selection (list): Filtered selection of items
67
+ selection (list[str | tuple]): Filtered selection of items. If tuple, it contains (text, data) pairs.
47
68
  """
69
+ if isinstance(selection, tuple):
70
+ # If selection is a tuple, it contains (text, data) pairs
71
+ selection = [text for text, _ in selection]
48
72
  if not isinstance(widget.completer, QCompleter):
49
73
  completer = QCompleter(widget)
50
74
  widget.setCompleter(completer)
@@ -64,19 +88,47 @@ class LineEditFilterHandler(WidgetFilterHandler):
64
88
  model_data = [model.data(model.index(i)) for i in range(model.rowCount())]
65
89
  return text in model_data
66
90
 
91
+ def update_with_kind(
92
+ self, kind: Kind, signal_filter: set, device_info: dict, device_name: str
93
+ ) -> list[str | tuple]:
94
+ """Update the selection based on the kind of signal.
95
+
96
+ Args:
97
+ kind (Kind): The kind of signal to filter.
98
+ signal_filter (set): Set of signal kinds to filter.
99
+ device_info (dict): Dictionary containing device information.
100
+ device_name (str): Name of the device.
101
+
102
+ Returns:
103
+ list[str | tuple]: A list of filtered signals based on the kind.
104
+ """
105
+
106
+ return [
107
+ signal
108
+ for signal, signal_info in device_info.items()
109
+ if kind in signal_filter and (signal_info.get("kind_str", None) == str(kind.name))
110
+ ]
111
+
67
112
 
68
113
  class ComboBoxFilterHandler(WidgetFilterHandler):
69
114
  """Handler for QComboBox widget"""
70
115
 
71
- def set_selection(self, widget: QComboBox, selection: list) -> None:
116
+ def set_selection(self, widget: QComboBox, selection: list[str | tuple]) -> None:
72
117
  """Set the selection for the widget to the completer model
73
118
 
74
119
  Args:
75
120
  widget (QComboBox): The QComboBox widget
76
- selection (list): Filtered selection of items
121
+ selection (list[str | tuple]): Filtered selection of items. If tuple, it contains (text, data) pairs.
77
122
  """
78
123
  widget.clear()
79
- widget.addItems(selection)
124
+ if len(selection) == 0:
125
+ return
126
+ for element in selection:
127
+ if isinstance(element, str):
128
+ widget.addItem(element)
129
+ elif isinstance(element, tuple):
130
+ # If element is a tuple, it contains (text, data) pairs
131
+ widget.addItem(*element)
80
132
 
81
133
  def check_input(self, widget: QComboBox, text: str) -> bool:
82
134
  """Check if the input text is in the filtered selection
@@ -90,6 +142,40 @@ class ComboBoxFilterHandler(WidgetFilterHandler):
90
142
  """
91
143
  return text in [widget.itemText(i) for i in range(widget.count())]
92
144
 
145
+ def update_with_kind(
146
+ self, kind: Kind, signal_filter: set, device_info: dict, device_name: str
147
+ ) -> list[str | tuple]:
148
+ """Update the selection based on the kind of signal.
149
+
150
+ Args:
151
+ kind (Kind): The kind of signal to filter.
152
+ signal_filter (set): Set of signal kinds to filter.
153
+ device_info (dict): Dictionary containing device information.
154
+ device_name (str): Name of the device.
155
+
156
+ Returns:
157
+ list[str | tuple]: A list of filtered signals based on the kind.
158
+ """
159
+ out = []
160
+ for signal, signal_info in device_info.items():
161
+ if kind not in signal_filter or (signal_info.get("kind_str", None) != str(kind.name)):
162
+ continue
163
+ obj_name = signal_info.get("obj_name", "")
164
+ component_name = signal_info.get("component_name", "")
165
+ signal_wo_device = obj_name.removeprefix(f"{device_name}_")
166
+ if not signal_wo_device:
167
+ signal_wo_device = obj_name
168
+
169
+ if signal_wo_device != signal and component_name.replace(".", "_") != signal_wo_device:
170
+ # If the object name is not the same as the signal name, we use the object name
171
+ # to display in the combobox.
172
+ out.append((f"{signal_wo_device} ({signal})", signal_info))
173
+ else:
174
+ # If the object name is the same as the signal name, we do not change it.
175
+ out.append((signal, signal_info))
176
+
177
+ return out
178
+
93
179
 
94
180
  class FilterIO:
95
181
  """Public interface to set filters for input widgets.
@@ -99,13 +185,14 @@ class FilterIO:
99
185
  _handlers = {QLineEdit: LineEditFilterHandler, QComboBox: ComboBoxFilterHandler}
100
186
 
101
187
  @staticmethod
102
- def set_selection(widget, selection: list, ignore_errors=True):
188
+ def set_selection(widget, selection: list[str | tuple], ignore_errors=True):
103
189
  """
104
190
  Retrieve value from the widget instance.
105
191
 
106
192
  Args:
107
193
  widget: Widget instance.
108
- selection(list): List of filtered selection items.
194
+ selection (list[str | tuple]): Filtered selection of items.
195
+ If tuple, it contains (text, data) pairs.
109
196
  ignore_errors(bool, optional): Whether to ignore if no handler is found.
110
197
  """
111
198
  handler_class = FilterIO._find_handler(widget)
@@ -139,6 +226,35 @@ class FilterIO:
139
226
  )
140
227
  return None
141
228
 
229
+ @staticmethod
230
+ def update_with_kind(
231
+ widget, kind: Kind, signal_filter: set, device_info: dict, device_name: str
232
+ ) -> list[str | tuple]:
233
+ """
234
+ Update the selection based on the kind of signal.
235
+
236
+ Args:
237
+ widget: Widget instance.
238
+ kind (Kind): The kind of signal to filter.
239
+ signal_filter (set): Set of signal kinds to filter.
240
+ device_info (dict): Dictionary containing device information.
241
+ device_name (str): Name of the device.
242
+
243
+ Returns:
244
+ list[str | tuple]: A list of filtered signals based on the kind.
245
+ """
246
+ handler_class = FilterIO._find_handler(widget)
247
+ if handler_class:
248
+ return handler_class().update_with_kind(
249
+ kind=kind,
250
+ signal_filter=signal_filter,
251
+ device_info=device_info,
252
+ device_name=device_name,
253
+ )
254
+ raise ValueError(
255
+ f"No matching handler for widget type: {type(widget)} in handler list {FilterIO._handlers}"
256
+ )
257
+
142
258
  @staticmethod
143
259
  def _find_handler(widget):
144
260
  """
@@ -1,9 +1,28 @@
1
+ from __future__ import annotations
2
+
1
3
  import os
2
4
 
3
5
  from bec_lib.endpoints import MessageEndpoints
4
- from qtpy.QtCore import QEvent, QSize, Qt, QTimer
6
+ from qtpy.QtCore import (
7
+ QAbstractAnimation,
8
+ QEasingCurve,
9
+ QEvent,
10
+ QPropertyAnimation,
11
+ QSize,
12
+ Qt,
13
+ QTimer,
14
+ )
5
15
  from qtpy.QtGui import QAction, QActionGroup, QIcon
6
- from qtpy.QtWidgets import QApplication, QFrame, QLabel, QMainWindow, QStyle, QVBoxLayout, QWidget
16
+ from qtpy.QtWidgets import (
17
+ QApplication,
18
+ QFrame,
19
+ QHBoxLayout,
20
+ QLabel,
21
+ QMainWindow,
22
+ QStyle,
23
+ QVBoxLayout,
24
+ QWidget,
25
+ )
7
26
 
8
27
  import bec_widgets
9
28
  from bec_widgets.utils import UILoader
@@ -13,6 +32,7 @@ from bec_widgets.utils.error_popups import SafeSlot
13
32
  from bec_widgets.utils.widget_io import WidgetHierarchy
14
33
  from bec_widgets.widgets.containers.main_window.addons.scroll_label import ScrollLabel
15
34
  from bec_widgets.widgets.containers.main_window.addons.web_links import BECWebLinksMixin
35
+ from bec_widgets.widgets.progress.scan_progressbar.scan_progressbar import ScanProgressBar
16
36
 
17
37
  MODULE_PATH = os.path.dirname(bec_widgets.__file__)
18
38
 
@@ -20,6 +40,8 @@ MODULE_PATH = os.path.dirname(bec_widgets.__file__)
20
40
  class BECMainWindow(BECWidget, QMainWindow):
21
41
  RPC = False
22
42
  PLUGIN = False
43
+ SCAN_PROGRESS_WIDTH = 100 # px
44
+ STATUS_BAR_WIDGETS_EXPIRE_TIME = 60_000 # milliseconds
23
45
 
24
46
  def __init__(
25
47
  self,
@@ -33,6 +55,7 @@ class BECMainWindow(BECWidget, QMainWindow):
33
55
  super().__init__(parent=parent, gui_id=gui_id, **kwargs)
34
56
 
35
57
  self.app = QApplication.instance()
58
+ self.status_bar = self.statusBar()
36
59
  self.setWindowTitle(window_title)
37
60
  self._init_ui()
38
61
  self._connect_to_theme_change()
@@ -61,14 +84,13 @@ class BECMainWindow(BECWidget, QMainWindow):
61
84
  """
62
85
  Prepare the BEC specific widgets in the status bar.
63
86
  """
64
- status_bar = self.statusBar()
65
87
 
66
88
  # Left: App‑ID label
67
89
  self._app_id_label = QLabel()
68
90
  self._app_id_label.setAlignment(
69
91
  Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter
70
92
  )
71
- status_bar.addWidget(self._app_id_label)
93
+ self.status_bar.addWidget(self._app_id_label)
72
94
 
73
95
  # Add a separator after the app ID label
74
96
  self._add_separator()
@@ -78,16 +100,100 @@ class BECMainWindow(BECWidget, QMainWindow):
78
100
  self._client_info_label.setAlignment(
79
101
  Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter
80
102
  )
81
- status_bar.addWidget(self._client_info_label, 1)
103
+ self.status_bar.addWidget(self._client_info_label, 1)
82
104
 
83
105
  # Timer to automatically clear client messages once they expire
84
106
  self._client_info_expire_timer = QTimer(self)
85
107
  self._client_info_expire_timer.setSingleShot(True)
86
108
  self._client_info_expire_timer.timeout.connect(lambda: self._client_info_label.setText(""))
87
109
 
88
- def _add_separator(self):
110
+ # Add scan_progress bar with display logic
111
+ self._add_scan_progress_bar()
112
+
113
+ ################################################################################
114
+ # Progress‑bar helpers
115
+ def _add_scan_progress_bar(self):
116
+
117
+ # --- Progress bar -------------------------------------------------
118
+ # Scan progress bar minimalistic design setup
119
+ self._scan_progress_bar = ScanProgressBar(self, one_line_design=True)
120
+ self._scan_progress_bar.show_elapsed_time = False
121
+ self._scan_progress_bar.show_remaining_time = False
122
+ self._scan_progress_bar.show_source_label = False
123
+ self._scan_progress_bar.progressbar.label_template = ""
124
+ self._scan_progress_bar.progressbar.setFixedHeight(8)
125
+ self._scan_progress_bar.progressbar.setFixedWidth(80)
126
+
127
+ # Bundle the progress bar with a separator
128
+ separator = self._add_separator(separate_object=True)
129
+ self._scan_progress_bar_with_separator = QWidget()
130
+ self._scan_progress_bar_with_separator.layout = QHBoxLayout(
131
+ self._scan_progress_bar_with_separator
132
+ )
133
+ self._scan_progress_bar_with_separator.layout.setContentsMargins(0, 0, 0, 0)
134
+ self._scan_progress_bar_with_separator.layout.setSpacing(0)
135
+ self._scan_progress_bar_with_separator.layout.addWidget(separator)
136
+ self._scan_progress_bar_with_separator.layout.addWidget(self._scan_progress_bar)
137
+
138
+ # Set Size
139
+ self._scan_progress_bar_target_width = self.SCAN_PROGRESS_WIDTH
140
+ self._scan_progress_bar_with_separator.setMaximumWidth(self._scan_progress_bar_target_width)
141
+
142
+ self.status_bar.addWidget(self._scan_progress_bar_with_separator)
143
+
144
+ # Visibility logic
145
+ self._scan_progress_bar_with_separator.hide()
146
+ self._scan_progress_bar_with_separator.setMaximumWidth(0)
147
+
148
+ # Timer for hiding logic
149
+ self._scan_progress_hide_timer = QTimer(self)
150
+ self._scan_progress_hide_timer.setSingleShot(True)
151
+ self._scan_progress_hide_timer.setInterval(self.STATUS_BAR_WIDGETS_EXPIRE_TIME)
152
+ self._scan_progress_hide_timer.timeout.connect(self._animate_hide_scan_progress_bar)
153
+
154
+ # Show / hide behaviour
155
+ self._scan_progress_bar.progress_started.connect(self._show_scan_progress_bar)
156
+ self._scan_progress_bar.progress_finished.connect(self._delay_hide_scan_progress_bar)
157
+
158
+ def _show_scan_progress_bar(self):
159
+ if self._scan_progress_hide_timer.isActive():
160
+ self._scan_progress_hide_timer.stop()
161
+ if self._scan_progress_bar_with_separator.isVisible():
162
+ return
163
+
164
+ # Make visible and reset width
165
+ self._scan_progress_bar_with_separator.show()
166
+ self._scan_progress_bar_with_separator.setMaximumWidth(0)
167
+
168
+ self._show_container_anim = QPropertyAnimation(
169
+ self._scan_progress_bar_with_separator, b"maximumWidth", self
170
+ )
171
+ self._show_container_anim.setDuration(300)
172
+ self._show_container_anim.setStartValue(0)
173
+ self._show_container_anim.setEndValue(self._scan_progress_bar_target_width)
174
+ self._show_container_anim.setEasingCurve(QEasingCurve.OutCubic)
175
+ self._show_container_anim.start()
176
+
177
+ def _delay_hide_scan_progress_bar(self):
178
+ """Start the countdown to hide the scan progress bar."""
179
+ if hasattr(self, "_scan_progress_hide_timer"):
180
+ self._scan_progress_hide_timer.start()
181
+
182
+ def _animate_hide_scan_progress_bar(self):
183
+ """Shrink container to the right, then hide."""
184
+ self._hide_container_anim = QPropertyAnimation(
185
+ self._scan_progress_bar_with_separator, b"maximumWidth", self
186
+ )
187
+ self._hide_container_anim.setDuration(300)
188
+ self._hide_container_anim.setStartValue(self._scan_progress_bar_with_separator.width())
189
+ self._hide_container_anim.setEndValue(0)
190
+ self._hide_container_anim.setEasingCurve(QEasingCurve.InCubic)
191
+ self._hide_container_anim.finished.connect(self._scan_progress_bar_with_separator.hide)
192
+ self._hide_container_anim.start()
193
+
194
+ def _add_separator(self, separate_object: bool = False) -> QWidget | None:
89
195
  """
90
- Add a vertically centred separator to the status bar.
196
+ Add a vertically centred separator to the status bar or just return it as a separate object.
91
197
  """
92
198
  status_bar = self.statusBar()
93
199
 
@@ -106,6 +212,8 @@ class BECMainWindow(BECWidget, QMainWindow):
106
212
  vbox.addStretch()
107
213
  wrapper.setFixedWidth(line.sizeHint().width())
108
214
 
215
+ if separate_object:
216
+ return wrapper
109
217
  status_bar.addWidget(wrapper)
110
218
 
111
219
  def _init_bec_icon(self):
@@ -279,10 +387,16 @@ class BECMainWindow(BECWidget, QMainWindow):
279
387
  child.close()
280
388
  child.deleteLater()
281
389
 
390
+ # Timer cleanup
282
391
  if hasattr(self, "_client_info_expire_timer") and self._client_info_expire_timer.isActive():
283
392
  self._client_info_expire_timer.stop()
393
+ if hasattr(self, "_scan_progress_hide_timer") and self._scan_progress_hide_timer.isActive():
394
+ self._scan_progress_hide_timer.stop()
395
+
284
396
  # Status bar widgets cleanup
285
397
  self._client_info_label.cleanup()
398
+ self._scan_progress_bar.close()
399
+ self._scan_progress_bar.deleteLater()
286
400
  super().cleanup()
287
401
 
288
402
 
@@ -296,4 +410,5 @@ if __name__ == "__main__":
296
410
  app = QApplication(sys.argv)
297
411
  main_window = UILaunchWindow()
298
412
  main_window.show()
413
+ main_window.resize(800, 600)
299
414
  sys.exit(app.exec())