bec-widgets 0.69.0__py3-none-any.whl → 0.71.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 (39) hide show
  1. CHANGELOG.md +64 -87
  2. PKG-INFO +1 -1
  3. bec_widgets/cli/client.py +19 -0
  4. bec_widgets/examples/plugin_example_pyside/__init__.py +0 -0
  5. bec_widgets/examples/plugin_example_pyside/main.py +17 -0
  6. bec_widgets/examples/plugin_example_pyside/registertictactoe.py +12 -0
  7. bec_widgets/examples/plugin_example_pyside/taskmenuextension.pyproject +4 -0
  8. bec_widgets/examples/plugin_example_pyside/tictactoe.py +135 -0
  9. bec_widgets/examples/plugin_example_pyside/tictactoeplugin.py +68 -0
  10. bec_widgets/examples/plugin_example_pyside/tictactoetaskmenu.py +67 -0
  11. bec_widgets/utils/bec_designer.py +87 -0
  12. bec_widgets/utils/widget_io.py +18 -2
  13. bec_widgets/widgets/device_inputs/device_combobox/device_combobox.py +10 -13
  14. bec_widgets/widgets/device_inputs/device_combobox/device_combobox.pyproject +4 -0
  15. bec_widgets/widgets/device_inputs/device_combobox/device_combobox_plugin.py +54 -0
  16. bec_widgets/widgets/device_inputs/device_combobox/launch_device_combobox.py +11 -0
  17. bec_widgets/widgets/device_inputs/device_combobox/register_device_combobox.py +17 -0
  18. bec_widgets/widgets/device_inputs/device_input_base.py +5 -2
  19. bec_widgets/widgets/device_inputs/device_line_edit/device_line_edit.py +16 -14
  20. bec_widgets/widgets/device_inputs/device_line_edit/device_line_edit.pyproject +4 -0
  21. bec_widgets/widgets/device_inputs/device_line_edit/device_line_edit_plugin.py +54 -0
  22. bec_widgets/widgets/device_inputs/device_line_edit/launch_device_line_edit.py +11 -0
  23. bec_widgets/widgets/device_inputs/device_line_edit/register_device_line_edit.py +17 -0
  24. bec_widgets/widgets/scan_control/scan_control.py +133 -365
  25. bec_widgets/widgets/scan_control/scan_group_box.py +223 -0
  26. {bec_widgets-0.69.0.dist-info → bec_widgets-0.71.0.dist-info}/METADATA +1 -1
  27. {bec_widgets-0.69.0.dist-info → bec_widgets-0.71.0.dist-info}/RECORD +39 -18
  28. {bec_widgets-0.69.0.dist-info → bec_widgets-0.71.0.dist-info}/WHEEL +1 -1
  29. {bec_widgets-0.69.0.dist-info → bec_widgets-0.71.0.dist-info}/entry_points.txt +1 -0
  30. docs/user/widgets/bec_status_box.md +1 -1
  31. docs/user/widgets/scan_control.gif +0 -0
  32. docs/user/widgets/scan_control.md +35 -0
  33. pyproject.toml +2 -1
  34. tests/end-2-end/test_scan_control_e2e.py +71 -0
  35. tests/unit_tests/test_device_input_base.py +4 -4
  36. tests/unit_tests/test_device_input_widgets.py +10 -10
  37. tests/unit_tests/test_scan_control.py +255 -115
  38. tests/unit_tests/test_scan_control_group_box.py +160 -0
  39. {bec_widgets-0.69.0.dist-info → bec_widgets-0.71.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,53 +1,37 @@
1
+ import qdarktheme
1
2
  from bec_lib.endpoints import MessageEndpoints
2
3
  from qtpy.QtWidgets import (
3
4
  QApplication,
4
- QCheckBox,
5
5
  QComboBox,
6
- QDoubleSpinBox,
7
- QFrame,
8
6
  QGridLayout,
9
7
  QGroupBox,
10
- QHBoxLayout,
11
- QHeaderView,
12
- QLabel,
13
- QLayout,
14
- QLineEdit,
15
8
  QPushButton,
16
- QSpinBox,
17
- QTableWidget,
18
- QTableWidgetItem,
9
+ QSizePolicy,
19
10
  QVBoxLayout,
20
11
  QWidget,
21
12
  )
22
13
 
23
- from bec_widgets.utils.bec_dispatcher import BECDispatcher
24
- from bec_widgets.utils.widget_io import WidgetIO
14
+ from bec_widgets.utils import BECConnector
15
+ from bec_widgets.widgets.buttons.stop_button.stop_button import StopButton
16
+ from bec_widgets.widgets.scan_control.scan_group_box import ScanGroupBox
25
17
 
26
18
 
27
- class ScanArgType:
28
- DEVICE = "device"
29
- FLOAT = "float"
30
- INT = "int"
31
- BOOL = "bool"
32
- STR = "str"
19
+ class ScanControl(BECConnector, QWidget):
33
20
 
34
-
35
- class ScanControl(QWidget):
36
- WIDGET_HANDLER = {
37
- ScanArgType.DEVICE: QLineEdit,
38
- ScanArgType.FLOAT: QDoubleSpinBox,
39
- ScanArgType.INT: QSpinBox,
40
- ScanArgType.BOOL: QCheckBox,
41
- ScanArgType.STR: QLineEdit,
42
- }
43
-
44
- def __init__(self, parent=None, client=None, allowed_scans=None):
45
- super().__init__(parent)
21
+ def __init__(
22
+ self, parent=None, client=None, gui_id: str | None = None, allowed_scans: list | None = None
23
+ ):
24
+ super().__init__(client=client, gui_id=gui_id)
25
+ QWidget.__init__(self, parent=parent)
46
26
 
47
27
  # Client from BEC + shortcuts to device manager and scans
48
- self.client = BECDispatcher().client if client is None else client
49
- self.dev = self.client.device_manager.devices
50
- self.scans = self.client.scans
28
+ self.get_bec_shortcuts()
29
+
30
+ # Main layout
31
+ self.layout = QVBoxLayout(self)
32
+ self.arg_box = None
33
+ self.kwarg_boxes = []
34
+ self.expert_mode = False # TODO implement in the future versions
51
35
 
52
36
  # Scan list - allowed scans for the GUI
53
37
  self.allowed_scans = allowed_scans
@@ -56,389 +40,173 @@ class ScanControl(QWidget):
56
40
  self._init_UI()
57
41
 
58
42
  def _init_UI(self):
59
- self.verticalLayout = QVBoxLayout(self)
43
+ """
44
+ Initializes the UI of the scan control widget. Create the top box for scan selection and populate scans to main combobox.
45
+ """
60
46
 
61
47
  # Scan selection group box
62
- self.scan_selection_group = QGroupBox("Scan Selection", self)
63
- self.scan_selection_layout = QVBoxLayout(self.scan_selection_group)
64
- self.comboBox_scan_selection = QComboBox(self.scan_selection_group)
65
- self.button_run_scan = QPushButton("Run Scan", self.scan_selection_group)
66
- self.scan_selection_layout.addWidget(self.comboBox_scan_selection)
67
- self.scan_selection_layout.addWidget(self.button_run_scan)
68
- self.verticalLayout.addWidget(self.scan_selection_group)
69
-
70
- # Scan control group box
71
- self.scan_control_group = QGroupBox("Scan Control", self)
72
- self.scan_control_layout = QVBoxLayout(self.scan_control_group)
73
- self.verticalLayout.addWidget(self.scan_control_group)
74
-
75
- # Kwargs layout - just placeholder
76
- self.kwargs_layout = QGridLayout()
77
- self.scan_control_layout.addLayout(self.kwargs_layout)
78
-
79
- # 1st Separator
80
- self.add_horizontal_separator(self.scan_control_layout)
81
-
82
- # Buttons
83
- self.button_layout = QHBoxLayout()
84
- self.pushButton_add_bundle = QPushButton("Add Bundle", self.scan_control_group)
85
- self.pushButton_add_bundle.clicked.connect(self.add_bundle)
86
- self.pushButton_remove_bundle = QPushButton("Remove Bundle", self.scan_control_group)
87
- self.pushButton_remove_bundle.clicked.connect(self.remove_bundle)
88
- self.button_layout.addWidget(self.pushButton_add_bundle)
89
- self.button_layout.addWidget(self.pushButton_remove_bundle)
90
- self.scan_control_layout.addLayout(self.button_layout)
91
-
92
- # 2nd Separator
93
- self.add_horizontal_separator(self.scan_control_layout)
94
-
95
- # Initialize the QTableWidget for args
96
- self.args_table = QTableWidget()
97
- self.args_table.verticalHeader().setSectionResizeMode(QHeaderView.Fixed)
98
-
99
- self.scan_control_layout.addWidget(self.args_table)
48
+ self.scan_selection_group = self.create_scan_selection_group()
49
+ self.scan_selection_group.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
50
+ self.layout.addWidget(self.scan_selection_group)
100
51
 
101
52
  # Connect signals
102
53
  self.comboBox_scan_selection.currentIndexChanged.connect(self.on_scan_selected)
103
54
  self.button_run_scan.clicked.connect(self.run_scan)
55
+ self.button_add_bundle.clicked.connect(self.add_arg_bundle)
56
+ self.button_remove_bundle.clicked.connect(self.remove_arg_bundle)
104
57
 
105
58
  # Initialize scan selection
106
59
  self.populate_scans()
107
60
 
108
- def add_horizontal_separator(self, layout) -> None:
61
+ def create_scan_selection_group(self) -> QGroupBox:
109
62
  """
110
- Adds a horizontal separator to the given layout
63
+ Creates the scan selection group box with combobox to select the scan and start/stop button.
111
64
 
112
- Args:
113
- layout: Layout to add the separator to
114
- """
115
- separator = QFrame(self.scan_control_group)
116
- separator.setFrameShape(QFrame.HLine)
117
- separator.setFrameShadow(QFrame.Sunken)
118
- layout.addWidget(separator)
65
+ Returns:
66
+ QGroupBox: Group box containing the scan selection widgets.
67
+ """
68
+
69
+ scan_selection_group = QGroupBox("Scan Selection", self)
70
+ self.scan_selection_layout = QGridLayout(scan_selection_group)
71
+ self.comboBox_scan_selection = QComboBox(scan_selection_group)
72
+ # Run button
73
+ self.button_run_scan = QPushButton("Start", scan_selection_group)
74
+ self.button_run_scan.setStyleSheet("background-color: #559900; color: white")
75
+ # Stop button
76
+ self.button_stop_scan = StopButton(parent=scan_selection_group)
77
+ # Add bundle button
78
+ self.button_add_bundle = QPushButton("Add Bundle", scan_selection_group)
79
+ # Remove bundle button
80
+ self.button_remove_bundle = QPushButton("Remove Bundle", scan_selection_group)
81
+
82
+ self.scan_selection_layout.addWidget(self.comboBox_scan_selection, 0, 0, 1, 2)
83
+ self.scan_selection_layout.addWidget(self.button_run_scan, 1, 0)
84
+ self.scan_selection_layout.addWidget(self.button_stop_scan, 1, 1)
85
+ self.scan_selection_layout.addWidget(self.button_add_bundle, 2, 0)
86
+ self.scan_selection_layout.addWidget(self.button_remove_bundle, 2, 1)
87
+
88
+ return scan_selection_group
119
89
 
120
90
  def populate_scans(self):
121
- """Populates the scan selection combo box with available scans"""
122
- self.available_scans = self.client.producer.get(MessageEndpoints.available_scans()).resource
91
+ """Populates the scan selection combo box with available scans from BEC session."""
92
+ self.available_scans = self.client.connector.get(
93
+ MessageEndpoints.available_scans()
94
+ ).resource
123
95
  if self.allowed_scans is None:
124
- allowed_scans = self.available_scans.keys()
96
+ supported_scans = ["ScanBase", "SyncFlyScanBase", "AsyncFlyScanBase"]
97
+ allowed_scans = [
98
+ scan_name
99
+ for scan_name, scan_info in self.available_scans.items()
100
+ if scan_info["base_class"] in supported_scans and len(scan_info["gui_config"]) > 0
101
+ ]
102
+
125
103
  else:
126
104
  allowed_scans = self.allowed_scans
127
- # TODO check parent class is ScanBase -> filter out the scans not relevant for GUI
128
105
  self.comboBox_scan_selection.addItems(allowed_scans)
129
106
 
130
107
  def on_scan_selected(self):
131
108
  """Callback for scan selection combo box"""
109
+ self.reset_layout()
132
110
  selected_scan_name = self.comboBox_scan_selection.currentText()
133
111
  selected_scan_info = self.available_scans.get(selected_scan_name, {})
134
112
 
135
- print(selected_scan_info) # TODO remove when widget will be more mature
136
- # Generate kwargs input
137
- self.generate_kwargs_input_fields(selected_scan_info)
138
-
139
- # Args section
140
- self.generate_args_input_fields(selected_scan_info)
141
-
142
- def add_labels_to_layout(self, labels: list, grid_layout: QGridLayout) -> None:
143
- """
144
- Adds labels to the given grid layout as a separate row.
145
-
146
- Args:
147
- labels (list): List of label names to add.
148
- grid_layout (QGridLayout): The grid layout to which labels will be added.
149
- """
150
- row_index = grid_layout.rowCount() # Get the next available row
151
- for column_index, label_name in enumerate(labels):
152
- label = QLabel(label_name.capitalize(), self.scan_control_group)
153
- # Add the label to the grid layout at the calculated row and current column
154
- grid_layout.addWidget(label, row_index, column_index)
155
-
156
- def add_labels_to_table(
157
- self, labels: list, table: QTableWidget
158
- ) -> None: # TODO could be moved to BECTable
159
- """
160
- Adds labels to the given table widget as a header row.
161
-
162
- Args:
163
- labels(list): List of label names to add.
164
- table(QTableWidget): The table widget to which labels will be added.
165
- """
166
- table.setColumnCount(len(labels))
167
- table.setHorizontalHeaderLabels(labels)
168
-
169
- def generate_args_input_fields(self, scan_info: dict) -> None:
170
- """
171
- Generates input fields for args.
113
+ gui_config = selected_scan_info.get("gui_config", {})
114
+ self.arg_group = gui_config.get("arg_group", None)
115
+ self.kwarg_groups = gui_config.get("kwarg_groups", None)
172
116
 
173
- Args:
174
- scan_info(dict): Scan signature dictionary from BEC.
175
- """
117
+ if self.arg_box is None:
118
+ self.button_add_bundle.setEnabled(False)
119
+ self.button_remove_bundle.setEnabled(False)
176
120
 
177
- # Setup args table limits
178
- self.set_args_table_limits(self.args_table, scan_info)
121
+ if len(self.arg_group["arg_inputs"]) > 0:
122
+ self.button_add_bundle.setEnabled(True)
123
+ self.button_remove_bundle.setEnabled(True)
124
+ self.add_arg_group(self.arg_group)
125
+ if len(self.kwarg_groups) > 0:
126
+ self.add_kwargs_boxes(self.kwarg_groups)
179
127
 
180
- # Get arg_input from selected scan
181
- self.arg_input = scan_info.get("arg_input", {})
128
+ self.update()
129
+ self.adjustSize()
182
130
 
183
- # Generate labels for table
184
- self.add_labels_to_table(list(self.arg_input.keys()), self.args_table)
185
-
186
- # add minimum number of args rows
187
- if self.arg_size_min is not None:
188
- for i in range(self.arg_size_min):
189
- self.add_bundle()
190
-
191
- def generate_kwargs_input_fields(self, scan_info: dict) -> None:
131
+ def add_kwargs_boxes(self, groups: list):
192
132
  """
193
- Generates input fields for kwargs
133
+ Adds the given gui_groups to the scan control layout.
194
134
 
195
135
  Args:
196
- scan_info(dict): Scan signature dictionary from BEC.
197
- """
198
- # Create a new kwarg layout to replace the old one - this is necessary because otherwise row count is not reseted
199
- self.clear_and_delete_layout(self.kwargs_layout)
200
- self.kwargs_layout = self.create_new_grid_layout() # Create new grid layout
201
- self.scan_control_layout.insertLayout(0, self.kwargs_layout)
202
-
203
- # Get signature
204
- signature = scan_info.get("signature", [])
205
-
206
- # Extract kwargs from the converted signature
207
- kwargs = [param["name"] for param in signature if param["kind"] == "KEYWORD_ONLY"]
208
-
209
- # Add labels
210
- self.add_labels_to_layout(kwargs, self.kwargs_layout)
211
-
212
- # Add widgets
213
- widgets = self.generate_widgets_from_signature(kwargs, signature)
214
-
215
- self.add_widgets_row_to_layout(self.kwargs_layout, widgets)
216
-
217
- def generate_widgets_from_signature(self, items: list, signature: dict = None) -> list:
136
+ groups(list): List of dictionaries containing the gui_group information.
218
137
  """
219
- Generates widgets from the given list of items.
220
-
221
- Args:
222
- items(list): List of items to create widgets for.
223
- signature(dict, optional): Scan signature dictionary from BEC.
138
+ for group in groups:
139
+ box = ScanGroupBox(box_type="kwargs", config=group)
140
+ box.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
141
+ self.layout.addWidget(box)
142
+ self.kwarg_boxes.append(box)
224
143
 
225
- Returns:
226
- list: List of widgets created from the given items.
227
- """
228
- widgets = [] # Initialize an empty list to hold the widgets
229
-
230
- for item in items:
231
- if signature:
232
- # If a signature is provided, extract type and name from it
233
- kwarg_info = next((info for info in signature if info["name"] == item), None)
234
- if kwarg_info:
235
- item_type = kwarg_info.get("annotation", "_empty")
236
- item_name = item
237
- else:
238
- # If no signature is provided, assume the item is a tuple of (name, type)
239
- item_name, item_type = item
240
-
241
- widget_class = self.WIDGET_HANDLER.get(item_type, None)
242
- if widget_class is None:
243
- print(f"Unsupported annotation '{item_type}' for parameter '{item_name}'")
244
- continue
245
-
246
- # Instantiate the widget and set some properties if necessary
247
- widget = widget_class()
248
-
249
- # set high default range for spin boxes #TODO can be linked to motor/device limits from BEC
250
- if isinstance(widget, (QSpinBox, QDoubleSpinBox)):
251
- widget.setRange(-9999, 9999)
252
- widget.setValue(0)
253
- # Add the widget to the list
254
- widgets.append(widget)
255
-
256
- return widgets
257
-
258
- def set_args_table_limits(self, table: QTableWidget, scan_info: dict) -> None:
259
- # Get bundle info
260
- arg_bundle_size = scan_info.get("arg_bundle_size", {})
261
- self.arg_size_min = arg_bundle_size.get("min", 1)
262
- self.arg_size_max = arg_bundle_size.get("max", None)
263
-
264
- # Clear the previous input fields
265
- table.setRowCount(0) # Wipe table
266
-
267
- def add_widgets_row_to_layout(
268
- self, grid_layout: QGridLayout, widgets: list, row_index: int = None
269
- ) -> None:
144
+ def add_arg_group(self, group: dict):
270
145
  """
271
- Adds a row of widgets to the given grid layout.
146
+ Adds the given gui_groups to the scan control layout.
272
147
 
273
148
  Args:
274
- grid_layout (QGridLayout): The grid layout to which widgets will be added.
275
- items (list): List of parameter names to create widgets for.
276
- row_index (int): The row index where the widgets should be added.
277
149
  """
278
- # If row_index is not specified, add to the next available row
279
- if row_index is None:
280
- row_index = grid_layout.rowCount()
150
+ self.arg_box = ScanGroupBox(box_type="args", config=group)
151
+ self.arg_box.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
152
+ self.layout.addWidget(self.arg_box)
281
153
 
282
- for column_index, widget in enumerate(widgets):
283
- # Add the widget to the grid layout at the specified row and column
284
- grid_layout.addWidget(widget, row_index, column_index)
285
-
286
- def add_widgets_row_to_table(
287
- self, table_widget: QTableWidget, widgets: list, row_index: int = None
288
- ) -> None:
289
- """
290
- Adds a row of widgets to the given QTableWidget.
154
+ def add_arg_bundle(self):
155
+ self.arg_box.add_widget_bundle()
291
156
 
292
- Args:
293
- table_widget (QTableWidget): The table widget to which widgets will be added.
294
- widgets (list): List of widgets to add to the table.
295
- row_index (int): The row index where the widgets should be added. If None, add to the end.
296
- """
297
- # If row_index is not specified, add to the end of the table
298
- if row_index is None or row_index > table_widget.rowCount():
299
- row_index = table_widget.rowCount()
300
- if self.arg_size_max is not None: # ensure the max args size is not exceeded
301
- if row_index >= self.arg_size_max:
302
- return
303
- table_widget.insertRow(row_index)
304
-
305
- for column_index, widget in enumerate(widgets):
306
- # If the widget is a subclass of QWidget, use setCellWidget
307
- if issubclass(type(widget), QWidget):
308
- table_widget.setCellWidget(row_index, column_index, widget)
309
- else:
310
- # Otherwise, assume it's a string or some other value that should be displayed as text
311
- item = QTableWidgetItem(str(widget))
312
- table_widget.setItem(row_index, column_index, item)
313
-
314
- # Optionally, adjust the row height based on the content #TODO decide if needed
315
- table_widget.setRowHeight(
316
- row_index,
317
- max(widget.sizeHint().height() for widget in widgets if isinstance(widget, QWidget)),
318
- )
319
-
320
- def remove_last_row_from_table(self, table_widget: QTableWidget) -> None:
321
- """
322
- Removes the last row from the given QTableWidget until only one row is left.
323
-
324
- Args:
325
- table_widget (QTableWidget): The table widget from which the last row will be removed.
326
- """
327
- row_count = table_widget.rowCount()
328
- if (
329
- row_count > self.arg_size_min
330
- ): # Check to ensure there is a minimum number of rows remaining
331
- table_widget.removeRow(row_count - 1)
332
-
333
- def create_new_grid_layout(self):
334
- new_layout = QGridLayout()
335
- # TODO maybe setup other layouts properties here?
336
- return new_layout
337
-
338
- def clear_and_delete_layout(self, layout: QLayout):
339
- """
340
- Clears and deletes the given layout and all its child widgets.
157
+ def remove_arg_bundle(self):
158
+ self.arg_box.remove_widget_bundle()
341
159
 
342
- Args:
343
- layout(QLayout): Layout to clear and delete
344
- """
345
- if layout is not None:
346
- while layout.count():
347
- item = layout.takeAt(0)
348
- widget = item.widget()
349
- if widget:
350
- widget.deleteLater()
351
- else:
352
- sub_layout = item.layout()
353
- if sub_layout:
354
- self.clear_and_delete_layout(sub_layout)
355
- layout.deleteLater()
356
-
357
- def add_bundle(self) -> None:
358
- """Adds a new bundle to the scan control layout"""
359
- # Get widgets used for particular scan and save them to be able to use for adding bundles
360
- args_widgets = self.generate_widgets_from_signature(
361
- self.arg_input.items()
362
- ) # TODO decide if make sense to put widget list into method parameters
363
-
364
- # Add first widgets row to the table
365
- self.add_widgets_row_to_table(self.args_table, args_widgets)
366
-
367
- def remove_bundle(self) -> None:
368
- """Removes the last bundle from the scan control layout"""
369
- self.remove_last_row_from_table(self.args_table)
370
-
371
- def extract_kwargs_from_grid_row(self, grid_layout: QGridLayout, row: int) -> dict:
372
- kwargs = {}
373
- for column in range(grid_layout.columnCount()):
374
- label_item = grid_layout.itemAtPosition(row, column)
375
- if label_item is not None:
376
- label_widget = label_item.widget()
377
- if isinstance(label_widget, QLabel):
378
- key = label_widget.text()
379
-
380
- # The corresponding value widget is in the next row
381
- value_item = grid_layout.itemAtPosition(row + 1, column)
382
- if value_item is not None:
383
- value_widget = value_item.widget()
384
- # Use WidgetIO.get_value to extract the value
385
- value = WidgetIO.get_value(value_widget)
386
- kwargs[key] = value
387
- return kwargs
388
-
389
- def extract_args_from_table(self, table: QTableWidget) -> list:
390
- """
391
- Extracts the arguments from the given table widget.
160
+ def reset_layout(self):
161
+ """Clears the scan control layout from GuiGroups and ArgGroups boxes."""
162
+ if self.arg_box is not None:
163
+ self.layout.removeWidget(self.arg_box)
164
+ self.arg_box.deleteLater()
165
+ self.arg_box = None
166
+ if self.kwarg_boxes != []:
167
+ self.remove_kwarg_boxes()
392
168
 
393
- Args:
394
- table(QTableWidget): Table widget from which to extract the arguments
395
- """
396
- args = []
397
- for row in range(table.rowCount()):
398
- row_args = []
399
- for column in range(table.columnCount()):
400
- widget = table.cellWidget(row, column)
401
- if widget:
402
- if isinstance(widget, QLineEdit): # special case for QLineEdit for Devices
403
- value = widget.text().lower()
404
- if value in self.dev:
405
- value = getattr(self.dev, value)
406
- else:
407
- raise ValueError(f"The device '{value}' is not recognized.")
408
- else:
409
- value = WidgetIO.get_value(widget)
410
- row_args.append(value)
411
- args.extend(row_args)
412
- return args
169
+ def remove_kwarg_boxes(self):
170
+ for box in self.kwarg_boxes:
171
+ self.layout.removeWidget(box)
172
+ box.deleteLater()
173
+ self.kwarg_boxes = []
413
174
 
414
175
  def run_scan(self):
415
- # Extract kwargs for the scan
416
- kwargs = {
417
- k.lower(): v
418
- for k, v in self.extract_kwargs_from_grid_row(self.kwargs_layout, 1).items()
419
- }
420
-
421
- # Extract args from the table
422
- args = self.extract_args_from_table(self.args_table)
423
-
424
- # Convert args to lowercase if they are strings
425
- args = [arg.lower() if isinstance(arg, str) else arg for arg in args]
426
-
427
- # Execute the scan
176
+ args = []
177
+ kwargs = {}
178
+ if self.arg_box is not None:
179
+ args = self.arg_box.get_parameters()
180
+ for box in self.kwarg_boxes:
181
+ box_kwargs = box.get_parameters()
182
+ kwargs.update(box_kwargs)
428
183
  scan_function = getattr(self.scans, self.comboBox_scan_selection.currentText())
429
184
  if callable(scan_function):
430
185
  scan_function(*args, **kwargs)
431
186
 
187
+ def cleanup(self):
188
+ self.button_stop_scan.cleanup()
189
+ if self.arg_box:
190
+ for widget in self.arg_box.widgets:
191
+ if hasattr(widget, "cleanup"):
192
+ widget.cleanup()
193
+ for kwarg_box in self.kwarg_boxes:
194
+ for widget in kwarg_box.widgets:
195
+ if hasattr(widget, "cleanup"):
196
+ widget.cleanup()
197
+ super().cleanup()
198
+
199
+ def closeEvent(self, event):
200
+ self.cleanup()
201
+ QWidget().closeEvent(event)
202
+
432
203
 
433
204
  # Application example
434
205
  if __name__ == "__main__": # pragma: no cover
435
- # BECclient global variables
436
- client = BECDispatcher().client
437
- client.start()
438
-
439
206
  app = QApplication([])
440
- scan_control = ScanControl(client=client) # allowed_scans=["line_scan", "grid_scan"])
207
+ scan_control = ScanControl()
441
208
 
209
+ qdarktheme.setup_theme("auto")
442
210
  window = scan_control
443
211
  window.show()
444
212
  app.exec()