bec-widgets 1.12.0__py3-none-any.whl → 1.14.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 (31) hide show
  1. .gitlab-ci.yml +1 -0
  2. CHANGELOG.md +5727 -0
  3. PKG-INFO +4 -3
  4. bec_widgets/cli/auto_updates.py +45 -61
  5. bec_widgets/cli/client.py +19 -2
  6. bec_widgets/cli/client_utils.py +142 -198
  7. bec_widgets/cli/generate_cli.py +2 -2
  8. bec_widgets/cli/rpc/__init__.py +0 -0
  9. bec_widgets/cli/rpc/rpc_base.py +177 -0
  10. bec_widgets/cli/server.py +66 -29
  11. bec_widgets/qt_utils/error_popups.py +4 -2
  12. bec_widgets/tests/utils.py +8 -0
  13. bec_widgets/utils/bec_connector.py +1 -1
  14. bec_widgets/utils/widget_io.py +85 -5
  15. bec_widgets/widgets/containers/dock/dock.py +1 -1
  16. bec_widgets/widgets/containers/dock/dock_area.py +40 -2
  17. bec_widgets/widgets/containers/layout_manager/layout_manager.py +1 -1
  18. bec_widgets/widgets/containers/main_window/main_window.py +33 -1
  19. bec_widgets/widgets/games/__init__.py +3 -0
  20. bec_widgets/widgets/games/minesweeper.py +413 -0
  21. bec_widgets/widgets/games/minesweeper.pyproject +1 -0
  22. bec_widgets/widgets/games/minesweeper_plugin.py +54 -0
  23. bec_widgets/widgets/games/register_minesweeper.py +15 -0
  24. {bec_widgets-1.12.0.dist-info → bec_widgets-1.14.0.dist-info}/METADATA +4 -3
  25. {bec_widgets-1.12.0.dist-info → bec_widgets-1.14.0.dist-info}/RECORD +31 -24
  26. {bec_widgets-1.12.0.dist-info → bec_widgets-1.14.0.dist-info}/WHEEL +1 -1
  27. pyproject.toml +2 -2
  28. /bec_widgets/cli/{rpc_register.py → rpc/rpc_register.py} +0 -0
  29. /bec_widgets/cli/{rpc_wigdet_handler.py → rpc/rpc_widget_handler.py} +0 -0
  30. {bec_widgets-1.12.0.dist-info → bec_widgets-1.14.0.dist-info}/entry_points.txt +0 -0
  31. {bec_widgets-1.12.0.dist-info → bec_widgets-1.14.0.dist-info}/licenses/LICENSE +0 -0
bec_widgets/cli/server.py CHANGED
@@ -1,9 +1,11 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import functools
3
4
  import json
4
5
  import signal
5
6
  import sys
6
- from contextlib import redirect_stderr, redirect_stdout
7
+ import types
8
+ from contextlib import contextmanager, redirect_stderr, redirect_stdout
7
9
  from typing import Union
8
10
 
9
11
  from bec_lib.endpoints import MessageEndpoints
@@ -12,7 +14,8 @@ from bec_lib.service_config import ServiceConfig
12
14
  from bec_lib.utils.import_utils import lazy_import
13
15
  from qtpy.QtCore import Qt, QTimer
14
16
 
15
- from bec_widgets.cli.rpc_register import RPCRegister
17
+ from bec_widgets.cli.rpc.rpc_register import RPCRegister
18
+ from bec_widgets.qt_utils.error_popups import ErrorPopupUtility
16
19
  from bec_widgets.utils import BECDispatcher
17
20
  from bec_widgets.utils.bec_connector import BECConnector
18
21
  from bec_widgets.widgets.containers.dock import BECDockArea
@@ -23,6 +26,27 @@ messages = lazy_import("bec_lib.messages")
23
26
  logger = bec_logger.logger
24
27
 
25
28
 
