bec-widgets 0.76.1__py3-none-any.whl → 0.78.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 (44) hide show
  1. CHANGELOG.md +42 -48
  2. PKG-INFO +2 -1
  3. bec_widgets/cli/client.py +73 -196
  4. bec_widgets/examples/jupyter_console/jupyter_console_window.py +25 -4
  5. bec_widgets/utils/bec_connector.py +66 -8
  6. bec_widgets/utils/colors.py +38 -0
  7. bec_widgets/utils/yaml_dialog.py +27 -3
  8. bec_widgets/widgets/buttons/color_button/__init__.py +0 -0
  9. bec_widgets/widgets/buttons/color_button/assets/color_button.png +0 -0
  10. bec_widgets/widgets/buttons/color_button/color_button.py +17 -0
  11. bec_widgets/widgets/buttons/color_button/color_button.pyproject +1 -0
  12. bec_widgets/widgets/buttons/color_button/color_button_plugin.py +55 -0
  13. bec_widgets/widgets/buttons/color_button/register_color_button.py +15 -0
  14. bec_widgets/widgets/console/console.py +496 -0
  15. bec_widgets/widgets/dock/dock.py +2 -2
  16. bec_widgets/widgets/dock/dock_area.py +2 -2
  17. bec_widgets/widgets/figure/figure.py +149 -195
  18. bec_widgets/widgets/figure/plots/image/image.py +62 -49
  19. bec_widgets/widgets/figure/plots/image/image_item.py +4 -3
  20. bec_widgets/widgets/figure/plots/motor_map/motor_map.py +98 -29
  21. bec_widgets/widgets/figure/plots/plot_base.py +1 -1
  22. bec_widgets/widgets/figure/plots/waveform/waveform.py +7 -8
  23. bec_widgets/widgets/figure/plots/waveform/waveform_curve.py +2 -2
  24. bec_widgets/widgets/ring_progress_bar/ring.py +3 -3
  25. bec_widgets/widgets/ring_progress_bar/ring_progress_bar.py +3 -3
  26. {bec_widgets-0.76.1.dist-info → bec_widgets-0.78.0.dist-info}/METADATA +2 -1
  27. {bec_widgets-0.76.1.dist-info → bec_widgets-0.78.0.dist-info}/RECORD +44 -37
  28. pyproject.toml +2 -1
  29. tests/end-2-end/test_bec_dock_rpc_e2e.py +16 -16
  30. tests/end-2-end/test_bec_figure_rpc_e2e.py +7 -7
  31. tests/end-2-end/test_rpc_register_e2e.py +8 -8
  32. tests/unit_tests/client_mocks.py +1 -0
  33. tests/unit_tests/test_bec_figure.py +49 -26
  34. tests/unit_tests/test_bec_motor_map.py +179 -41
  35. tests/unit_tests/test_color_validation.py +15 -0
  36. tests/unit_tests/test_device_input_base.py +1 -1
  37. tests/unit_tests/test_device_input_widgets.py +2 -0
  38. tests/unit_tests/test_motor_control.py +5 -4
  39. tests/unit_tests/test_plot_base.py +3 -3
  40. tests/unit_tests/test_waveform1d.py +18 -17
  41. tests/unit_tests/test_yaml_dialog.py +7 -7
  42. {bec_widgets-0.76.1.dist-info → bec_widgets-0.78.0.dist-info}/WHEEL +0 -0
  43. {bec_widgets-0.76.1.dist-info → bec_widgets-0.78.0.dist-info}/entry_points.txt +0 -0
  44. {bec_widgets-0.76.1.dist-info → bec_widgets-0.78.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,15 +1,19 @@
1
1
  # pylint: disable = no-name-in-module,missing-module-docstring
2
2
  from __future__ import annotations
3
3
 
4
+ import os
4
5
  import time
6
+ import uuid
5
7
  from typing import Optional
6
8
 
9
+ import yaml
7
10
  from bec_lib.utils.import_utils import lazy_import_from
8
11
  from pydantic import BaseModel, Field, field_validator
9
12
  from qtpy.QtCore import QObject, QRunnable, QThreadPool, Signal
10
13
  from qtpy.QtCore import Slot as pyqtSlot
11
14
 
12
15
  from bec_widgets.cli.rpc_register import RPCRegister
16
+ from bec_widgets.utils.yaml_dialog import load_yaml, load_yaml_gui, save_yaml, save_yaml_gui
13
17
 
14
18
  BECDispatcher = lazy_import_from("bec_widgets.utils.bec_dispatcher", ("BECDispatcher",))
15
19
 
@@ -62,7 +66,7 @@ class Worker(QRunnable):
62
66
  class BECConnector:
63
67
  """Connection mixin class for all BEC widgets, to handle BEC client and device manager"""
64
68
 
65
- USER_ACCESS = ["config_dict", "get_all_rpc"]
69
+ USER_ACCESS = ["_config_dict", "_get_all_rpc"]
66
70
 
67
71
  def __init__(self, client=None, config: ConnectionConfig = None, gui_id: str = None):
68
72
  # BEC related connections
@@ -126,23 +130,23 @@ class BECConnector:
126
130
  self._thread_pool.start(worker)
127
131
  return worker
128
132
 
129
- def get_all_rpc(self) -> dict:
133
+ def _get_all_rpc(self) -> dict:
130
134
  """Get all registered RPC objects."""
131
135
  all_connections = self.rpc_register.list_all_connections()
132
136
  return dict(all_connections)
133
137
 
134
138
  @property
135
- def rpc_id(self) -> str:
139
+ def _rpc_id(self) -> str:
136
140
  """Get the RPC ID of the widget."""
137
141
  return self.gui_id
138
142
 
139
- @rpc_id.setter
140
- def rpc_id(self, rpc_id: str) -> None:
143
+ @_rpc_id.setter
144
+ def _rpc_id(self, rpc_id: str) -> None:
141
145
  """Set the RPC ID of the widget."""
142
146
  self.gui_id = rpc_id
143
147
 
144
148
  @property
145
- def config_dict(self) -> dict:
149
+ def _config_dict(self) -> dict:
146
150
  """
147
151
  Get the configuration of the widget.
148
152
 
@@ -151,8 +155,8 @@ class BECConnector:
151
155
  """
152
156
  return self.config.model_dump()
153
157
 
154
- @config_dict.setter
155
- def config_dict(self, config: BaseModel) -> None:
158
+ @_config_dict.setter
159
+ def _config_dict(self, config: BaseModel) -> None:
156
160
  """
157
161
  Get the configuration of the widget.
158
162
 
@@ -161,6 +165,60 @@ class BECConnector:
161
165
  """
162
166
  self.config = config
163
167
 
168
+ def apply_config(self, config: dict, generate_new_id: bool = True) -> None:
169
+ """
170
+ Apply the configuration to the widget.
171
+
172
+ Args:
173
+ config(dict): Configuration settings.
174
+ generate_new_id(bool): If True, generate a new GUI ID for the widget.
175
+ """
176
+ self.config = ConnectionConfig(**config)
177
+ if generate_new_id is True:
178
+ gui_id = str(uuid.uuid4())
179
+ self.rpc_register.remove_rpc(self)
180
+ self.set_gui_id(gui_id)
181
+ self.rpc_register.add_rpc(self)
182
+ else:
183
+ self.gui_id = self.config.gui_id
184
+
185
+ def load_config(self, path: str | None = None, gui: bool = False):
186
+ """
187
+ Load the configuration of the widget from YAML.
188
+
189
+ Args:
190
+ path(str): Path to the configuration file for non-GUI dialog mode.
191
+ gui(bool): If True, use the GUI dialog to load the configuration file.
192
+ """
193
+ if gui is True:
194
+ config = load_yaml_gui(self)
195
+ else:
196
+ config = load_yaml(path)
197
+
198
+ if config is not None:
199
+ if config.get("widget_class") != self.__class__.__name__:
200
+ raise ValueError(
201
+ f"Configuration file is not for {self.__class__.__name__}. Got configuration for {config.get('widget_class')}."
202
+ )
203
+ self.apply_config(config)
204
+
205
+ def save_config(self, path: str | None = None, gui: bool = False):
206
+ """
207
+ Save the configuration of the widget to YAML.
208
+
209
+ Args:
210
+ path(str): Path to save the configuration file for non-GUI dialog mode.
211
+ gui(bool): If True, use the GUI dialog to save the configuration file.
212
+ """
213
+ if gui is True:
214
+ save_yaml_gui(self, self._config_dict)
215
+ else:
216
+ if path is None:
217
+ path = os.getcwd()
218
+ file_path = os.path.join(path, f"{self.__class__.__name__}_config.yaml")
219
+
220
+ save_yaml(file_path, self._config_dict)
221
+
164
222
  @pyqtSlot(str)
165
223
  def set_gui_id(self, gui_id: str) -> None:
166
224
  """
@@ -67,6 +67,44 @@ class Colors:
67
67
  raise ValueError("Unsupported format. Please choose 'RGB', 'HEX', or 'QColor'.")
68
68
  return colors
69
69
 
70
+ @staticmethod
71
+ def hex_to_rgba(hex_color: str, alpha=255) -> tuple:
72
+ """
73
+ Convert HEX color to RGBA.
74
+
75
+ Args:
76
+ hex_color(str): HEX color string.
77
+ alpha(int): Alpha value (0-255). Default is 255 (opaque).
78
+
79
+ Returns:
80
+ tuple: RGBA color tuple (r, g, b, a).
81
+ """
82
+ hex_color = hex_color.lstrip("#")
83
+ if len(hex_color) == 6:
84
+ r, g, b = tuple(int(hex_color[i : i + 2], 16) for i in (0, 2, 4))
85
+ elif len(hex_color) == 8:
86
+ r, g, b, a = tuple(int(hex_color[i : i + 2], 16) for i in (0, 2, 4, 6))
87
+ return (r, g, b, a)
88
+ else:
89
+ raise ValueError("HEX color must be 6 or 8 characters long.")
90
+ return (r, g, b, alpha)
91
+
92
+ @staticmethod
93
+ def rgba_to_hex(r: int, g: int, b: int, a: int = 255) -> str:
94
+ """
95
+ Convert RGBA color to HEX.
96
+
97
+ Args:
98
+ r(int): Red value (0-255).
99
+ g(int): Green value (0-255).
100
+ b(int): Blue value (0-255).
101
+ a(int): Alpha value (0-255). Default is 255 (opaque).
102
+
103
+ Returns:
104
+ hec_color(str): HEX color string.
105
+ """
106
+ return "#{:02X}{:02X}{:02X}{:02X}".format(r, g, b, a)
107
+
70
108
  @staticmethod
71
109
  def validate_color(color: tuple | str) -> tuple | str:
72
110
  """
@@ -6,7 +6,7 @@ import yaml
6
6
  from qtpy.QtWidgets import QFileDialog
7
7
 
8
8
 
9
- def load_yaml(instance) -> Union[dict, None]:
9
+ def load_yaml_gui(instance) -> Union[dict, None]:
10
10
  """
11
11
  Load YAML file from disk.
12
12
 
@@ -20,12 +20,25 @@ def load_yaml(instance) -> Union[dict, None]:
20
20
  file_path, _ = QFileDialog.getOpenFileName(
21
21
  instance, "Load Settings", "", "YAML Files (*.yaml *.yml);;All Files (*)", options=options
22
22
  )
23
+ config = load_yaml(file_path)
24
+ return config
23
25
 
26
+
27
+ def load_yaml(file_path: str) -> Union[dict, None]:
28
+ """
29
+ Load YAML file from disk.
30
+
31
+ Args:
32
+ file_path(str): Path to the YAML file.
33
+
34
+ Returns:
35
+ dict: Configuration data loaded from the YAML file.
36
+ """
24
37
  if not file_path:
25
38
  return None
26
39
  try:
27
40
  with open(file_path, "r") as file:
28
- config = yaml.safe_load(file)
41
+ config = yaml.load(file, Loader=yaml.FullLoader)
29
42
  return config
30
43
 
31
44
  except FileNotFoundError:
@@ -38,7 +51,7 @@ def load_yaml(instance) -> Union[dict, None]:
38
51
  print(f"An error occurred while loading the settings from {file_path}: {e}")
39
52
 
40
53
 
41
- def save_yaml(instance, config: dict) -> None:
54
+ def save_yaml_gui(instance, config: dict) -> None:
42
55
  """
43
56
  Save YAML file to disk.
44
57
 
@@ -51,6 +64,17 @@ def save_yaml(instance, config: dict) -> None:
51
64
  instance, "Save Settings", "", "YAML Files (*.yaml *.yml);;All Files (*)", options=options
52
65
  )
