bec-widgets 2.2.0__py3-none-any.whl → 2.4.3__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 (43) hide show
  1. .github/ISSUE_TEMPLATE/bug_report.md +26 -0
  2. .github/ISSUE_TEMPLATE/feature_request.md +48 -0
  3. .github/workflows/check_pr.yml +28 -0
  4. .github/workflows/ci.yml +36 -0
  5. .github/workflows/end2end-conda.yml +48 -0
  6. .github/workflows/formatter.yml +61 -0
  7. .github/workflows/generate-cli-check.yml +49 -0
  8. .github/workflows/pytest-matrix.yml +48 -0
  9. .github/workflows/pytest.yml +64 -0
  10. .github/workflows/semantic_release.yml +103 -0
  11. CHANGELOG.md +1720 -1545
  12. LICENSE +1 -1
  13. PKG-INFO +2 -1
  14. README.md +11 -0
  15. bec_widgets/cli/client.py +11 -0
  16. bec_widgets/tests/utils.py +3 -3
  17. bec_widgets/utils/bec_connector.py +11 -0
  18. bec_widgets/utils/entry_validator.py +13 -3
  19. bec_widgets/utils/side_panel.py +65 -39
  20. bec_widgets/utils/toolbar.py +79 -0
  21. bec_widgets/widgets/containers/layout_manager/layout_manager.py +34 -1
  22. bec_widgets/widgets/control/device_input/base_classes/device_input_base.py +1 -1
  23. bec_widgets/widgets/control/device_input/base_classes/device_signal_input_base.py +27 -31
  24. bec_widgets/widgets/control/device_input/device_combobox/device_combobox.py +1 -1
  25. bec_widgets/widgets/control/device_input/device_line_edit/device_line_edit.py +1 -1
  26. bec_widgets/widgets/editors/dict_backed_table.py +7 -0
  27. bec_widgets/widgets/editors/scan_metadata/scan_metadata.py +1 -0
  28. bec_widgets/widgets/editors/web_console/register_web_console.py +15 -0
  29. bec_widgets/widgets/editors/web_console/web_console.py +230 -0
  30. bec_widgets/widgets/editors/web_console/web_console.pyproject +1 -0
  31. bec_widgets/widgets/editors/web_console/web_console_plugin.py +54 -0
  32. bec_widgets/widgets/plots/waveform/settings/curve_settings/curve_tree.py +11 -46
  33. bec_widgets/widgets/utility/visual/color_button_native/__init__.py +0 -0
  34. bec_widgets/widgets/utility/visual/color_button_native/color_button_native.py +58 -0
  35. bec_widgets/widgets/utility/visual/color_button_native/color_button_native.pyproject +1 -0
  36. bec_widgets/widgets/utility/visual/color_button_native/color_button_native_plugin.py +56 -0
  37. bec_widgets/widgets/utility/visual/color_button_native/register_color_button_native.py +17 -0
  38. {bec_widgets-2.2.0.dist-info → bec_widgets-2.4.3.dist-info}/METADATA +2 -1
  39. {bec_widgets-2.2.0.dist-info → bec_widgets-2.4.3.dist-info}/RECORD +43 -24
  40. {bec_widgets-2.2.0.dist-info → bec_widgets-2.4.3.dist-info}/licenses/LICENSE +1 -1
  41. pyproject.toml +17 -5
  42. {bec_widgets-2.2.0.dist-info → bec_widgets-2.4.3.dist-info}/WHEEL +0 -0
  43. {bec_widgets-2.2.0.dist-info → bec_widgets-2.4.3.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,230 @@
1
+ from __future__ import annotations
2
+
3
+ import secrets
4
+ import subprocess
5
+ import time
6
+
7
+ from bec_lib.logger import bec_logger
8
+ from louie.saferef import safe_ref
9
+ from qtpy.QtCore import QUrl, qInstallMessageHandler
10
+ from qtpy.QtWebEngineWidgets import QWebEnginePage, QWebEngineView
11
+ from qtpy.QtWidgets import QApplication, QVBoxLayout, QWidget
12
+
13
+ from bec_widgets.utils.bec_widget import BECWidget
14
+
15
+ logger = bec_logger.logger
16
+
17
+
18
+ class WebConsoleRegistry:
19
+ """
20
+ A registry for the WebConsole class to manage its instances.
21
+ """
22
+
23
+ def __init__(self):
24
+ """
25
+ Initialize the registry.
26
+ """
27
+ self._instances = {}
28
+ self._server_process = None
29
+ self._server_port = None
30
+ self._token = secrets.token_hex(16)
31
+
32
+ def register(self, instance: WebConsole):
33
+ """
34
+ Register an instance of WebConsole.
35
+ """
36
+ self._instances[instance.gui_id] = safe_ref(instance)
37
+ self.cleanup()
38
+
39
+ if self._server_process is None:
40
+ # Start the ttyd server if not already running
41
+ self.start_ttyd()
42
+
43
+ def start_ttyd(self, use_zsh: bool | None = None):
44
+ """
45
+ Start the ttyd server
46
+ ttyd -q -W -t 'theme={"background": "black"}' zsh
47
+
48
+ Args:
49
+ use_zsh (bool): Whether to use zsh or bash. If None, it will try to detect if zsh is available.
50
+ """
51
+
52
+ # First, check if ttyd is installed
53
+ try:
54
+ subprocess.run(["ttyd", "--version"], check=True, stdout=subprocess.PIPE)
55
+ except FileNotFoundError:
56
+ # pylint: disable=raise-missing-from
57
+ raise RuntimeError("ttyd is not installed. Please install it first.")
58
+
59
+ if use_zsh is None:
60
+ # Check if we can use zsh
61
+ try:
62
+ subprocess.run(["zsh", "--version"], check=True, stdout=subprocess.PIPE)
63
+ use_zsh = True
64
+ except FileNotFoundError:
65
+ use_zsh = False
66
+
67
+ command = [
68
+ "ttyd",
69
+ "-p",
70
+ "0",
71
+ "-W",
72
+ "-t",
73
+ 'theme={"background": "black"}',
74
+ "-c",
75
+ f"user:{self._token}",
76
+ ]
77
+ if use_zsh:
78
+ command.append("zsh")
79
+ else:
80
+ command.append("bash")
81
+
82
+ # Start the ttyd server
83
+ self._server_process = subprocess.Popen(
84
+ command, stdout=subprocess.PIPE, stderr=subprocess.PIPE
85
+ )
86
+
87
+ self._wait_for_server_port()
88
+
89
+ self._server_process.stdout.close()
90
+ self._server_process.stderr.close()
91
+
92
+ def _wait_for_server_port(self, timeout: float = 10):
93
+ """
94
+ Wait for the ttyd server to start and get the port number.
95
+
96
+ Args:
97
+ timeout (float): The timeout in seconds to wait for the server to start.
98
+ """
99
+ start_time = time.time()
100
+ while True:
101
+ output = self._server_process.stderr.readline()
102
+ if output == b"" and self._server_process.poll() is not None:
103
+ break
104
+ if not output:
105
+ continue
106
+
107
+ output = output.decode("utf-8").strip()
108
+ if "Listening on" in output:
109
+ # Extract the port number from the output
110
+ self._server_port = int(output.split(":")[-1])
111
+ logger.info(f"ttyd server started on port {self._server_port}")
112
+ break
113
+ if time.time() - start_time > timeout:
114
+ raise TimeoutError(
115
+ "Timeout waiting for ttyd server to start. Please check if ttyd is installed and available in your PATH."
116
+ )
117
+
118
+ def cleanup(self):
119
+ """
120
+ Clean up the registry by removing any instances that are no longer valid.
121
+ """
122
+ for gui_id, weak_ref in list(self._instances.items()):
123
+ if weak_ref() is None:
124
+ del self._instances[gui_id]
125
+
126
+ if not self._instances and self._server_process:
127
+ # If no instances are left, terminate the server process
128
+ self._server_process.terminate()
129
+ self._server_process = None
130
+ self._server_port = None
131
+ logger.info("ttyd server terminated")
132
+
133
+ def unregister(self, instance: WebConsole):
134
+ """
135
+ Unregister an instance of WebConsole.
136
+
137
+ Args:
138
+ instance (WebConsole): The instance to unregister.
139
+ """
140
+ if instance.gui_id in self._instances:
141
+ del self._instances[instance.gui_id]
142
+
143
+ self.cleanup()
144
+
145
+
146
+ _web_console_registry = WebConsoleRegistry()
147
+
148
+
149
+ def suppress_qt_messages(type_, context, msg):
150
+ if context.category in ["js", "default"]:
151
+ return
152
+ print(msg)
153
+
154
+
155
+ qInstallMessageHandler(suppress_qt_messages)
156
+
157
+
158
+ class BECWebEnginePage(QWebEnginePage):
159
+ def javaScriptConsoleMessage(self, level, message, lineNumber, sourceID):
160
+ logger.info(f"[JS Console] {level.name} at line {lineNumber} in {sourceID}: {message}")
161
+
162
+
163
+ class WebConsole(BECWidget, QWidget):
164
+ """
165
+ A simple widget to display a website
166
+ """
167
+
168
+ PLUGIN = True
169
+ ICON_NAME = "terminal"
170
+
171
+ def __init__(self, parent=None, config=None, client=None, gui_id=None, **kwargs):
172
+ super().__init__(parent=parent, client=client, gui_id=gui_id, config=config, **kwargs)
173
+ _web_console_registry.register(self)
174
+ self._token = _web_console_registry._token
175
+ layout = QVBoxLayout()
176
+ layout.setContentsMargins(0, 0, 0, 0)
177
+ self.browser = QWebEngineView(self)
178
+ self.page = BECWebEnginePage(self)
179
+ self.page.authenticationRequired.connect(self._authenticate)
180
+ self.browser.setPage(self.page)
181
+ layout.addWidget(self.browser)
182
+ self.setLayout(layout)
183
+ self.page.setUrl(QUrl(f"http://localhost:{_web_console_registry._server_port}"))
184
+
185
+ def write(self, data: str, send_return: bool = True):
186
+ """
187
+ Send data to the web page
188
+ """
189
+ self.page.runJavaScript(f"window.term.paste('{data}');")
190
+ if send_return:
191
+ self.send_return()
192
+
193
+ def _authenticate(self, _, auth):
194
+ """
195
+ Authenticate the request with the provided username and password.
196
+ """
197
+ auth.setUser("user")
198
+ auth.setPassword(self._token)
199
+
200
+ def send_return(self):
201
+ """
202
+ Send return to the web page
203
+ """
204
+ self.page.runJavaScript(
205
+ "document.querySelector('textarea.xterm-helper-textarea').dispatchEvent(new KeyboardEvent('keypress', {charCode: 13}))"
206
+ )
207
+
208
+ def send_ctrl_c(self):
209
+ """
210
+ Send Ctrl+C to the web page
211
+ """
212
+ self.page.runJavaScript(
213
+ "document.querySelector('textarea.xterm-helper-textarea').dispatchEvent(new KeyboardEvent('keypress', {charCode: 3}))"
214
+ )
215
+
216
+ def cleanup(self):
217
+ """
218
+ Clean up the registry by removing any instances that are no longer valid.
219
+ """
220
+ _web_console_registry.unregister(self)
221
+ super().cleanup()
222
+
223
+
224
+ if __name__ == "__main__": # pragma: no cover
225
+ import sys
226
+
227
+ app = QApplication(sys.argv)
228
+ widget = WebConsole()
229
+ widget.show()
230
+ sys.exit(app.exec_())
@@ -0,0 +1 @@
1
+ {'files': ['web_console.py']}
@@ -0,0 +1,54 @@
1
+ # Copyright (C) 2022 The Qt Company Ltd.
2
+ # SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
3
+
4
+ from qtpy.QtDesigner import QDesignerCustomWidgetInterface
5
+
6
+ from bec_widgets.utils.bec_designer import designer_material_icon
7
+ from bec_widgets.widgets.editors.web_console.web_console import WebConsole
8
+
9
+ DOM_XML = """
10
+ <ui language='c++'>
11
+ <widget class='WebConsole' name='web_console'>
12
+ </widget>
13
+ </ui>
14
+ """
15
+
16
+
17
+ class WebConsolePlugin(QDesignerCustomWidgetInterface): # pragma: no cover
18
+ def __init__(self):
19
+ super().__init__()
20
+ self._form_editor = None
21
+
22
+ def createWidget(self, parent):
23
+ t = WebConsole(parent)
24
+ return t
25
+
26
+ def domXml(self):
27
+ return DOM_XML
28
+
29
+ def group(self):
30
+ return "BEC Console"
31
+
32
+ def icon(self):
33
+ return designer_material_icon(WebConsole.ICON_NAME)
34
+
35
+ def includeFile(self):
36
+ return "web_console"
37
+
38
+ def initialize(self, form_editor):
39
+ self._form_editor = form_editor
40
+
41
+ def isContainer(self):
42
+ return False
43
+
44
+ def isInitialized(self):
45
+ return self._form_editor is not None
46
+
47
+ def name(self):
48
+ return "WebConsole"
49
+
50
+ def toolTip(self):
51
+ return ""
52
+
53
+ def whatsThis(self):
54
+ return self.toolTip()
@@ -31,6 +31,9 @@ from bec_widgets.widgets.control.device_input.device_line_edit.device_line_edit
31
31
  )