29
+ @contextmanager
30
+ def rpc_exception_hook(err_func):
31
+ """This context replaces the popup message box for error display with a specific hook"""
32
+ # get error popup utility singleton
33
+ popup = ErrorPopupUtility()
34
+ # save current setting
35
+ old_exception_hook = popup.custom_exception_hook
36
+
37
+ # install err_func, if it is a callable
38
+ def custom_exception_hook(self, exc_type, value, tb, **kwargs):
39
+ err_func({"error": popup.get_error_message(exc_type, value, tb)})
40
+
41
+ popup.custom_exception_hook = types.MethodType(custom_exception_hook, popup)
42
+
43
+ try:
44
+ yield popup
45
+ finally:
46
+ # restore state of error popup utility singleton
47
+ popup.custom_exception_hook = old_exception_hook
48
+
49
+
26
50
  class BECWidgetsCLIServer:
27
51
 
28
52
  def __init__(
@@ -57,18 +81,19 @@ class BECWidgetsCLIServer:
57
81
  def on_rpc_update(self, msg: dict, metadata: dict):
58
82
  request_id = metadata.get("request_id")
59
83
  logger.debug(f"Received RPC instruction: {msg}, metadata: {metadata}")
60
- try:
61
- obj = self.get_object_from_config(msg["parameter"])
62
- method = msg["action"]
63
- args = msg["parameter"].get("args", [])
64
- kwargs = msg["parameter"].get("kwargs", {})
65
- res = self.run_rpc(obj, method, args, kwargs)
66
- except Exception as e:
67
- logger.error(f"Error while executing RPC instruction: {e}")
68
- self.send_response(request_id, False, {"error": str(e)})
69
- else:
70
- logger.debug(f"RPC instruction executed successfully: {res}")
71
- self.send_response(request_id, True, {"result": res})
84
+ with rpc_exception_hook(functools.partial(self.send_response, request_id, False)):
85
+ try:
86
+ obj = self.get_object_from_config(msg["parameter"])
87
+ method = msg["action"]
88
+ args = msg["parameter"].get("args", [])
89
+ kwargs = msg["parameter"].get("kwargs", {})
90
+ res = self.run_rpc(obj, method, args, kwargs)
91
+ except Exception as e:
92
+ logger.error(f"Error while executing RPC instruction: {e}")
93
+ self.send_response(request_id, False, {"error": str(e)})
94
+ else:
95
+ logger.debug(f"RPC instruction executed successfully: {res}")
96
+ self.send_response(request_id, True, {"result": res})
72
97
 
73
98
  def send_response(self, request_id: str, accepted: bool, msg: dict):
74
99
  self.client.connector.set_and_publish(
@@ -181,14 +206,8 @@ def main():
181
206
 
182
207
  import bec_widgets
183
208
 
184
- bec_logger.level = bec_logger.LOGLEVEL.DEBUG
185
- if __name__ != "__main__":
186
- # if not running as main, set the log level to critical
187
- # pylint: disable=protected-access
188
- bec_logger._stderr_log_level = bec_logger.LOGLEVEL.CRITICAL
189
-
190
209
  parser = argparse.ArgumentParser(description="BEC Widgets CLI Server")
191
- parser.add_argument("--id", type=str, help="The id of the server")
210
+ parser.add_argument("--id", type=str, default="test", help="The id of the server")
192
211
  parser.add_argument(
193
212
  "--gui_class",
194
213
  type=str,
@@ -199,10 +218,20 @@ def main():
199
218
 
200
219
  args = parser.parse_args()
201
220
 
202
- if args.gui_class == "BECFigure":
203
- gui_class = BECFigure
204
- elif args.gui_class == "BECDockArea":
221
+ if args.hide:
222
+ # if we start hidden, it means we are under control of the client
223
+ # -> set the log level to critical to not see all the messages
224
+ # pylint: disable=protected-access
225
+ # bec_logger._stderr_log_level = bec_logger.LOGLEVEL.CRITICAL
226
+ bec_logger.level = bec_logger.LOGLEVEL.CRITICAL
227
+ else:
228
+ # verbose log
229
+ bec_logger.level = bec_logger.LOGLEVEL.DEBUG
230
+
231
+ if args.gui_class == "BECDockArea":
205
232
  gui_class = BECDockArea
233
+ elif args.gui_class == "BECFigure":
234
+ gui_class = BECFigure
206
235
  else:
207
236
  print(
208
237
  "Please specify a valid gui_class to run. Use -h for help."
@@ -213,8 +242,10 @@ def main():
213
242
  with redirect_stdout(SimpleFileLikeFromLogOutputFunc(logger.info)):
214
243
  with redirect_stderr(SimpleFileLikeFromLogOutputFunc(logger.error)):
215
244
  app = QApplication(sys.argv)
216
- app.setQuitOnLastWindowClosed(False)
217
- app.setApplicationName("BEC Figure")
245
+ # set close on last window, only if not under control of client ;
246
+ # indeed, Qt considers a hidden window a closed window, so if all windows
247
+ # are hidden by default it exits
248
+ app.setQuitOnLastWindowClosed(not args.hide)
218
249
  module_path = os.path.dirname(bec_widgets.__file__)
219
250
  icon = QIcon()
220
251
  icon.addFile(
@@ -222,6 +253,8 @@ def main():
222
253
  size=QSize(48, 48),
223
254
  )
224
255
  app.setWindowIcon(icon)
256
+ # store gui id within QApplication object, to make it available to all widgets
257
+ app.gui_id = args.id
225
258
 
226
259
  server = _start_server(args.id, gui_class, args.config)
227
260
 
@@ -233,7 +266,6 @@ def main():
233
266
 
234
267
  gui = server.gui
235
268
  win.setCentralWidget(gui)
236
- win.resize(800, 600)
237
269
  if not args.hide:
238
270
  win.show()
239
271
 
@@ -242,6 +274,12 @@ def main():
242
274
  def sigint_handler(*args):
243
275
  # display message, for people to let it terminate gracefully
244
276
  print("Caught SIGINT, exiting")
277
+ # first hide all top level windows
278
+ # this is to discriminate the cases between "user clicks on [X]"
279
+ # (which should be filtered, to not close -see BECDockArea-)
280
+ # or "app is asked to close"
281
+ for window in app.topLevelWidgets():
282
+ window.hide() # so, we know we can exit because it is hidden
245
283
  app.quit()
246
284
 
247
285
  signal.signal(signal.SIGINT, sigint_handler)
@@ -250,6 +288,5 @@ def main():
250
288
  sys.exit(app.exec())
251
289
 
252
290
 
253
- if __name__ == "__main__": # pragma: no cover
254
- sys.argv = ["bec_widgets.cli.server", "--id", "e2860", "--gui_class", "BECDockArea"]
291
+ if __name__ == "__main__":
255
292
  main()
@@ -169,12 +169,14 @@ class _ErrorPopupUtility(QObject):
169
169
  error_message = " ".join(captured_message)
170
170
  return error_message
171
171
 
172
+ def get_error_message(self, exctype, value, tb):
173
+ return "".join(traceback.format_exception(exctype, value, tb))
174
+
172
175
  def custom_exception_hook(self, exctype, value, tb, popup_error=False):
173
176
  if popup_error or self.enable_error_popup:
174
- error_message = traceback.format_exception(exctype, value, tb)
175
177
  self.error_occurred.emit(
176
178
  "Method error" if popup_error else "Application Error",
177
- "".join(error_message),
179
+ self.get_error_message(exctype, value, tb),
178
180
  self.parent(),
179
181
  )
180
182
  else:
@@ -224,3 +224,11 @@ DEVICES = [
224
224
  Positioner("test", limits=[-10, 10], read_value=2.0),
225
225
  Device("test_device"),
226
226
  ]
227
+
228
+
229
+ def check_remote_data_size(widget, plot_name, num_elements):
230
+ """
231
+ Check if the remote data has the correct number of elements.
232
+ Used in the qtbot.waitUntil function.
233
+ """
234
+ return len(widget.get_all_data()[plot_name]["x"]) == num_elements
@@ -12,7 +12,7 @@ from pydantic import BaseModel, Field, field_validator
12
12
  from qtpy.QtCore import QObject, QRunnable, QThreadPool, Signal
13
13
  from qtpy.QtWidgets import QApplication
14
14
 
15
- from bec_widgets.cli.rpc_register import RPCRegister
15
+ from bec_widgets.cli.rpc.rpc_register import RPCRegister
16
16
  from bec_widgets.qt_utils.error_popups import ErrorPopupUtility
17
17
  from bec_widgets.qt_utils.error_popups import SafeSlot as pyqtSlot
18
18
  from bec_widgets.utils.yaml_dialog import load_yaml, load_yaml_gui, save_yaml, save_yaml_gui
@@ -1,6 +1,5 @@
1
1
  # pylint: disable=no-name-in-module
2
2
  from abc import ABC, abstractmethod
3
- from typing import Literal
4
3
 
5
4
  from qtpy.QtWidgets import (
6
5
  QApplication,
@@ -28,6 +27,15 @@ class WidgetHandler(ABC):
28
27
  def set_value(self, widget: QWidget, value):
29
28
  """Set a value on the widget instance."""
30
29
 
30
+ def connect_change_signal(self, widget: QWidget, slot):
31
+ """
32
+ Connect a change signal from this widget to the given slot.
33
+ If the widget type doesn't have a known "value changed" signal, do nothing.
34
+
35
+ slot: a function accepting two arguments (widget, value)
36
+ """
37
+ pass
38
+
31
39
 
32
40
  class LineEditHandler(WidgetHandler):
33
41
  """Handler for QLineEdit widgets."""
@@ -38,6 +46,9 @@ class LineEditHandler(WidgetHandler):
38
46
  def set_value(self, widget: QLineEdit, value: str) -> None:
39
47
  widget.setText(value)
40
48
 
49
+ def connect_change_signal(self, widget: QLineEdit, slot):
50
+ widget.textChanged.connect(lambda text, w=widget: slot(w, text))
51
+
41
52
 
42
53
  class ComboBoxHandler(WidgetHandler):
43
54
  """Handler for QComboBox widgets."""
@@ -53,6 +64,11 @@ class ComboBoxHandler(WidgetHandler):
53
64
  if isinstance(value, int):
54
65
  widget.setCurrentIndex(value)
55
66
 
67
+ def connect_change_signal(self, widget: QComboBox, slot):
68
+ # currentIndexChanged(int) or currentIndexChanged(str) both possible.
69
+ # We use currentIndexChanged(int) for a consistent behavior.
70
+ widget.currentIndexChanged.connect(lambda idx, w=widget: slot(w, self.get_value(w)))
71
+
56
72
 
57
73
  class TableWidgetHandler(WidgetHandler):
58
74
  """Handler for QTableWidget widgets."""
@@ -72,6 +88,16 @@ class TableWidgetHandler(WidgetHandler):
72
88
  item = QTableWidgetItem(str(cell_value))
73
89
  widget.setItem(row, col, item)
74
90
 
91
+ def connect_change_signal(self, widget: QTableWidget, slot):
92
+ # If desired, we could connect cellChanged(row, col) and then fetch all data.
93
+ # This might be noisy if table is large.
94
+ # For demonstration, connect cellChanged to update entire table value.
95
+ def on_cell_changed(row, col, w=widget):
96
+ val = self.get_value(w)
97
+ slot(w, val)
98
+
99
+ widget.cellChanged.connect(on_cell_changed)
100
+
75
101
 
76
102
  class SpinBoxHandler(WidgetHandler):
77
103
  """Handler for QSpinBox and QDoubleSpinBox widgets."""
@@ -82,6 +108,9 @@ class SpinBoxHandler(WidgetHandler):
82
108
  def set_value(self, widget, value):
83
109
  widget.setValue(value)
84
110
 
111
+ def connect_change_signal(self, widget: QSpinBox | QDoubleSpinBox, slot):
112
+ widget.valueChanged.connect(lambda val, w=widget: slot(w, val))
113
+
85
114
 
86
115
  class CheckBoxHandler(WidgetHandler):
87
116
  """Handler for QCheckBox widgets."""
@@ -92,6 +121,9 @@ class CheckBoxHandler(WidgetHandler):
92
121
  def set_value(self, widget, value):
93
122
  widget.setChecked(value)
94
123
 
124
+ def connect_change_signal(self, widget: QCheckBox, slot):
125
+ widget.toggled.connect(lambda val, w=widget: slot(w, val))
126
+
95
127
 
96
128
  class LabelHandler(WidgetHandler):
97
129
  """Handler for QLabel widgets."""
@@ -99,12 +131,15 @@ class LabelHandler(WidgetHandler):
99
131
  def get_value(self, widget, **kwargs):
100
132
  return widget.text()
101
133
 
102
- def set_value(self, widget, value):
134
+ def set_value(self, widget: QLabel, value):
103
135
  widget.setText(value)
104
136
 
137
+ # QLabel typically doesn't have user-editable changes. No signal to connect.
138
+ # If needed, this can remain empty.
139
+
105
140
 
106
141
  class WidgetIO:
107
- """Public interface for getting and setting values using handler mapping"""
142
+ """Public interface for getting, setting values and connecting signals using handler mapping"""
108
143
 
109
144
  _handlers = {
110
145
  QLineEdit: LineEditHandler,
@@ -148,6 +183,17 @@ class WidgetIO:
148
183
  elif not ignore_errors:
149
184
  raise ValueError(f"No handler for widget type: {type(widget)}")
150
185
 
186
+ @staticmethod
187
+ def connect_widget_change_signal(widget, slot):
188
+ """
189
+ Connect the widget's value-changed signal to a generic slot function (widget, value).
190
+ This now delegates the logic to the widget's handler.
191
+ """
192
+ handler_class = WidgetIO._find_handler(widget)
193
+ if handler_class:
194
+ handler = handler_class()
195
+ handler.connect_change_signal(widget, slot)
196
+
151
197
  @staticmethod
152
198
  def check_and_adjust_limits(spin_box: QDoubleSpinBox, number: float):
153
199
  """
@@ -309,8 +355,8 @@ class WidgetHierarchy:
309
355
  WidgetHierarchy.import_config_from_dict(child, widget_config, set_values)
310
356
 
311
357
 
312
- # Example application to demonstrate the usage of the functions
313
- if __name__ == "__main__": # pragma: no cover
358
+ # Example usage
359
+ def hierarchy_example(): # pragma: no cover
314
360
  app = QApplication([])
315
361
 
316
362
  # Create instance of WidgetHierarchy
@@ -365,3 +411,37 @@ if __name__ == "__main__": # pragma: no cover
365
411
  print(f"Config dict new REDUCED: {config_dict_new_reduced}")
366
412
 
367
413
  app.exec()
414
+
415
+
416
+ def widget_io_signal_example(): # pragma: no cover
417
+ app = QApplication([])
418
+
419
+ main_widget = QWidget()
420
+ layout = QVBoxLayout(main_widget)
421
+ line_edit = QLineEdit(main_widget)
422
+ combo_box = QComboBox(main_widget)
423
+ spin_box = QSpinBox(main_widget)
424
+ combo_box.addItems(["Option 1", "Option 2", "Option 3"])
425
+
426
+ layout.addWidget(line_edit)
427
+ layout.addWidget(combo_box)
428
+ layout.addWidget(spin_box)
429
+
430
+ main_widget.show()
431
+
432
+ def universal_slot(w, val):
433
+ print(f"Widget {w.objectName() or w} changed, new value: {val}")
434
+
435
+ # Connect all supported widgets through their handlers
436
+ WidgetIO.connect_widget_change_signal(line_edit, universal_slot)
437
+ WidgetIO.connect_widget_change_signal(combo_box, universal_slot)
438
+ WidgetIO.connect_widget_change_signal(spin_box, universal_slot)
439
+
440
+ app.exec_()
441
+
442
+
443
+ if __name__ == "__main__": # pragma: no cover
444
+ # Change example function to test different scenarios
445
+
446
+ # hierarchy_example()
447
+ widget_io_signal_example()
@@ -6,7 +6,7 @@ from pydantic import Field
6
6
  from pyqtgraph.dockarea import Dock, DockLabel
7
7
  from qtpy import QtCore, QtGui
8
8
 
9
- from bec_widgets.cli.rpc_wigdet_handler import widget_handler
9
+ from bec_widgets.cli.rpc.rpc_widget_handler import widget_handler
10
10
  from bec_widgets.utils import ConnectionConfig, GridLayoutManager
11
11
  from bec_widgets.utils.bec_widget import BECWidget
12
12
 
@@ -3,11 +3,12 @@ from __future__ import annotations
3
3
  from typing import Literal, Optional
4
4
  from weakref import WeakValueDictionary
5
5
 
6
+ from bec_lib.endpoints import MessageEndpoints
6
7
  from pydantic import Field
7
8
  from pyqtgraph.dockarea.DockArea import DockArea
8
- from qtpy.QtCore import Qt
9
+ from qtpy.QtCore import QSize, Qt
9
10
  from qtpy.QtGui import QPainter, QPaintEvent
10
- from qtpy.QtWidgets import QSizePolicy, QVBoxLayout, QWidget
11
+ from qtpy.QtWidgets import QApplication, QSizePolicy, QVBoxLayout, QWidget
11
12
 
12
13
  from bec_widgets.qt_utils.error_popups import SafeSlot
13
14
  from bec_widgets.qt_utils.toolbar import (
@@ -43,6 +44,7 @@ class BECDockArea(BECWidget, QWidget):
43
44
  PLUGIN = True
44
45
  USER_ACCESS = [
45
46
  "_config_dict",
47
+ "selected_device",
46
48
  "panels",
47
49
  "save_state",
48
50
  "remove_dock",
@@ -55,6 +57,7 @@ class BECDockArea(BECWidget, QWidget):
55
57
  "temp_areas",
56
58
  "show",
57
59
  "hide",
60
+ "delete",
58
61
  ]
59
62
 
60
63
  def __init__(
@@ -158,6 +161,9 @@ class BECDockArea(BECWidget, QWidget):
158
161
  self.toolbar.addWidget(DarkModeButton(toolbar=True))
159
162
  self._hook_toolbar()
160
163
 
164
+ def minimumSizeHint(self):
165
+ return QSize(800, 600)
166
+
161
167
  def _hook_toolbar(self):
162
168
  # Menu Plot
163
169
  self.toolbar.widgets["menu_plots"].widgets["waveform"].triggered.connect(
@@ -210,6 +216,17 @@ class BECDockArea(BECWidget, QWidget):
210
216
  "Add docks using 'add_dock' method from CLI\n or \n Add widget docks using the toolbar",
211
217
  )
212
218
 
219
+ @property
220
+ def selected_device(self) -> str:
221
+ gui_id = QApplication.instance().gui_id
222
+ auto_update_config = self.client.connector.get(
223
+ MessageEndpoints.gui_auto_update_config(gui_id)
224
+ )
225
+ try:
226
+ return auto_update_config.selected_device
227
+ except AttributeError:
228
+ return None
229
+
213
230
  @property
214
231
  def panels(self) -> dict[str, BECDock]:
215
232
  """
@@ -406,6 +423,17 @@ class BECDockArea(BECWidget, QWidget):
406
423
  self.dock_area.deleteLater()
407
424
  super().cleanup()
408
425
 
426
+ def closeEvent(self, event):
427
+ if self.parent() is None:
428
+ # we are at top-level (independent window)
429
+ if self.isVisible():
430
+ # we are visible => user clicked on [X]
431
+ # (when closeEvent is called from shutdown procedure,
432
+ # everything is hidden first)
433
+ # so, let's ignore "close", and do hide instead
434
+ event.ignore()
435
+ self.setVisible(False)
436
+
409
437
  def close(self):
410
438
  """
411
439
  Close the dock area and cleanup.
@@ -418,14 +446,24 @@ class BECDockArea(BECWidget, QWidget):
418
446
  """Show all windows including floating docks."""
419
447
  super().show()
420
448
  for docks in self.panels.values():
449
+ if docks.window() is self:
450
+ # avoid recursion
451
+ continue
421
452
  docks.window().show()
422
453
 
423
454
  def hide(self):
424
455
  """Hide all windows including floating docks."""
425
456
  super().hide()
426
457
  for docks in self.panels.values():
458
+ if docks.window() is self:
459
+ # avoid recursion
460
+ continue
427
461
  docks.window().hide()
428
462
 
463
+ def delete(self):
464
+ self.hide()
465
+ self.deleteLater()
466
+
429
467
 
430
468
  if __name__ == "__main__":
431
469
  from qtpy.QtWidgets import QApplication
@@ -20,7 +20,7 @@ from qtpy.QtWidgets import (
20
20
  )
21
21
  from typeguard import typechecked
22
22
 
23
- from bec_widgets.cli.rpc_wigdet_handler import widget_handler
23
+ from bec_widgets.cli.rpc.rpc_widget_handler import widget_handler
24
24
 
25
25
 
26
26
  class LayoutManagerWidget(QWidget):
@@ -1,9 +1,41 @@
1
- from qtpy.QtWidgets import QMainWindow
1
+ from qtpy.QtWidgets import QApplication, QMainWindow
2
2
 
3
3
  from bec_widgets.utils import BECConnector
4
+ from bec_widgets.widgets.containers.dock.dock_area import BECDockArea
4
5
 
5
6
 
6
7
  class BECMainWindow(QMainWindow, BECConnector):
7
8
  def __init__(self, *args, **kwargs):
8
9
  BECConnector.__init__(self, **kwargs)
9
10
  QMainWindow.__init__(self, *args, **kwargs)
11
+
12
+ def _dump(self):
13
+ """Return a dictionary with informations about the application state, for use in tests"""
14
+ # TODO: ModularToolBar and something else leak top-level widgets (3 or 4 QMenu + 2 QWidget);
15
+ # so, a filtering based on title is applied here, but the solution is to not have those widgets
16
+ # as top-level (so for now, a window with no title does not appear in _dump() result)
17
+
18
+ # NOTE: the main window itself is excluded, since we want to dump dock areas
19
+ info = {
20
+ tlw.gui_id: {
21
+ "title": tlw.windowTitle(),
22
+ "visible": tlw.isVisible(),
23
+ "class": str(type(tlw)),
24
+ }
25
+ for tlw in QApplication.instance().topLevelWidgets()
26
+ if tlw is not self and tlw.windowTitle()
27
+ }
28
+ # Add the main window dock area
29
+ info[self.centralWidget().gui_id] = {
30
+ "title": self.windowTitle(),
31
+ "visible": self.isVisible(),
32
+ "class": str(type(self.centralWidget())),
33
+ }
34
+ return info
35
+
36
+ def new_dock_area(self, name):
37
+ dock_area = BECDockArea()
38
+ dock_area.resize(dock_area.minimumSizeHint())
39
+ dock_area.window().setWindowTitle(name)
40
+ dock_area.show()
41
+ return dock_area
@@ -0,0 +1,3 @@
1
+ from bec_widgets.widgets.games.minesweeper import Minesweeper
2
+
3
+ __ALL__ = ["Minesweeper"]