53
66
 
67
+ save_yaml(file_path, config)
68
+
69
+
70
+ def save_yaml(file_path: str, config: dict) -> None:
71
+ """
72
+ Save YAML file to disk.
73
+
74
+ Args:
75
+ file_path(str): Path to the YAML file.
76
+ config(dict): Configuration data to be saved.
77
+ """
54
78
  if not file_path:
55
79
  return
56
80
  try:
File without changes
@@ -0,0 +1,17 @@
1
+ import pyqtgraph as pg
2
+
3
+
4
+ class ColorButton(pg.ColorButton):
5
+ """
6
+ A ColorButton that opens a dialog to select a color. Inherits from pyqtgraph.ColorButton.
7
+ Patches event loop of the ColorDialog, if opened in another QDialog.
8
+ """
9
+
10
+ def __init__(self, *args, **kwargs):
11
+ super().__init__(*args, **kwargs)
12
+
13
+ def selectColor(self):
14
+ self.origColor = self.color()
15
+ self.colorDialog.setCurrentColor(self.color())
16
+ self.colorDialog.open()
17
+ self.colorDialog.exec()
@@ -0,0 +1 @@
1
+ {'files': ['color_button.py']}
@@ -0,0 +1,55 @@
1
+ import os
2
+
3
+ from qtpy.QtDesigner import QDesignerCustomWidgetInterface
4
+ from qtpy.QtGui import QIcon
5
+
6
+ from bec_widgets.widgets.buttons.color_button.color_button import ColorButton
7
+
8
+ DOM_XML = """
9
+ <ui language='c++'>
10
+ <widget class='ColorButton' name='color_button'>
11
+ </widget>
12
+ </ui>
13
+ """
14
+
15
+
16
+ class ColorButtonPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
17
+ def __init__(self):
18
+ super().__init__()
19
+ self._form_editor = None
20
+
21
+ def createWidget(self, parent):
22
+ t = ColorButton(parent)
23
+ return t
24
+
25
+ def domXml(self):
26
+ return DOM_XML
27
+
28
+ def group(self):
29
+ return "BEC Buttons"
30
+
31
+ def icon(self):
32
+ current_path = os.path.dirname(__file__)
33
+ icon_path = os.path.join(current_path, "assets", "color_button.png")
34
+ return QIcon(icon_path)
35
+
36
+ def includeFile(self):
37
+ return "color_button"
38
+
39
+ def initialize(self, form_editor):
40
+ self._form_editor = form_editor
41
+
42
+ def isContainer(self):
43
+ return False
44
+
45
+ def isInitialized(self):
46
+ return self._form_editor is not None
47
+
48
+ def name(self):
49
+ return "ColorButton"
50
+
51
+ def toolTip(self):
52
+ return "ColorButton which opens a color dialog."
53
+
54
+ def whatsThis(self):
55
+ return self.toolTip()
@@ -0,0 +1,15 @@
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.buttons.color_button.color_button_plugin import ColorButtonPlugin
10
+
11
+ QPyDesignerCustomWidgetCollection.addCustomWidget(ColorButtonPlugin())
12
+
13
+
14
+ if __name__ == "__main__": # pragma: no cover
15
+ main()