32
32
  from bec_widgets.widgets.dap.dap_combo_box.dap_combo_box import DapComboBox
33
33
  from bec_widgets.widgets.plots.waveform.curve import CurveConfig, DeviceSignal
34
+ from bec_widgets.widgets.utility.visual.color_button_native.color_button_native import (
35
+ ColorButtonNative,
36
+ )
34
37
  from bec_widgets.widgets.utility.visual.colormap_widget.colormap_widget import BECColorMapWidget
35
38
 
36
39
  if TYPE_CHECKING: # pragma: no cover
@@ -40,49 +43,6 @@ if TYPE_CHECKING: # pragma: no cover
40
43
  logger = bec_logger.logger
41
44
 
42
45
 
43
- class ColorButton(QPushButton):
44
- """A QPushButton subclass that displays a color.
45
-
46
- The background is set to the given color and the button text is the hex code.
47
- The text color is chosen automatically (black if the background is light, white if dark)
48
- to guarantee good readability.
49
- """
50
-
51
- def __init__(self, color="#000000", parent=None):
52
- """Initialize the color button.
53
-
54
- Args:
55
- color (str): The initial color in hex format (e.g., '#000000').
56
- parent: Optional QWidget parent.
57
- """
58
- super().__init__(parent)
59
- self.set_color(color)
60
-
61
- def set_color(self, color):
62
- """Set the button's color and update its appearance.
63
-
64
- Args:
65
- color (str or QColor): The new color to assign.
66
- """
67
- if isinstance(color, QColor):
68
- self._color = color.name()
69
- else:
70
- self._color = color
71
- self._update_appearance()
72
-
73
- def color(self):
74
- """Return the current color in hex."""
75
- return self._color
76
-
77
- def _update_appearance(self):
78
- """Update the button style based on the background color's brightness."""
79
- c = QColor(self._color)
80
- brightness = c.lightnessF()
81
- text_color = "#000000" if brightness > 0.5 else "#FFFFFF"
82
- self.setStyleSheet(f"background-color: {self._color}; color: {text_color};")
83
- self.setText(self._color)
84
-
85
-
86
46
  class CurveRow(QTreeWidgetItem):
