qplotter 1.6.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 (130) hide show
  1. qplot/__init__.py +46 -0
  2. qplot/__main__.py +232 -0
  3. qplot/_auxiliary_paths.py +67 -0
  4. qplot/_build_info.json +1 -0
  5. qplot/_metadata.py +75 -0
  6. qplot/_repair.py +14 -0
  7. qplot/_shutdown_supervisor.py +4049 -0
  8. qplot/_version.py +12 -0
  9. qplot/_windows_shutdown_job.py +1166 -0
  10. qplot/configuration/__init__.py +3 -0
  11. qplot/configuration/config.py +578 -0
  12. qplot/configuration/config_schema.json +357 -0
  13. qplot/configuration/fonts.py +3 -0
  14. qplot/configuration/scripts.py +255 -0
  15. qplot/configuration/themes/__init__.py +13 -0
  16. qplot/configuration/themes/_base.py +592 -0
  17. qplot/configuration/themes/blank.py +17 -0
  18. qplot/configuration/themes/dark.py +54 -0
  19. qplot/configuration/themes/light.py +54 -0
  20. qplot/datahandling/LoadFromDB.py +224 -0
  21. qplot/datahandling/__init__.py +47 -0
  22. qplot/datahandling/_trusted_live_helper.py +812 -0
  23. qplot/datahandling/_trusted_live_protocol.py +1745 -0
  24. qplot/datahandling/database.py +2340 -0
  25. qplot/datahandling/dimensions.py +43 -0
  26. qplot/datahandling/file_identity.py +461 -0
  27. qplot/datahandling/parameter_data.py +149 -0
  28. qplot/datahandling/plot_progress.py +17 -0
  29. qplot/datahandling/qcodes_cache.py +277 -0
  30. qplot/datahandling/qcodes_compat.py +14 -0
  31. qplot/datahandling/readSQL.py +2442 -0
  32. qplot/datahandling/readonly.py +2566 -0
  33. qplot/datahandling/source_activity.py +21 -0
  34. qplot/datahandling/trusted_array_preview.py +148 -0
  35. qplot/datahandling/trusted_derived_cache.py +1605 -0
  36. qplot/datahandling/trusted_derived_rendering.py +876 -0
  37. qplot/datahandling/trusted_heatmap.py +246 -0
  38. qplot/datahandling/trusted_live.py +2808 -0
  39. qplot/datahandling/trusted_live_queries.py +5907 -0
  40. qplot/datahandling/trusted_live_service.py +1919 -0
  41. qplot/datahandling/trusted_live_supervisor.py +2161 -0
  42. qplot/datahandling/trusted_plot.py +283 -0
  43. qplot/datahandling/trusted_presentation.py +1065 -0
  44. qplot/datahandling/trusted_snapshot.py +1132 -0
  45. qplot/datahandling/trusted_work_coordinator.py +2157 -0
  46. qplot/datahandling/trusted_work_scheduler.py +1204 -0
  47. qplot/diagnostics.py +241 -0
  48. qplot/resources/testdata/qplot_test_db_01_10mb.csv +11 -0
  49. qplot/resources/testdata/qplot_test_db_02_25mb.csv +14 -0
  50. qplot/resources/testdata/qplot_test_db_03_50mb.csv +17 -0
  51. qplot/resources/testdata/qplot_test_db_04_100mb.csv +20 -0
  52. qplot/resources/testdata/qplot_test_db_05_250mb.csv +23 -0
  53. qplot/resources/testdata/qplot_test_db_06_500mb.csv +26 -0
  54. qplot/resources/testdata/qplot_test_db_07_1gb.csv +29 -0
  55. qplot/resources/testdata/qplot_test_db_08_5gb.csv +32 -0
  56. qplot/resources/testdata/qplot_test_db_09_10gb.csv +35 -0
  57. qplot/resources/testdata/qplot_test_db_10_30gb.csv +38 -0
  58. qplot/testdata.py +1804 -0
  59. qplot/tools/__init__.py +13 -0
  60. qplot/tools/finite_means.py +123 -0
  61. qplot/tools/general.py +139 -0
  62. qplot/tools/heatmap_geometry.py +391 -0
  63. qplot/tools/operation_registry.py +179 -0
  64. qplot/tools/plot_tools.py +650 -0
  65. qplot/tools/sample_statistics.py +111 -0
  66. qplot/tools/worker.py +2838 -0
  67. qplot/windows/__init__.py +9 -0
  68. qplot/windows/_colorbar.py +422 -0
  69. qplot/windows/_commands.py +523 -0
  70. qplot/windows/_config_persistence.py +123 -0
  71. qplot/windows/_database_actions.py +4933 -0
  72. qplot/windows/_dataset_handle.py +221 -0
  73. qplot/windows/_display_precision.py +48 -0
  74. qplot/windows/_dragdrop.py +70 -0
  75. qplot/windows/_export_paths.py +1167 -0
  76. qplot/windows/_help.py +210 -0
  77. qplot/windows/_mixed_appearance.py +303 -0
  78. qplot/windows/_mixed_plots.py +461 -0
  79. qplot/windows/_native_averaging.py +323 -0
  80. qplot/windows/_native_transforms.py +551 -0
  81. qplot/windows/_plot1d_snap.py +612 -0
  82. qplot/windows/_plot1d_traces.py +2236 -0
  83. qplot/windows/_plot2d_colorbar.py +1127 -0
  84. qplot/windows/_plot2d_colorbar_dialog.py +786 -0
  85. qplot/windows/_plot2d_layers.py +2291 -0
  86. qplot/windows/_plot2d_sweeps.py +712 -0
  87. qplot/windows/_plotWin.py +2082 -0
  88. qplot/windows/_plot_actions.py +2703 -0
  89. qplot/windows/_plot_annotation.py +186 -0
  90. qplot/windows/_plot_appearance.py +283 -0
  91. qplot/windows/_plot_axis_scaling.py +1521 -0
  92. qplot/windows/_plot_data_viewbox.py +94 -0
  93. qplot/windows/_plot_drop_axes.py +224 -0
  94. qplot/windows/_plot_export.py +1445 -0
  95. qplot/windows/_plot_feedback.py +112 -0
  96. qplot/windows/_plot_marquee.py +659 -0
  97. qplot/windows/_plot_refresh.py +950 -0
  98. qplot/windows/_plot_source_promotion.py +178 -0
  99. qplot/windows/_plot_state.py +237 -0
  100. qplot/windows/_plot_status.py +115 -0
  101. qplot/windows/_plot_tiling.py +35 -0
  102. qplot/windows/_plot_transform_labels.py +60 -0
  103. qplot/windows/_preferences.py +724 -0
  104. qplot/windows/_refresh_interval.py +172 -0
  105. qplot/windows/_run_controls.py +789 -0
  106. qplot/windows/_shortcuts.py +47 -0
  107. qplot/windows/_subplots/__init__.py +8 -0
  108. qplot/windows/_subplots/subplot1d.py +532 -0
  109. qplot/windows/_subplots/subplot2d.py +919 -0
  110. qplot/windows/_svg_export.py +209 -0
  111. qplot/windows/_trusted_derived_qt.py +1759 -0
  112. qplot/windows/_widgets/__init__.py +23 -0
  113. qplot/windows/_widgets/_run_formatting.py +387 -0
  114. qplot/windows/_widgets/details_tables.py +1119 -0
  115. qplot/windows/_widgets/dropbox.py +290 -0
  116. qplot/windows/_widgets/operations.py +460 -0
  117. qplot/windows/_widgets/preview.py +2997 -0
  118. qplot/windows/_widgets/run_list_items.py +488 -0
  119. qplot/windows/_widgets/toolbar.py +111 -0
  120. qplot/windows/_widgets/treeWidgets.py +2572 -0
  121. qplot/windows/_window_controls.py +414 -0
  122. qplot/windows/main.py +1632 -0
  123. qplot/windows/plot1d.py +313 -0
  124. qplot/windows/plot2d.py +1935 -0
  125. qplotter-1.6.0.dist-info/METADATA +322 -0
  126. qplotter-1.6.0.dist-info/RECORD +130 -0
  127. qplotter-1.6.0.dist-info/WHEEL +5 -0
  128. qplotter-1.6.0.dist-info/entry_points.txt +4 -0
  129. qplotter-1.6.0.dist-info/licenses/LICENSE +21 -0
  130. qplotter-1.6.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,789 @@
