mesofield 0.3.2b0__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 (111) hide show
  1. docs/_static/custom.css +40 -0
  2. docs/_static/favicon.png +0 -0
  3. docs/_static/logo.png +0 -0
  4. docs/api/index.md +70 -0
  5. docs/conf.py +200 -0
  6. docs/developer_guide.md +303 -0
  7. docs/index.md +25 -0
  8. docs/tutorial.md +4 -0
  9. docs/user_guide.md +172 -0
  10. examples/teensy_pulse_generator.py +320 -0
  11. experiments/pipeline_demo/experiment.json +24 -0
  12. experiments/pipeline_demo/hardware.yaml +23 -0
  13. experiments/pipeline_demo/procedure.py +50 -0
  14. experiments/two_cam_demo/experiment.json +24 -0
  15. experiments/two_cam_demo/hardware.yaml +58 -0
  16. experiments/two_cam_demo/load_dataset.py +213 -0
  17. experiments/two_cam_demo/procedure.py +87 -0
  18. external/video-codecs/openh264-1.8.0-win64.dll +0 -0
  19. mesofield/__init__.py +45 -0
  20. mesofield/__main__.py +11 -0
  21. mesofield/_version.py +24 -0
  22. mesofield/base.py +750 -0
  23. mesofield/cli/__init__.py +57 -0
  24. mesofield/cli/_richhelp.py +100 -0
  25. mesofield/cli/acquire.py +254 -0
  26. mesofield/cli/datakit.py +165 -0
  27. mesofield/cli/process.py +376 -0
  28. mesofield/cli/rig.py +108 -0
  29. mesofield/cli/tools.py +347 -0
  30. mesofield/config.py +751 -0
  31. mesofield/data/__init__.py +23 -0
  32. mesofield/data/batch.py +633 -0
  33. mesofield/data/manager.py +388 -0
  34. mesofield/data/writer.py +289 -0
  35. mesofield/datakit/__init__.py +44 -0
  36. mesofield/datakit/__main__.py +35 -0
  37. mesofield/datakit/_utils/_logger.py +5 -0
  38. mesofield/datakit/_version.py +141 -0
  39. mesofield/datakit/config.py +50 -0
  40. mesofield/datakit/core.py +783 -0
  41. mesofield/datakit/datamodel.py +200 -0
  42. mesofield/datakit/discover.py +124 -0
  43. mesofield/datakit/explore.py +651 -0
  44. mesofield/datakit/notebooks/pupil_dlc.ipynb +2445 -0
  45. mesofield/datakit/profile.py +535 -0
  46. mesofield/datakit/shell.py +83 -0
  47. mesofield/datakit/sources/__init__.py +65 -0
  48. mesofield/datakit/sources/analysis/mesomap.py +194 -0
  49. mesofield/datakit/sources/analysis/mesoscope.py +77 -0
  50. mesofield/datakit/sources/analysis/pupil.py +246 -0
  51. mesofield/datakit/sources/behavior/__init__.py +0 -0
  52. mesofield/datakit/sources/behavior/dataqueue.py +281 -0
  53. mesofield/datakit/sources/behavior/psychopy.py +364 -0
  54. mesofield/datakit/sources/behavior/treadmill.py +323 -0
  55. mesofield/datakit/sources/behavior/wheel.py +277 -0
  56. mesofield/datakit/sources/camera/mesoscope.py +32 -0
  57. mesofield/datakit/sources/camera/metadata_json.py +130 -0
  58. mesofield/datakit/sources/camera/pupil.py +28 -0
  59. mesofield/datakit/sources/camera/suite2p.py +547 -0
  60. mesofield/datakit/sources/register.py +204 -0
  61. mesofield/datakit/sources/session/config.py +130 -0
  62. mesofield/datakit/sources/session/notes.py +63 -0
  63. mesofield/datakit/sources/session/timestamps.py +58 -0
  64. mesofield/datakit/timeline.py +306 -0
  65. mesofield/devices/__init__.py +42 -0
  66. mesofield/devices/base.py +498 -0
  67. mesofield/devices/base_camera.py +295 -0
  68. mesofield/devices/cameras.py +740 -0
  69. mesofield/devices/daq.py +151 -0
  70. mesofield/devices/encoder.py +384 -0
  71. mesofield/devices/mocks.py +275 -0
  72. mesofield/devices/psychopy_device.py +455 -0
  73. mesofield/devices/subprocesses/__init__.py +0 -0
  74. mesofield/devices/subprocesses/psychopy.py +133 -0
  75. mesofield/devices/treadmill.py +318 -0
  76. mesofield/engines.py +380 -0
  77. mesofield/gui/Mesofield_icon.png +0 -0
  78. mesofield/gui/__init__.py +76 -0
  79. mesofield/gui/config_wizard.py +724 -0
  80. mesofield/gui/controller.py +535 -0
  81. mesofield/gui/dynamic_controller.py +78 -0
  82. mesofield/gui/maingui.py +427 -0
  83. mesofield/gui/mdagui.py +285 -0
  84. mesofield/gui/qt_device_adapter.py +109 -0
  85. mesofield/gui/speedplotter.py +152 -0
  86. mesofield/gui/theme.py +445 -0
  87. mesofield/gui/tiff_viewer.py +1050 -0
  88. mesofield/gui/viewer.py +691 -0
  89. mesofield/hardware.py +549 -0
  90. mesofield/playback.py +1298 -0
  91. mesofield/processing/__init__.py +12 -0
  92. mesofield/processing/runner.py +237 -0
  93. mesofield/processors/__init__.py +13 -0
  94. mesofield/processors/base.py +287 -0
  95. mesofield/processors/frame_mean.py +19 -0
  96. mesofield/protocols.py +378 -0
  97. mesofield/scaffold/__init__.py +34 -0
  98. mesofield/scaffold/experiment.py +400 -0
  99. mesofield/scaffold/rigs.py +121 -0
  100. mesofield/signals.py +85 -0
  101. mesofield/utils/__init__.py +0 -0
  102. mesofield/utils/_logger.py +156 -0
  103. mesofield/utils/retrofit.py +309 -0
  104. mesofield/utils/utils.py +217 -0
  105. mesofield-0.3.2b0.dist-info/METADATA +178 -0
  106. mesofield-0.3.2b0.dist-info/RECORD +111 -0
  107. mesofield-0.3.2b0.dist-info/WHEEL +5 -0
  108. mesofield-0.3.2b0.dist-info/entry_points.txt +2 -0
  109. mesofield-0.3.2b0.dist-info/licenses/LICENSE +21 -0
  110. mesofield-0.3.2b0.dist-info/top_level.txt +6 -0
  111. scripts/bench_frame_processor.py +103 -0
