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.
- qplot/__init__.py +46 -0
- qplot/__main__.py +232 -0
- qplot/_auxiliary_paths.py +67 -0
- qplot/_build_info.json +1 -0
- qplot/_metadata.py +75 -0
- qplot/_repair.py +14 -0
- qplot/_shutdown_supervisor.py +4049 -0
- qplot/_version.py +12 -0
- qplot/_windows_shutdown_job.py +1166 -0
- qplot/configuration/__init__.py +3 -0
- qplot/configuration/config.py +578 -0
- qplot/configuration/config_schema.json +357 -0
- qplot/configuration/fonts.py +3 -0
- qplot/configuration/scripts.py +255 -0
- qplot/configuration/themes/__init__.py +13 -0
- qplot/configuration/themes/_base.py +592 -0
- qplot/configuration/themes/blank.py +17 -0
- qplot/configuration/themes/dark.py +54 -0
- qplot/configuration/themes/light.py +54 -0
- qplot/datahandling/LoadFromDB.py +224 -0
- qplot/datahandling/__init__.py +47 -0
- qplot/datahandling/_trusted_live_helper.py +812 -0
- qplot/datahandling/_trusted_live_protocol.py +1745 -0
- qplot/datahandling/database.py +2340 -0
- qplot/datahandling/dimensions.py +43 -0
- qplot/datahandling/file_identity.py +461 -0
- qplot/datahandling/parameter_data.py +149 -0
- qplot/datahandling/plot_progress.py +17 -0
- qplot/datahandling/qcodes_cache.py +277 -0
- qplot/datahandling/qcodes_compat.py +14 -0
- qplot/datahandling/readSQL.py +2442 -0
- qplot/datahandling/readonly.py +2566 -0
- qplot/datahandling/source_activity.py +21 -0
- qplot/datahandling/trusted_array_preview.py +148 -0
- qplot/datahandling/trusted_derived_cache.py +1605 -0
- qplot/datahandling/trusted_derived_rendering.py +876 -0
- qplot/datahandling/trusted_heatmap.py +246 -0
- qplot/datahandling/trusted_live.py +2808 -0
- qplot/datahandling/trusted_live_queries.py +5907 -0
- qplot/datahandling/trusted_live_service.py +1919 -0
- qplot/datahandling/trusted_live_supervisor.py +2161 -0
- qplot/datahandling/trusted_plot.py +283 -0
- qplot/datahandling/trusted_presentation.py +1065 -0
- qplot/datahandling/trusted_snapshot.py +1132 -0
- qplot/datahandling/trusted_work_coordinator.py +2157 -0
- qplot/datahandling/trusted_work_scheduler.py +1204 -0
- qplot/diagnostics.py +241 -0
- qplot/resources/testdata/qplot_test_db_01_10mb.csv +11 -0
- qplot/resources/testdata/qplot_test_db_02_25mb.csv +14 -0
- qplot/resources/testdata/qplot_test_db_03_50mb.csv +17 -0
- qplot/resources/testdata/qplot_test_db_04_100mb.csv +20 -0
- qplot/resources/testdata/qplot_test_db_05_250mb.csv +23 -0
- qplot/resources/testdata/qplot_test_db_06_500mb.csv +26 -0
- qplot/resources/testdata/qplot_test_db_07_1gb.csv +29 -0
- qplot/resources/testdata/qplot_test_db_08_5gb.csv +32 -0
- qplot/resources/testdata/qplot_test_db_09_10gb.csv +35 -0
- qplot/resources/testdata/qplot_test_db_10_30gb.csv +38 -0
- qplot/testdata.py +1804 -0
- qplot/tools/__init__.py +13 -0
- qplot/tools/finite_means.py +123 -0
- qplot/tools/general.py +139 -0
- qplot/tools/heatmap_geometry.py +391 -0
- qplot/tools/operation_registry.py +179 -0
- qplot/tools/plot_tools.py +650 -0
- qplot/tools/sample_statistics.py +111 -0
- qplot/tools/worker.py +2838 -0
- qplot/windows/__init__.py +9 -0
- qplot/windows/_colorbar.py +422 -0
- qplot/windows/_commands.py +523 -0
- qplot/windows/_config_persistence.py +123 -0
- qplot/windows/_database_actions.py +4933 -0
- qplot/windows/_dataset_handle.py +221 -0
- qplot/windows/_display_precision.py +48 -0
- qplot/windows/_dragdrop.py +70 -0
- qplot/windows/_export_paths.py +1167 -0
- qplot/windows/_help.py +210 -0
- qplot/windows/_mixed_appearance.py +303 -0
- qplot/windows/_mixed_plots.py +461 -0
- qplot/windows/_native_averaging.py +323 -0
- qplot/windows/_native_transforms.py +551 -0
- qplot/windows/_plot1d_snap.py +612 -0
- qplot/windows/_plot1d_traces.py +2236 -0
- qplot/windows/_plot2d_colorbar.py +1127 -0
- qplot/windows/_plot2d_colorbar_dialog.py +786 -0
- qplot/windows/_plot2d_layers.py +2291 -0
- qplot/windows/_plot2d_sweeps.py +712 -0
- qplot/windows/_plotWin.py +2082 -0
- qplot/windows/_plot_actions.py +2703 -0
- qplot/windows/_plot_annotation.py +186 -0
- qplot/windows/_plot_appearance.py +283 -0
- qplot/windows/_plot_axis_scaling.py +1521 -0
- qplot/windows/_plot_data_viewbox.py +94 -0
- qplot/windows/_plot_drop_axes.py +224 -0
- qplot/windows/_plot_export.py +1445 -0
- qplot/windows/_plot_feedback.py +112 -0
- qplot/windows/_plot_marquee.py +659 -0
- qplot/windows/_plot_refresh.py +950 -0
- qplot/windows/_plot_source_promotion.py +178 -0
- qplot/windows/_plot_state.py +237 -0
- qplot/windows/_plot_status.py +115 -0
- qplot/windows/_plot_tiling.py +35 -0
- qplot/windows/_plot_transform_labels.py +60 -0
- qplot/windows/_preferences.py +724 -0
- qplot/windows/_refresh_interval.py +172 -0
- qplot/windows/_run_controls.py +789 -0
- qplot/windows/_shortcuts.py +47 -0
- qplot/windows/_subplots/__init__.py +8 -0
- qplot/windows/_subplots/subplot1d.py +532 -0
- qplot/windows/_subplots/subplot2d.py +919 -0
- qplot/windows/_svg_export.py +209 -0
- qplot/windows/_trusted_derived_qt.py +1759 -0
- qplot/windows/_widgets/__init__.py +23 -0
- qplot/windows/_widgets/_run_formatting.py +387 -0
- qplot/windows/_widgets/details_tables.py +1119 -0
- qplot/windows/_widgets/dropbox.py +290 -0
- qplot/windows/_widgets/operations.py +460 -0
- qplot/windows/_widgets/preview.py +2997 -0
- qplot/windows/_widgets/run_list_items.py +488 -0
- qplot/windows/_widgets/toolbar.py +111 -0
- qplot/windows/_widgets/treeWidgets.py +2572 -0
- qplot/windows/_window_controls.py +414 -0
- qplot/windows/main.py +1632 -0
- qplot/windows/plot1d.py +313 -0
- qplot/windows/plot2d.py +1935 -0
- qplotter-1.6.0.dist-info/METADATA +322 -0
- qplotter-1.6.0.dist-info/RECORD +130 -0
- qplotter-1.6.0.dist-info/WHEEL +5 -0
- qplotter-1.6.0.dist-info/entry_points.txt +4 -0
- qplotter-1.6.0.dist-info/licenses/LICENSE +21 -0
- 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)
|