1
+ import os
2
+
3
+ from PyQt6 import (
4
+ QtCore,
5
+ )
6
+ from PyQt6 import (
7
+ QtWidgets as qtw,
8
+ )
9
+ from PyQt6.QtGui import QIntValidator
10
+
11
+ from qplot.datahandling.file_identity import (
12
+ database_instance,
13
+ database_instances_differ,
14
+ )
15
+ from qplot.datahandling.trusted_live_service import TRUSTED_LIVE_MODE
16
+
17
+ from ._commands import create_action, plot_measurement_command_spec
18
+ from ._config_persistence import (
19
+ persist_config_value,
20
+ set_widget_value_without_signals,
21
+ )
22
+ from ._help import show_quick_start
23
+ from ._refresh_interval import (
24
+ RefreshIntervalSpinBox,
25
+ connect_refresh_interval_edits,
26
+ refresh_interval_value,
27
+ remember_refresh_interval,
28
+ set_refresh_interval,
29
+ )
30
+ from ._widgets import (
31
+ RunList,
32
+ moreInfo,
33
+ )
34
+ from ._widgets._run_formatting import run_is_complete
35
+
36
+ AUTO_PLOT_KEY = "user_preference.auto_plot"
37
+
38
+
39
+ def _run_timestamp_sort_key(metadata):
40
+ try:
41
+ return float(metadata.get("run_timestamp") or 0)
42
+ except (TypeError, ValueError):
43
+ return 0.0
44
+
45
+
46
+ class RunControlsMixin:
47
+ """
48
+ Main-window controls for run selection, refresh, and empty-state display.
49
+
50
+ Expects the host window to provide database and plotting actions such as
51
+ refreshMain, openRun, openPlot, and exportRunCsv.
52
+
53
+ """
54
+
55
+ def initRefresh(self):
56
+ """
57
+ Initialise the main window refresh controls.
58
+
59
+ Refresh checks for any new runs added to the dataset.
60
+
61
+ """
62
+ self.spinBox = RefreshIntervalSpinBox(
63
+ refresh_description="How often the run list checks for new runs.",
64
+ )
65
+ self.spinBox.setObjectName("refreshIntervalSpin")
66
+ self.spinBox.setRange(0.0, 10_000.0)
67
+ self.spinBox.setSingleStep(0.1)
68
+ self.spinBox.setDecimals(1)
69
+ self.spinBox.setSuffix(" s")
70
+ self.spinBox.setSizePolicy(
71
+ qtw.QSizePolicy.Policy.Fixed, qtw.QSizePolicy.Policy.Fixed
72
+ )
73
+ self.spinBox.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight)
74
+ set_refresh_interval(
75
+ self.spinBox, self.config.get("user_preference.default_refresh_rate"),
76
+ )
77
+
78
+ self._automatic_refresh_epoch = 0
79
+ self._automatic_refresh_load_generation = None
80
+ self._automatic_refresh_instance = None
81
+ self._automatic_refresh_shutdown = False
82
+ connect_refresh_interval_edits(self.spinBox, self.monitorIntervalChanged)
83
+ # Keep exactly one connection. The timeout is deliberately routed
84
+ # through a lifecycle guard instead of directly to refreshMain:
85
+ # stopping a QTimer does not make a timeout already queued on the GUI
86
+ # event loop disappear.
87
+ self.monitor.timeout.connect(self._automatic_refresh_timeout)
88
+
89
+ self.autoPlotBox = qtw.QCheckBox()
90
+ self.autoPlotBox.setChecked(self.config.get(AUTO_PLOT_KEY))
91
+ self.autoPlotBox.setToolTip(
92
+ "Open newly detected runs and the newest running run when enabled"
93
+ )
94
+ self.autoPlotBox.toggled.connect(self._auto_plot_changed)
95
+
96
+ self.refreshDatabaseButton = qtw.QToolButton()
97
+ self.refreshDatabaseButton.setObjectName("refreshIconButton")
98
+ self.refreshDatabaseButton.setIcon(
99
+ self.style().standardIcon(qtw.QStyle.StandardPixmap.SP_BrowserReload)
100
+ )
101
+ self.refreshDatabaseButton.setToolTip("Refresh the database run list (R)")
102
+ self.refreshDatabaseButton.setAccessibleName("Refresh database")
103
+ self.refreshDatabaseButton.setFixedSize(28, 26)
104
+ self.refreshDatabaseButton.clicked.connect(self.refreshMain)
105
+
106
+ self.closeAllPlotsButton = qtw.QToolButton()
107
+ self.closeAllPlotsButton.setObjectName("closeAllPlotsButton")
108
+ self.closeAllPlotsButton.setIcon(
109
+ self.style().standardIcon(qtw.QStyle.StandardPixmap.SP_TitleBarCloseButton)
110
+ )
111
+ self.closeAllPlotsButton.setToolTip("Close all plot windows (Ctrl+Shift+W)")
112
+ self.closeAllPlotsButton.setAccessibleName("Close all plot windows")
113
+ self.closeAllPlotsButton.setFixedSize(28, 26)
114
+ self.closeAllPlotsButton.clicked.connect(self.closeAll)
115
+
116
+ def initRunDisplay(self):
117
+ sublayout = qtw.QHBoxLayout()
118
+ sublayout.setContentsMargins(8, 0, 8, 2)
119
+ sublayout.setSpacing(6)
120
+
121
+ sublayout.addWidget(qtw.QLabel("ID:"))
122
+
123
+ self.selected_run_id = None
124
+
125
+ self.run_idBox = qtw.QLineEdit()
126
+ self.run_idBox.setMaximumWidth(58)
127
+ self.run_idBox.setFixedWidth(58)
128
+ self.run_idBox.setValidator(QIntValidator())
129
+ self.run_idBox.setPlaceholderText("ID")
130
+ self.run_idBox.setToolTip("Run ID to plot")
131
+ self.run_idBox.textEdited.connect(self.update_run_id)
132
+ self.run_idBox.editingFinished.connect(self.sync_run_id_selection)
133
+ self.run_idBox.returnPressed.connect(self.openRun)
134
+ sublayout.addWidget(self.run_idBox)
135
+
136
+ sublayout.addWidget(qtw.QLabel("Measurement:"))
137
+
138
+ self.measurementBox = qtw.QLineEdit()
139
+ self.measurementBox.setMaximumWidth(46)
140
+ self.measurementBox.setFixedWidth(46)
141
+ self.measurementBox.setText("*")
142
+ self.measurementBox.setToolTip("Measurement to plot; * to plot all")
143
+ self.measurementBox.returnPressed.connect(self.openRun)
144
+ sublayout.addWidget(self.measurementBox)
145
+
146
+ self.plotRunButton = qtw.QToolButton()
147
+ self.plotRunButton.setObjectName("plotIconButton")
148
+ self.plotRunButton.setIcon(
149
+ self.style().standardIcon(qtw.QStyle.StandardPixmap.SP_MediaPlay)
150
+ )
151
+ self.plotRunButton.setToolTip("Plot (Ctrl+Return)")
152
+ self.plotRunButton.setAccessibleName("Plot measurement")
153
+ self.plotRunButton.setFixedSize(28, 26)
154
+ self.plotRunButton.clicked.connect(self.openRun)
155
+ sublayout.addWidget(self.plotRunButton)
156
+
157
+ self.exportCsvButton = qtw.QToolButton()
158
+ self.exportCsvButton.setObjectName("exportIconButton")
159
+ self.exportCsvButton.setIcon(
160
+ self.style().standardIcon(qtw.QStyle.StandardPixmap.SP_DialogSaveButton)
161
+ )
162
+ self.exportCsvButton.setToolTip("Export CSV")
163
+ self.exportCsvButton.setAccessibleName("Export measurement to CSV")
164
+ self.exportCsvButton.setFixedSize(28, 26)
165
+ self.exportCsvButton.clicked.connect(self.exportRunCsv)
166
+ sublayout.addWidget(self.exportCsvButton)
167
+
168
+ sublayout.addStretch()
169
+
170
+ sublayout.addWidget(qtw.QLabel("Auto-plot"))
171
+ sublayout.addWidget(self.autoPlotBox)
172
+
173
+ sublayout.addSpacing(12)
174
+ sublayout.addWidget(qtw.QLabel("Refresh:"))
175
+ sublayout.addWidget(self.spinBox)
176
+ sublayout.addWidget(self.refreshDatabaseButton)
177
+
178
+ self.l.addLayout(self.targetLayout)
179
+ self.l.addWidget(self.databaseLoadFrame)
180
+ self.l.addLayout(sublayout)
181
+
182
+ self.RunList = RunList(config=self.config)
183
+ self.RunList.selected.connect(self.updateSelected)
184
+ self.RunList.nonSingleSelection.connect(self.clear_non_single_run_selection)
185
+ self.RunList.plot.connect(self.openPlot)
186
+ self.RunList.previewPlotRequested.connect(self.open_run_preview_plot)
187
+ self.RunList.previewExportRequested.connect(self.export_run_preview_csv)
188
+ self.RunList.verticalScrollBar().valueChanged.connect(
189
+ lambda _: self._run_table_view_changed()
190
+ )
191
+
192
+ self.infoBox = moreInfo(preview_size=self.preview_size)
193
+ self.infoBox.preview.plotRequested.connect(self.open_preview_plot)
194
+ self.infoBox.preview.exportRequested.connect(self.export_preview_csv)
195
+ self.infoBox.preview.previewsReady.connect(self.RunList.set_run_previews)
196
+ self.infoBox.preview.previewGenerationChanged.connect(
197
+ self.RunList.set_run_preview_generating
198
+ )
199
+ self.infoBox.preview.databaseReplaced.connect(
200
+ self._reload_replaced_database
201
+ )
202
+ if self.fileTextbox.text() and self.RunList.topLevelItemCount():
203
+ self.infoBox.preview.set_database_runs(
204
+ self.fileTextbox.text(),
205
+ self.RunList.all_run_metadata(),
206
+ )
207
+ self._prioritize_preview_runs()
208
+
209
+ self._init_empty_state()
210
+ self.runInfoSplitter = qtw.QSplitter(QtCore.Qt.Orientation.Vertical)
211
+ self.runInfoSplitter.setHandleWidth(8)
212
+ self.runInfoSplitter.setChildrenCollapsible(True)
213
+ self.runInfoSplitter.setOpaqueResize(True)
214
+ self.runInfoSplitter.addWidget(self.RunList)
215
+ self.runInfoSplitter.addWidget(self.infoBox)
216
+ self.runInfoSplitter.setCollapsible(0, False)
217
+ self.runInfoSplitter.setCollapsible(1, True)
218
+ self.runInfoSplitter.setStretchFactor(0, 3)
219
+ self.runInfoSplitter.setStretchFactor(1, 2)
220
+ self.runInfoSplitter.setSizes([380, self._details_pane_height()])
221
+ self.runInfoSplitter.handle(1).setToolTip(
222
+ "Drag to resize the run list and details panes"
223
+ )
224
+ self.l.addWidget(self.emptyStateFrame)
225
+ self.l.addWidget(self.runInfoSplitter, 1)
226
+ self._sync_empty_state()
227
+
228
+ def initShortcuts(self):
229
+ """
230
+ Register keyboard shortcuts for common run actions.
231
+
232
+ """
233
+ plot_entered = create_action("run.plot_entered", self)
234
+ plot_entered.triggered.connect(lambda _: self.plotRunButton.click())
235
+ self.addAction(plot_entered)
236
+
237
+ plot_selected_all = create_action("run.plot_selected_all", self)
238
+ plot_selected_all.triggered.connect(self.open_selected_run_all)
239
+ self.addAction(plot_selected_all)
240
+
241
+ self.open_param_actions = []
242
+ for itr in range(9):
243
+ action = create_action(plot_measurement_command_spec(itr), self)
244
+ action.triggered.connect(lambda _, index=itr: self.open_param_by_index(index))
245
+ self.addAction(action)
246
+ self.open_param_actions.append(action)
247
+
248
+ def _init_empty_state(self):
249
+ """
250
+ Create the empty-database prompt shown before any runs are available.
251
+
252
+ """
253
+ self.emptyStateFrame = qtw.QFrame()
254
+ self.emptyStateFrame.setObjectName("mainEmptyState")
255
+ self.emptyStateFrame.setFrameShape(qtw.QFrame.Shape.NoFrame)
256
+ layout = qtw.QHBoxLayout(self.emptyStateFrame)
257
+ layout.setContentsMargins(8, 8, 8, 8)
258
+ layout.setSpacing(10)
259
+
260
+ icon = qtw.QLabel()
261
+ icon.setPixmap(
262
+ self.style().standardIcon(qtw.QStyle.StandardPixmap.SP_DialogOpenButton).pixmap(24, 24)
263
+ )
264
+ layout.addWidget(icon)
265
+
266
+ text_layout = qtw.QVBoxLayout()
267
+ text_layout.setContentsMargins(0, 0, 0, 0)
268
+ text_layout.setSpacing(2)
269
+
270
+ title = qtw.QLabel("No database loaded")
271
+ title.setObjectName("mainEmptyStateTitle")
272
+ text_layout.addWidget(title)
273
+ self.emptyStateTitle = title
274
+
275
+ detail = qtw.QLabel(
276
+ "Drop a QCoDeS .db file onto the database field, or load one now."
277
+ )
278
+ detail.setObjectName("mainEmptyStateDetail")
279
+ detail.setWordWrap(True)
280
+ text_layout.addWidget(detail)
281
+ self.emptyStateDetail = detail
282
+ layout.addLayout(text_layout, 1)
283
+
284
+ load_button = qtw.QToolButton()
285
+ load_button.setObjectName("databaseIconButton")
286
+ load_button.setIcon(self.style().standardIcon(qtw.QStyle.StandardPixmap.SP_DialogOpenButton))
287
+ load_button.setText("Load Database...")
288
+ load_button.setToolButtonStyle(QtCore.Qt.ToolButtonStyle.ToolButtonTextBesideIcon)
289
+ load_button.setToolTip("Load a QCoDeS .db database")
290
+ load_button.setAccessibleName("Load database")
291
+ load_button.clicked.connect(self.getfile)
292
+ layout.addWidget(load_button)
293
+ self.emptyStateLoadButton = load_button
294
+
295
+ refresh_button = qtw.QToolButton()
296
+ refresh_button.setObjectName("databaseIconButton")
297
+ refresh_button.setIcon(self.style().standardIcon(qtw.QStyle.StandardPixmap.SP_BrowserReload))
298
+ refresh_button.setText("Refresh")
299
+ refresh_button.setToolButtonStyle(QtCore.Qt.ToolButtonStyle.ToolButtonTextBesideIcon)
300
+ refresh_button.setToolTip("Check for new measurements")
301
+ refresh_button.setAccessibleName("Refresh database")
302
+ refresh_button.clicked.connect(self.refreshMain)
303
+ layout.addWidget(refresh_button)
304
+ self.emptyStateRefreshButton = refresh_button
305
+
306
+ help_button = qtw.QToolButton()
307
+ help_button.setObjectName("databaseIconButton")
308
+ help_button.setIcon(self.style().standardIcon(qtw.QStyle.StandardPixmap.SP_DialogHelpButton))
309
+ help_button.setText("Quick Start")
310
+ help_button.setToolButtonStyle(QtCore.Qt.ToolButtonStyle.ToolButtonTextBesideIcon)
311
+ help_button.setToolTip("Show the basic qPlot workflow")
312
+ help_button.setAccessibleName("Show quick start")
313
+ help_button.clicked.connect(lambda: show_quick_start(self))
314
+ layout.addWidget(help_button)
315
+ self.emptyStateHelpButton = help_button
316
+
317
+ def _sync_empty_state(self):
318
+ """
319
+ Show the empty prompt while no runs are available.
320
+
321
+ """
322
+ frame = getattr(self, "emptyStateFrame", None)
323
+ if frame is None:
324
+ return
325
+
326
+ database_path = ""
327
+ if hasattr(self, "fileTextbox"):
328
+ database_path = self.fileTextbox.text()
329
+
330
+ run_count = 0
331
+ run_list = getattr(self, "RunList", None)
332
+ if run_list is not None and hasattr(run_list, "topLevelItemCount"):
333
+ run_count = run_list.topLevelItemCount()
334
+
335
+ loading = getattr(self, "_database_load_active", False)
336
+ has_runs = run_count > 0
337
+ frame.setVisible(not loading and not has_runs)
338
+
339
+ if loading or has_runs:
340
+ return
341
+
342
+ if database_path:
343
+ sync_loaded = getattr(self, "_sync_loaded_empty_state", None)
344
+ if callable(sync_loaded):
345
+ sync_loaded(database_path)
346
+ else:
347
+ sync_empty = getattr(self, "_sync_no_database_empty_state", None)
348
+ if callable(sync_empty):
349
+ sync_empty()
350
+
351
+ def _sync_no_database_empty_state(self):
352
+ title = getattr(self, "emptyStateTitle", None)
353
+ if title is not None:
354
+ title.setText("No database loaded")
355
+
356
+ detail = getattr(self, "emptyStateDetail", None)
357
+ if detail is not None:
358
+ detail.setText(
359
+ "Drop a QCoDeS .db file onto the database field, or load one now."
360
+ )
361
+
362
+ self._set_empty_state_button_visible("emptyStateLoadButton", True)
363
+ self._set_empty_state_button_visible("emptyStateRefreshButton", False)
364
+ self._set_empty_state_button_visible("emptyStateHelpButton", True)
365
+
366
+ def _sync_loaded_empty_state(self, database_path):
367
+ title = getattr(self, "emptyStateTitle", None)
368
+ if title is not None:
369
+ title.setText("Waiting for measurements")
370
+
371
+ detail = getattr(self, "emptyStateDetail", None)
372
+ if detail is not None:
373
+ detail.setText(self._loaded_empty_database_detail(database_path))
374
+
375
+ self._set_empty_state_button_visible("emptyStateLoadButton", True)
376
+ self._set_empty_state_button_visible("emptyStateRefreshButton", True)
377
+ self._set_empty_state_button_visible("emptyStateHelpButton", False)
378
+
379
+ def _set_empty_state_button_visible(self, attr, visible):
380
+ button = getattr(self, attr, None)
381
+ if button is not None:
382
+ button.setVisible(visible)
383
+
384
+ def _loaded_empty_database_detail(self, database_path):
385
+ basename = database_path
386
+ if database_path:
387
+ basename = os.path.basename(database_path) or database_path
388
+
389
+ interval = self._current_refresh_interval()
390
+ if interval > 0:
391
+ return (
392
+ f"{basename} is loaded. qPlot will add measurements as they "
393
+ f"appear, checking every {interval:g} s."
394
+ )
395
+
396
+ return (
397
+ f"{basename} is loaded. Refresh is set to manual; press Refresh "
398
+ "to check for measurements."
399
+ )
400
+
401
+ def _current_refresh_interval(self):
402
+ spin_box = getattr(self, "spinBox", None)
403
+ if spin_box is None or not hasattr(spin_box, "value"):
404
+ return 0.0
405
+
406
+ try:
407
+ return refresh_interval_value(spin_box)
408
+ except (TypeError, ValueError):
409
+ return 0.0
410
+
411
+ @QtCore.pyqtSlot(float)
412
+ def monitorIntervalChanged(self, interval):
413
+ """
414
+ Updates the refresh interval for checking for new runs in database.
415
+
416
+ """
417
+ if not self._save_refresh_interval(interval):
418
+ self._sync_empty_state()
419
+ return
420
+ remember_refresh_interval(self.spinBox, interval)
421
+ self._apply_refresh_interval(interval)
422
+ self._sync_empty_state()
423
+
424
+ @QtCore.pyqtSlot(bool)
425
+ def _auto_plot_changed(self, checked):
426
+ """
427
+ Persists the Auto-plot checkbox state.
428
+
429
+ """
430
+ previous_checked = self.config.get(AUTO_PLOT_KEY)
431
+
432
+ def rollback():
433
+ checkbox = getattr(self, "autoPlotBox", None)
434
+ if checkbox is not None:
435
+ set_widget_value_without_signals(
436
+ checkbox,
437
+ checkbox.setChecked,
438
+ previous_checked,
439
+ )
440
+
441
+ if not persist_config_value(
442
+ self,
443
+ self.config,
444
+ AUTO_PLOT_KEY,
445
+ bool(checked),
446
+ "the Auto-plot preference",
447
+ rollback,
448
+ ):
449
+ return
450
+ if checked:
451
+ self._auto_plot_current_running_run()
452
+
453
+ def _auto_plot_current_running_run(self):
454
+ """
455
+ Opens the newest incomplete run already present in the run list.
456
+
457
+ """
458
+ # Stage 4 leaves plot-data materialisation on the legacy snapshot path.
459
+ # A trusted session may enter that path only through an explicit plot or
460
+ # export action; enabling a persisted preference is not such an action.
461
+ if getattr(self, "_database_access_mode", None) == TRUSTED_LIVE_MODE:
462
+ return None
463
+ generation_gate = getattr(
464
+ self,
465
+ "_database_generation_transaction_blocks_path",
466
+ None,
467
+ )
468
+ if callable(generation_gate) and generation_gate():
469
+ return None
470
+ run_list = getattr(self, "RunList", None)
471
+ if run_list is None or not hasattr(run_list, "all_run_metadata"):
472
+ return None
473
+
474
+ running_runs = [
475
+ metadata
476
+ for metadata in run_list.all_run_metadata().values()
477
+ if metadata.get("guid") and not run_is_complete(metadata)
478
+ ]
479
+ if not running_runs:
480
+ return None
481
+
482
+ metadata = max(
483
+ running_runs,
484
+ key=_run_timestamp_sort_key,
485
+ )
486
+ self.openPlot(metadata["guid"])
487
+ return metadata["guid"]
488
+
489
+ def _apply_refresh_interval(self, interval):
490
+ """
491
+ Reconcile the automatic-refresh timer with the committed DB lifecycle.
492
+
493
+ A positive preference is retained even when there is no database to
494
+ refresh. It becomes active only after a DatabaseInstance has been
495
+ accepted by database_load_finished.
496
+
497
+ """
498
+ generation_gate = getattr(
499
+ self,
500
+ "_database_generation_transaction_blocks_path",
501
+ None,
502
+ )
503
+ if callable(generation_gate) and generation_gate():
504
+ state = getattr(self, "_test_database_replacement_state", None)
505
+ if state is not None:
506
+ state.monitor_was_active = interval > 0
507
+ state.monitor_interval_ms = max(1, round(interval * 1000))
508
+ RunControlsMixin._update_automatic_refresh_timer(self, interval)
509
+
510
+ def _committed_refresh_database_instance(self):
511
+ """Return the accepted source instance, never inferring it from text."""
512
+ instance = getattr(self, "_loaded_database_instance", None)
513
+ if instance is None or getattr(instance, "identity", None) is None:
514
+ return None
515
+ return instance
516
+
517
+ def _automatic_refresh_should_run(self, interval):
518
+ """Whether the main refresh timer is presently allowed to run."""
519
+ if (
520
+ interval <= 0
521
+ or RunControlsMixin._committed_refresh_database_instance(self)
522
+ is None
523
+ ):
524
+ return False
525
+ if getattr(self, "_automatic_refresh_shutdown", False):
526
+ return False
527
+ if (
528
+ getattr(self, "_shutdown_started", False)
529
+ or getattr(self, "_shutdown_ready", False)
530
+ or getattr(self, "_database_load_active", False)
531
+ or getattr(self, "_database_view_released_for_generation", False)
532
+ ):
533
+ return False
534
+ generation_gate = getattr(
535
+ self,
536
+ "_database_generation_transaction_blocks_path",
537
+ None,
538
+ )
539
+ return not (callable(generation_gate) and generation_gate())
540
+
541
+ def _stop_automatic_refresh_timer(self):
542
+ """Stop the timer and invalidate timeout events already queued."""
543
+ self._automatic_refresh_epoch = (
544
+ getattr(self, "_automatic_refresh_epoch", 0) + 1
545
+ )
546
+ self._automatic_refresh_load_generation = None
547
+ self._automatic_refresh_instance = None
548
+ monitor = getattr(self, "monitor", None)
549
+ stop = getattr(monitor, "stop", None)
550
+ if callable(stop):
551
+ stop()
552
+
553
+ def _update_automatic_refresh_timer(self, interval=None):
554
+ """Start or pause automatic refresh according to authoritative state."""
555
+ if interval is None:
556
+ interval = self._current_refresh_interval()
557
+ try:
558
+ interval = float(interval)
559
+ except (TypeError, ValueError):
560
+ interval = 0.0
561
+
562
+ RunControlsMixin._stop_automatic_refresh_timer(self)
563
+ if not RunControlsMixin._automatic_refresh_should_run(self, interval):
564
+ return False
565
+
566
+ instance = RunControlsMixin._committed_refresh_database_instance(self)
567
+ self._automatic_refresh_load_generation = getattr(
568
+ self,
569
+ "_database_load_generation",
570
+ 0,
571
+ )
572
+ self._automatic_refresh_instance = instance
573
+ monitor = getattr(self, "monitor", None)
574
+ start = getattr(monitor, "start", None)
575
+ if not callable(start):
576
+ return False
577
+ start(max(1, round(interval * 1000)))
578
+ return True
579
+
580
+ @QtCore.pyqtSlot()
581
+ def _automatic_refresh_timeout(self):
582
+ """Refresh only if this timeout still belongs to the committed source."""
583
+ interval = self._current_refresh_interval()
584
+ expected_instance = getattr(self, "_automatic_refresh_instance", None)
585
+ if (
586
+ not RunControlsMixin._automatic_refresh_should_run(self, interval)
587
+ or expected_instance is None
588
+ or expected_instance
589
+ != RunControlsMixin._committed_refresh_database_instance(self)
590
+ or getattr(self, "_automatic_refresh_load_generation", None)
591
+ != getattr(self, "_database_load_generation", 0)
592
+ ):
593
+ RunControlsMixin._stop_automatic_refresh_timer(self)
594
+ return
595
+
596
+ # A file can disappear or be atomically replaced between timer ticks.
597
+ # Let the normal replacement lifecycle handle that, but never launch a
598
+ # refresh for the old instance.
599
+ database_path = expected_instance.logical_path
600
+ try:
601
+ current_instance = database_instance(database_path)
602
+ except OSError:
603
+ current_instance = None
604
+ if current_instance is None or database_instances_differ(
605
+ expected_instance,
606
+ current_instance,
607
+ ):
608
+ RunControlsMixin._stop_automatic_refresh_timer(self)
609
+ reload_database = getattr(self, "_reload_replaced_database", None)
610
+ if callable(reload_database):
611
+ reload_database(database_path)
612
+ return
613
+
614
+ self.refreshMain(automatic=True)
615
+
616
+ def _save_refresh_interval(self, interval):
617
+ """
618
+ Persists the main refresh interval as the user's default.
619
+
620
+ """
621
+ interval = float(interval)
622
+ current_interval = float(
623
+ self.config.get("user_preference.default_refresh_rate")
624
+ )
625
+
626
+ if current_interval == interval:
627
+ return True
628
+
629
+ def rollback():
630
+ spin_box = getattr(self, "spinBox", None)
631
+ if spin_box is not None:
632
+ set_refresh_interval(spin_box, current_interval)
633
+
634
+ return persist_config_value(
635
+ self,
636
+ self.config,
637
+ "user_preference.default_refresh_rate",
638
+ interval,
639
+ "the refresh interval",
640
+ rollback,
641
+ )
642
+
643
+ @QtCore.pyqtSlot(str)
644
+ def update_run_id(self, text):
645
+ """
646
+ Updates the run ID target entered into the run text box.
647
+
648
+ """
649
+ cancel_selected_detail = getattr(
650
+ self,
651
+ "_cancel_selected_run_detail",
652
+ None,
653
+ )
654
+ if callable(cancel_selected_detail):
655
+ cancel_selected_detail()
656
+ self._selected_run_guid = None
657
+ self.RunList.blockSignals(True)
658
+ self.RunList.clearSelection()
659
+ self.RunList.blockSignals(False)
660
+ release_selected = getattr(self, "_release_selected_dataset", None)
661
+ if callable(release_selected):
662
+ release_selected()
663
+ else:
664
+ self.ds = None
665
+ self._selected_dataset_key = None
666
+ self.infoBox.clear()
667
+ derived_bridge = getattr(self, "_trusted_derived_bridge", None)
668
+ if derived_bridge is not None:
669
+ derived_bridge.request_priority_update()
670
+
671
+ try:
672
+ self.selected_run_id = int(text)
673
+ except ValueError:
674
+ self.selected_run_id = None
675
+
676
+ @QtCore.pyqtSlot()
677
+ def sync_run_id_selection(self):
678
+ """
679
+ Selects the typed run ID in the table if it is currently visible.
680
+
681
+ """
682
+ if self.selected_run_id is None:
683
+ return
684
+
685
+ matches = self.RunList.findItems(
686
+ str(self.selected_run_id),
687
+ QtCore.Qt.MatchFlag.MatchExactly,
688
+ 0,
689
+ )
690
+ if not matches:
691
+ return
692
+
693
+ item = matches[0]
694
+ self.RunList.setCurrentItem(item)
695
+ self.RunList.scrollToItem(item, qtw.QAbstractItemView.ScrollHint.PositionAtCenter)
696
+
697
+
698
+ def _run_table_view_changed(self):
699
+ generation_gate = getattr(
700
+ self,
701
+ "_database_generation_transaction_blocks_path",
702
+ None,
703
+ )
704
+ if callable(generation_gate) and generation_gate():
705
+ return
706
+ prioritize_details = getattr(self, "_prioritize_database_detail_runs", None)
707
+ if callable(prioritize_details):
708
+ prioritize_details()
709
+ derived_bridge = getattr(self, "_trusted_derived_bridge", None)
710
+ if derived_bridge is not None:
711
+ derived_bridge.request_priority_update()
712
+ self._prioritize_preview_runs()
713
+
714
+
715
+ def _prioritize_preview_runs(self, run_ids=None):
716
+ generation_gate = getattr(
717
+ self,
718
+ "_database_generation_transaction_blocks_path",
719
+ None,
720
+ )
721
+ if callable(generation_gate) and generation_gate():
722
+ return
723
+ if getattr(self, "_database_access_mode", None) == TRUSTED_LIVE_MODE:
724
+ derived_bridge = getattr(self, "_trusted_derived_bridge", None)
725
+ if derived_bridge is not None:
726
+ derived_bridge.request_priority_update()
727
+ return
728
+ preview = getattr(getattr(self, "infoBox", None), "preview", None)
729
+ prioritize = getattr(preview, "prioritize_runs", None)
730
+ if not callable(prioritize):
731
+ return
732
+
733
+ selected_run_ids, visible_run_ids = self._preview_priority_run_ids(run_ids)
734
+ prioritize(
735
+ selected_run_ids=selected_run_ids,
736
+ visible_run_ids=visible_run_ids,
737
+ )
738
+
739
+
740
+ def _preview_priority_run_ids(self, run_ids=None):
741
+ selected_run_ids = []
742
+ visible_run_ids = []
743
+ seen_selected = set()
744
+ seen_visible = set()
745
+
746
+ def add(target, seen, candidate):
747
+ if candidate is None:
748
+ return
749
+ try:
750
+ key = int(candidate)
751
+ except (TypeError, ValueError):
752
+ key = candidate
753
+ if key in seen:
754
+ return
755
+ target.append(candidate)
756
+ seen.add(key)
757
+
758
+ if isinstance(run_ids, (list, tuple, set)):
759
+ for run_id in run_ids:
760
+ add(selected_run_ids, seen_selected, run_id)
761
+ else:
762
+ add(selected_run_ids, seen_selected, run_ids)
763
+
764
+ run_list = getattr(self, "RunList", None)
765
+ selected = getattr(run_list, "selected_run_ids", None)
766
+ if callable(selected):
767
+ for run_id in selected():
768
+ add(selected_run_ids, seen_selected, run_id)
769
+
770
+ visible = getattr(run_list, "visible_run_ids", None)
771
+ if callable(visible):
772
+ for run_id in visible():
773
+ add(visible_run_ids, seen_visible, run_id)
774
+
775
+ return selected_run_ids, visible_run_ids
776
+
777
+ def _sync_refresh_interval(self):
778
+ interval = self.config.get("user_preference.default_refresh_rate")
779
+ if not hasattr(self, "spinBox"):
780
+ return
781
+
782
+ set_refresh_interval(self.spinBox, interval)
783
+ self._apply_refresh_interval(interval)
784
+
785
+ def _configured_preview_size(self):
786
+ return int(self.config.get("GUI.preview_size"))
787
+
788
+ def _details_pane_height(self):
789
+ return max(260, int(self.preview_size) + 84)