@@ -0,0 +1,427 @@
1
+ # Necessary modules for the IPython console
2
+ from qtconsole.rich_jupyter_widget import RichJupyterWidget
3
+ from qtconsole.inprocess import QtInProcessKernelManager
4
+
5
+ from PyQt6.QtWidgets import (
6
+ QMainWindow,
7
+ QWidget,
8
+ QHBoxLayout,
9
+ QVBoxLayout,
10
+ QTabWidget,
11
+ QLayout,
12
+ QToolBar,
13
+ QSizePolicy,
14
+ )
15
+
16
+ from PyQt6.QtGui import QAction
17
+ from PyQt6.QtCore import QCoreApplication, Qt
18
+
19
+ from mesofield.gui.mdagui import MDA
20
+ from mesofield.gui.controller import ConfigController
21
+ from mesofield.gui.speedplotter import SerialWidget
22
+ from mesofield.gui.config_wizard import ConfigWizard
23
+ from mesofield.config import ExperimentConfig
24
+ from mesofield.protocols import Procedure
25
+
26
+ class MainWindow(QMainWindow):
27
+ def __init__(self, procedure: Procedure):
28
+ super().__init__()
29
+ self.procedure = procedure
30
+ self.display_keys = self.procedure.config.display_keys
31
+ self.setWindowTitle("Mesofield")
32
+
33
+ #============================== Always-available widgets =============================#
34
+ self.config_wizard = ConfigWizard(self.procedure)
35
+ self.initialize_console(self.procedure)
36
+ #--------------------------------------------------------------------#
37
+
38
+ #============================== Toolbar ================================#
39
+ self._toolbar = QToolBar("Hardware")
40
+ self._toolbar.setMovable(False)
41
+ # Disable the toolbar's context menu so users cannot hide it via
42
+ # QMainWindow's default "Toolbars" toggle popup.
43
+ self._toolbar.setContextMenuPolicy(Qt.ContextMenuPolicy.PreventContextMenu)
44
+ self._toolbar.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon)
45
+ self.addToolBar(self._toolbar)
46
+ self._prop_browsers: list = [] # open PropertyBrowser dialogs
47
+ self._prop_actions: list = [] # QAction objects for property browsers
48
+ #--------------------------------------------------------------------#
49
+
50
+ #=========================== Toolbar action ==========================#
51
+ # Place frequently used tools on the main hardware toolbar.
52
+ self._act_tiff_viewer = QAction("TIFF Viewer\u2026", self)
53
+ self._act_tiff_viewer.setToolTip(
54
+ "Open the TIFF ROI viewer (read-only; refuses files in the active recording)."
55
+ )
56
+ self._act_tiff_viewer.triggered.connect(self._open_tiff_viewer)
57
+ self._toolbar.addAction(self._act_tiff_viewer)
58
+ self._tiff_viewer = None # keep a reference so the window isn't GC'd
59
+ #--------------------------------------------------------------------#
60
+
61
+ #============================== Layout ==============================#
62
+ central_widget = QWidget()
63
+ self.main_layout = QVBoxLayout(central_widget)
64
+ self.setCentralWidget(central_widget)
65
+ self.main_layout.setSizeConstraint(QLayout.SizeConstraint.SetMinimumSize)
66
+
67
+ # Build a tab widget (always present). Pin its horizontal size so the
68
+ # ConfigController tab (fixed width) doesn't get stretched when the
69
+ # main window is enlarged — extra horizontal space goes to the MDA
70
+ # acquisition view on the left instead.
71
+ self.right_tabs = QTabWidget()
72
+ self.right_tabs.addTab(self.config_wizard, "⚙ Setup")
73
+ self.right_tabs.addTab(self.console_widget, "Terminal")
74
+ self.right_tabs.setSizePolicy(
75
+ QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Expanding
76
+ )
77
+
78
+ # The top row layout will hold [acquisition_gui | right_tabs]
79
+ self._top_row = QHBoxLayout()
80
+ self._mda_layout = QVBoxLayout()
81
+ # Give all extra width to the MDA column; right_tabs stays at sizeHint.
82
+ self._top_row.addLayout(self._mda_layout, 1)
83
+ self._top_row.addWidget(self.right_tabs, 0)
84
+ self.main_layout.addLayout(self._top_row)
85
+ #--------------------------------------------------------------------#
86
+
87
+ # Tracking for widgets that get built after config is loaded
88
+ self._acquisition_gui: MDA | None = None
89
+ self._config_controller: ConfigController | None = None
90
+ self._encoder_widget: SerialWidget | None = None
91
+ # Widgets built from `procedure.processors` -- one SerialWidget per
92
+ # FrameProcessor whose `plot_enabled` is True. Tracked here so we
93
+ # can tear them down on a `_build_acquisition_ui` rebuild.
94
+ self._processor_widgets: list[SerialWidget] = []
95
+
96
+ # Connect hot-load signals
97
+ self.config_wizard.configApplied.connect(self._on_config_applied)
98
+ self.config_wizard.hardwareReady.connect(self._build_acquisition_ui)
99
+ self.config_wizard.procedureChanged.connect(self._on_procedure_changed)
100
+
101
+ # If hardware is already configured (e.g. config_path was passed),
102
+ # build the full UI immediately.
103
+ if self.procedure.config.hardware.is_configured:
104
+ self._build_acquisition_ui()
105
+ self._on_config_applied()
106
+
107
+ #============================== Methods =================================#
108
+ def toggle_console(self):
109
+ """Switch to the Terminal tab."""
110
+ terminal_index = self.right_tabs.indexOf(self.console_widget)
111
+ self.right_tabs.setCurrentIndex(terminal_index)
112
+
113
+ def _open_tiff_viewer(self):
114
+ """Launch the TIFF ROI viewer pre-pointed at the current experiment dir.
115
+
116
+ The viewer is given a reference to the running ``Procedure`` so it can
117
+ refuse to open any file inside the active recording's output directory
118
+ while a camera is acquiring.
119
+ """
120
+ from mesofield.gui.tiff_viewer import TiffViewer
121
+
122
+ cfg = self.procedure.config
123
+ initial_dir = (
124
+ getattr(cfg, "bids_dir", None)
125
+ or getattr(cfg, "save_dir", None)
126
+ or ""
127
+ )
128
+
129
+ # Re-use existing window if still open; otherwise create a new one.
130
+ if self._tiff_viewer is not None and self._tiff_viewer.isVisible():
131
+ self._tiff_viewer.raise_()
132
+ self._tiff_viewer.activateWindow()
133
+ return
134
+
135
+ viewer = TiffViewer(initial_dir=initial_dir, procedure=self.procedure)
136
+ viewer.setWindowFlag(Qt.WindowType.Window, True)
137
+ viewer.resize(1100, 800)
138
+ viewer.show()
139
+ self._tiff_viewer = viewer
140
+
141
+
142
+ def initialize_console(self, procedure):
143
+ """Initialize the IPython console and embed it into the application."""
144
+ import mesofield.data as data
145
+ # Create an in-process kernel
146
+ self.kernel_manager = QtInProcessKernelManager()
147
+ self.kernel_manager.start_kernel()
148
+ self.kernel = self.kernel_manager.kernel
149
+ # suppress the kernel’s built-in banner
150
+ self.kernel.shell.banner1 = ""
151
+ self.kernel.shell.banner2 = ""
152
+ self.kernel.gui = 'qt'
153
+
154
+ # Create a kernel client and start channels
155
+ self.kernel_client = self.kernel_manager.client()
156
+ self.kernel_client.start_channels()
157
+
158
+ # Create the console widget
159
+ self.console_widget = RichJupyterWidget()
160
+ self.console_widget.kernel_manager = self.kernel_manager
161
+ self.console_widget.kernel_client = self.kernel_client
162
+ self.console_widget.console_width = 100
163
+ # Expose variables to the console's namespace
164
+ console_namespace = {
165
+ #'mda': self.acquisition_gui.mda,
166
+ 'self': self,
167
+ 'procedure': procedure,
168
+ 'data': data
169
+ # Optional, so you can use 'self' directly in the console
170
+ }
171
+ self.kernel.shell.push(console_namespace)
172
+
173
+ # Register the what_do helper command
174
+ def what_do():
175
+ """Print a friendly guide on using the Mesofield terminal."""
176
+ print(
177
+ "\n"
178
+ "=== Mesofield Terminal ===\n"
179
+ "\n"
180
+ "This is a live IPython console embedded inside Mesofield.\n"
181
+ "You have direct access to the running experiment and can\n"
182
+ "inspect or control it interactively.\n"
183
+ "\n"
184
+ "Available objects:\n"
185
+ " procedure – the active Procedure (start, stop, pause)\n"
186
+ " procedure.config – current ExperimentConfig (paths, subjects, params)\n"
187
+ " data – mesofield.data module (loading, processing, analysis)\n"
188
+ " self – the MainWindow instance (GUI widgets, layout)\n"
189
+ "\n"
190
+ "Quick examples:\n"
191
+ " procedure.config.subject # current subject ID\n"
192
+ " procedure.config.bids_dir # BIDS output directory\n"
193
+ " data.load.sessions('path') # load session data\n"
194
+ "\n"
195
+ "Tips:\n"
196
+ " • Use tab-completion to explore objects and methods.\n"
197
+ " • Use '?' after a name (e.g. procedure?) for docs.\n"
198
+ " • Any valid Python / IPython syntax works here.\n"
199
+ )
200
+ self.kernel.shell.push({'what_do': what_do})
201
+
202
+ self.console_widget.banner = (
203
+ "Mesofield Terminal — interactive Python console\n"
204
+ "Type what_do() for a guide on available commands and objects.\n"
205
+ )
206
+ from mesofield.gui import theme
207
+ self.console_widget.setStyleSheet(theme.terminal_qss())
208
+ #----------------------------------------------------------------------------#
209
+
210
+ def closeEvent(self, event):
211
+ # 1. Shut down the IPython console
212
+ if hasattr(self, "kernel_client"):
213
+ self.kernel_client.stop_channels()
214
+ if hasattr(self, "kernel_manager"):
215
+ self.kernel_manager.shutdown_kernel()
216
+ if hasattr(self, "console_widget"):
217
+ self.console_widget.close()
218
+
219
+ # 2. shut down all hardware
220
+ try:
221
+ self.procedure.config.hardware.shutdown()
222
+ except Exception:
223
+ pass
224
+
225
+ event.accept()
226
+ QCoreApplication.quit()
227
+
228
+ #============================== Private Methods =============================#
229
+
230
+ def _on_procedure_changed(self, new_procedure) -> None:
231
+ """The ConfigWizard discovered a different ``Procedure`` subclass.
232
+
233
+ Rebind the live reference so all subsequent UI rebuilds operate on
234
+ the user's custom subclass.
235
+ """
236
+ self.procedure = new_procedure
237
+ # Refresh the embedded IPython console's namespace so that typing
238
+ # ``procedure`` in the Terminal returns the live, hardware-initialized
239
+ # instance instead of the empty default created at launch.
240
+ if getattr(self, "kernel", None) is not None:
241
+ self.kernel.shell.push({"procedure": new_procedure})
242
+
243
+ def _on_config_applied(self) -> None:
244
+ """Rebuild config-dependent tabs after the user applies a configuration."""
245
+ self.display_keys = self.procedure.config.display_keys
246
+
247
+ # Rebuild the ConfigController tab
248
+ if self._config_controller is not None:
249
+ idx = self.right_tabs.indexOf(self._config_controller)
250
+ self.right_tabs.removeTab(idx)
251
+ self._config_controller.deleteLater()
252
+
253
+ self._config_controller = ConfigController(
254
+ self.procedure, display_keys=self.display_keys
255
+ )
256
+ # Insert after the Setup tab (index 1) so ordering is:
257
+ # [Setup] [ExperimentConfig] [Terminal]
258
+ self.right_tabs.insertTab(1, self._config_controller, "ExperimentConfig")
259
+ self.right_tabs.setCurrentWidget(self._config_controller)
260
+
261
+ # Pin the right column width to the ConfigController's fixed width
262
+ # (plus a small allowance for tab frame/margins) so the tab area is
263
+ # not stretched by wider tabs (e.g. the Terminal/Setup wizard).
264
+ cc_width = self._config_controller.width() or self._config_controller.sizeHint().width()
265
+ self.right_tabs.setFixedWidth(cc_width + 12)
266
+
267
+ def _build_acquisition_ui(self) -> None:
268
+ """Build (or rebuild) hardware-dependent widgets: MDA viewer and encoder."""
269
+ # -- MDA / acquisition GUI -------------------------------------------
270
+ if self._acquisition_gui is not None:
271
+ self._mda_layout.removeWidget(self._acquisition_gui)
272
+ self._acquisition_gui.deleteLater()
273
+
274
+ self._acquisition_gui = MDA(self.procedure)
275
+ self._mda_layout.insertWidget(0, self._acquisition_gui)
276
+
277
+ # -- Encoder widget ---------------------------------------------------
278
+ if self.procedure.config.hardware.encoder is not None:
279
+ if self._encoder_widget is not None:
280
+ self.main_layout.removeWidget(self._encoder_widget)
281
+ self._encoder_widget.deleteLater()
282
+
283
+ self._encoder_widget = SerialWidget(
284
+ cfg=self.procedure.config,
285
+ device_attr="encoder",
286
+ signal_name="serialSpeedUpdated",
287
+ label="Encoder",
288
+ value_label="Speed",
289
+ value_units="mm/s",
290
+ )
291
+ self.main_layout.addWidget(self._encoder_widget)
292
+
293
+ # -- Plots for procedure-authored FrameProcessors --------------------
294
+ self._build_processor_plots()
295
+
296
+ # -- Refresh the MM config section in the wizard ---------------------
297
+ self.config_wizard.refresh_mm_section()
298
+
299
+ # -- Property browser toolbar buttons --------------------------------
300
+ self._build_property_browsers()
301
+
302
+ def _build_processor_plots(self) -> None:
303
+ """Add one SerialWidget per (processor, channel) where the channel
304
+ has an entry in ``processor.plot_config``.
305
+
306
+ The procedure (or the ``@processor`` decorator) is responsible for
307
+ constructing the FrameProcessor and declaring per-channel plot
308
+ styling. We just discover ``procedure.processors`` and render
309
+ anything the user opted in to plot.
310
+ """
311
+ # Tear down anything we built on the previous pass.
312
+ for widget in self._processor_widgets:
313
+ try:
314
+ self.main_layout.removeWidget(widget)
315
+ widget.deleteLater()
316
+ except Exception:
317
+ pass
318
+ self._processor_widgets.clear()
319
+
320
+ cfg = self.procedure.config
321
+ for proc in getattr(self.procedure, "processors", []):
322
+ plot_cfg = getattr(proc, "plot_config", None) or {}
323
+ if not plot_cfg:
324
+ continue
325
+ attr = proc.device_id
326
+ # Expose on cfg.hardware once so SerialWidget's ``device_attr``
327
+ # lookup resolves to this processor for every channel.
328
+ setattr(cfg.hardware, attr, proc)
329
+ for channel in getattr(proc, "channels", ("value",)):
330
+ if channel not in plot_cfg:
331
+ continue
332
+ styling = dict(plot_cfg[channel])
333
+ styling.setdefault(
334
+ "label",
335
+ f"{attr.replace('_', ' ').title()} — {channel}"
336
+ if len(plot_cfg) > 1 else attr.replace("_", " ").title(),
337
+ )
338
+ styling.setdefault("value_label", "Value")
339
+ styling.setdefault("value_units", "")
340
+ styling.setdefault("y_range", (0, 4096))
341
+ styling.setdefault("value_scale", 1.0)
342
+ try:
343
+ widget = SerialWidget(
344
+ cfg=cfg,
345
+ device_attr=attr,
346
+ signal_name=f"{channel}Updated",
347
+ **styling,
348
+ )
349
+ except Exception as exc:
350
+ # Don't let a bad plot config kill the whole UI.
351
+ self._log_exception(
352
+ f"build SerialWidget for {attr}:{channel}", exc
353
+ )
354
+ continue
355
+ self.main_layout.addWidget(widget)
356
+ self._processor_widgets.append(widget)
357
+
358
+ def _log_exception(self, ctx: str, exc: Exception) -> None:
359
+ # Defensive logger — MainWindow has no central logger today.
360
+ try:
361
+ print(f"[MainWindow] {ctx} failed: {exc}")
362
+ except Exception:
363
+ pass
364
+
365
+ def _build_property_browsers(self) -> None:
366
+ """Add a toolbar button per MicroManager camera that opens a PropertyBrowser."""
367
+ # Close any existing browsers and remove only property-browser actions
368
+ for dlg in self._prop_browsers:
369
+ dlg.close()
370
+ dlg.deleteLater()
371
+ self._prop_browsers.clear()
372
+ # Remove previously-added property-browser actions but keep other toolbar actions
373
+ for act in getattr(self, "_prop_actions", []):
374
+ try:
375
+ self._toolbar.removeAction(act)
376
+ except Exception:
377
+ pass
378
+ self._prop_actions.clear()
379
+
380
+ cameras = self.procedure.config.hardware.cameras
381
+ mm_cams = [
382
+ cam for cam in cameras
383
+ if cam.backend == "micromanager" and hasattr(cam, "core")
384
+ ]
385
+ if not mm_cams:
386
+ return
387
+
388
+ try:
389
+ from pymmcore_widgets import PropertyBrowser
390
+ except ImportError:
391
+ # pymmcore-widgets not installed – skip toolbar
392
+ return
393
+
394
+ for cam in mm_cams:
395
+ browser = PropertyBrowser(mmcore=cam.core, parent=self)
396
+ browser.setWindowTitle(f"Properties — {cam.name}")
397
+ browser.resize(900, 600)
398
+ self._prop_browsers.append(browser)
399
+
400
+ action = QAction(f"🔬 {cam.name} Properties", self)
401
+ action.setToolTip(
402
+ f"Open the device property browser for {cam.name}"
403
+ )
404
+ # Use a default-argument closure to capture the correct browser
405
+ action.triggered.connect(
406
+ lambda checked, b=browser: self._show_property_browser(b)
407
+ )
408
+ self._toolbar.addAction(action)
409
+ self._prop_actions.append(action)
410
+
411
+ @staticmethod
412
+ def _show_property_browser(browser) -> None:
413
+ """Show (or raise) a PropertyBrowser dialog."""
414
+ browser.show()
415
+ browser.raise_()
416
+ browser.activateWindow()
417
+
418
+ def _on_end(self) -> None:
419
+ """Called when the MDA is finished."""
420
+
421
+ def _update_config(self, config):
422
+ self.procedure.config = config
423
+
424
+ def _on_pause(self, state: bool) -> None:
425
+ """Called when the MDA is paused."""
426
+
427
+