87
47
  DELETE_BUTTON_COLOR = "#CC181E"
88
48
  """A unified row that can represent either a device or a DAP curve.
@@ -193,7 +153,7 @@ class CurveRow(QTreeWidgetItem):
193
153
  def _init_style_controls(self):
194
154
  """Create columns 3..6: color button, style combo, width spin, symbol spin."""
195
155
  # Color in col 3
196
- self.color_button = ColorButton(self.config.color)
156
+ self.color_button = ColorButtonNative(color=self.config.color)
197
157
  self.color_button.clicked.connect(lambda: self._select_color(self.color_button))
198
158
  self.tree.setItemWidget(self, 3, self.color_button)
199
159
 
@@ -284,6 +244,11 @@ class CurveRow(QTreeWidgetItem):
284
244
  self.dap_combo.deleteLater()
285
245
  self.dap_combo = None
286
246
 
247
+ if getattr(self, "color_button", None) is not None:
248
+ self.color_button.close()
249
+ self.color_button.deleteLater()
250
+ self.color_button = None
251
+
287
252
  # Remove the item from the tree widget
288
253
  index = self.tree.indexOfTopLevelItem(self)
289
254
  if index != -1:
@@ -337,8 +302,8 @@ class CurveRow(QTreeWidgetItem):
337
302
  self.config.label = f"{parent_conf.label}-{new_dap}"
338
303
 
339
304
  # Common style fields
340
- self.config.color = self.color_button.color()
341
- self.config.symbol_color = self.color_button.color()
305
+ self.config.color = self.color_button.color
306
+ self.config.symbol_color = self.color_button.color
342
307
  self.config.pen_style = self.style_combo.currentText()
343
308
  self.config.pen_width = self.width_spin.value()
344
309
  self.config.symbol_size = self.symbol_spin.value()
@@ -0,0 +1,58 @@
1
+ from qtpy.QtGui import QColor
2
+ from qtpy.QtWidgets import QPushButton
3
+
4
+ from bec_widgets import BECWidget, SafeProperty, SafeSlot
5
+
6
+
7
+ class ColorButtonNative(BECWidget, QPushButton):
8
+ """A QPushButton subclass that displays a color.
9
+
10
+ The background is set to the given color and the button text is the hex code.
11
+ The text color is chosen automatically (black if the background is light, white if dark)
12
+ to guarantee good readability.
13
+ """
14
+
15
+ RPC = False
16
+ PLUGIN = True
17
+ ICON_NAME = "colors"
18
+
19
+ def __init__(self, parent=None, color="#000000", **kwargs):
20
+ """Initialize the color button.
21
+
22
+ Args:
23
+ parent: Optional QWidget parent.
24
+ color (str): The initial color in hex format (e.g., '#000000').
25
+ """
26
+ super().__init__(parent=parent, **kwargs)
27
+ self.set_color(color)
28
+
29
+ @SafeSlot()
30
+ def set_color(self, color):
31
+ """Set the button's color and update its appearance.
32
+
33
+ Args:
34
+ color (str or QColor): The new color to assign.
35
+ """
36
+ if isinstance(color, QColor):
37
+ self._color = color.name()
38
+ else:
39
+ self._color = color
40
+ self._update_appearance()
41
+
42
+ @SafeProperty("QColor")
43
+ def color(self):
44
+ """Return the current color in hex."""
45
+ return self._color
46
+
47
+ @color.setter
48
+ def color(self, value):
49
+ """Set the button's color and update its appearance."""
50
+ self.set_color(value)
51
+
52
+ def _update_appearance(self):
53
+ """Update the button style based on the background color's brightness."""
54
+ c = QColor(self._color)
55
+ brightness = c.lightnessF()
56
+ text_color = "#000000" if brightness > 0.5 else "#FFFFFF"
57
+ self.setStyleSheet(f"background-color: {self._color}; color: {text_color};")
58
+ self.setText(self._color)
@@ -0,0 +1 @@
1
+ {'files': ['color_button_native.py']}
@@ -0,0 +1,56 @@
1
+ # Copyright (C) 2022 The Qt Company Ltd.
2
+ # SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
3
+
4
+ from qtpy.QtDesigner import QDesignerCustomWidgetInterface
5
+
6
+ from bec_widgets.utils.bec_designer import designer_material_icon
7
+ from bec_widgets.widgets.utility.visual.color_button_native.color_button_native import (
8
+ ColorButtonNative,
9
+ )
10
+
11
+ DOM_XML = """
12
+ <ui language='c++'>
13
+ <widget class='ColorButtonNative' name='color_button_native'>
14
+ </widget>
15
+ </ui>
16
+ """
17
+
18
+
19
+ class ColorButtonNativePlugin(QDesignerCustomWidgetInterface): # pragma: no cover
20
+ def __init__(self):
21
+ super().__init__()
22
+ self._form_editor = None
23
+
24
+ def createWidget(self, parent):
25
+ t = ColorButtonNative(parent)
26
+ return t
27
+
28
+ def domXml(self):
29
+ return DOM_XML
30
+
31
+ def group(self):
32
+ return "BEC Buttons"
33
+
34
+ def icon(self):
35
+ return designer_material_icon(ColorButtonNative.ICON_NAME)
36
+
37
+ def includeFile(self):
38
+ return "color_button_native"
39
+
40
+ def initialize(self, form_editor):
41
+ self._form_editor = form_editor
42
+
43
+ def isContainer(self):
44
+ return False
45
+
46
+ def isInitialized(self):
47
+ return self._form_editor is not None
48
+
49
+ def name(self):
50
+ return "ColorButtonNative"
51
+
52
+ def toolTip(self):
53
+ return "A QPushButton subclass that displays a color."
54
+
55
+ def whatsThis(self):
56
+ return self.toolTip()
@@ -0,0 +1,17 @@
1
+ def main(): # pragma: no cover
2
+ from qtpy import PYSIDE6
3
+
4
+ if not PYSIDE6:
5
+ print("PYSIDE6 is not available in the environment. Cannot patch designer.")
6
+ return
7
+ from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
8
+
9
+ from bec_widgets.widgets.utility.visual.color_button_native.color_button_native_plugin import (
10
+ ColorButtonNativePlugin,
11
+ )
12
+
13
+ QPyDesignerCustomWidgetCollection.addCustomWidget(ColorButtonNativePlugin())
14
+
15
+
16
+ if __name__ == "__main__": # pragma: no cover
17
+ main()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bec_widgets
3
- Version: 2.2.0
3
+ Version: 2.4.3
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
@@ -25,6 +25,7 @@ Requires-Dist: coverage~=7.0; extra == 'dev'
25
25
  Requires-Dist: fakeredis>=2.23.2,~=2.23; extra == 'dev'
26
26
  Requires-Dist: isort>=5.13.2,~=5.13; extra == 'dev'
27
27
  Requires-Dist: pytest-bec-e2e<=4.0,>=2.21.4; extra == 'dev'
28
+ Requires-Dist: pytest-cov~=6.1.1; extra == 'dev'
28
29
  Requires-Dist: pytest-qt~=4.4; extra == 'dev'
29
30
  Requires-Dist: pytest-random-order~=1.1; extra == 'dev'
30
31
  Requires-Dist: pytest-timeout~=2.2; extra == 'dev'