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,488 @@
|
|
|
1
|
+
from PyQt6 import QtCore, QtGui
|
|
2
|
+
from PyQt6 import QtWidgets as qtw
|
|
3
|
+
|
|
4
|
+
from ._run_formatting import one_dimensional_duplicate_point_count, run_tooltip_text
|
|
5
|
+
from .preview import (
|
|
6
|
+
DraggablePreviewImageLabel,
|
|
7
|
+
PreviewImageLabel,
|
|
8
|
+
preview_placeholder_dimensions,
|
|
9
|
+
unsupported_preview_label,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
MEASUREMENT_PREVIEW_SIZE = 22
|
|
13
|
+
MEASUREMENT_PREVIEW_SPACING = 3
|
|
14
|
+
MEASUREMENT_PREVIEW_GENERATING_COLORS = (
|
|
15
|
+
"#e9f4ff",
|
|
16
|
+
"#edf7f1",
|
|
17
|
+
"#fff4e3",
|
|
18
|
+
"#f4efff",
|
|
19
|
+
"#eef7f8",
|
|
20
|
+
"#f8eff4",
|
|
21
|
+
)
|
|
22
|
+
MEASUREMENT_PREVIEW_GENERATING_BORDERS = (
|
|
23
|
+
"#bdd8f2",
|
|
24
|
+
"#c5dfcc",
|
|
25
|
+
"#ead3aa",
|
|
26
|
+
"#d4c8ee",
|
|
27
|
+
"#bfdadd",
|
|
28
|
+
"#e0c3d1",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class RunPreviewCell(qtw.QWidget):
|
|
33
|
+
plotRequested = QtCore.pyqtSignal(str, str)
|
|
34
|
+
exportRequested = QtCore.pyqtSignal(str, str)
|
|
35
|
+
|
|
36
|
+
def __init__(self, guid, count, parent=None, icon_size=MEASUREMENT_PREVIEW_SIZE):
|
|
37
|
+
super().__init__(parent)
|
|
38
|
+
self.guid = guid
|
|
39
|
+
self.placeholder_count = max(0, int(count or 0))
|
|
40
|
+
self.icon_size = int(icon_size)
|
|
41
|
+
self._generating = False
|
|
42
|
+
self._has_rendered_previews = False
|
|
43
|
+
self._placeholder_dimensions = []
|
|
44
|
+
self._placeholder_parameters = []
|
|
45
|
+
self._previews = []
|
|
46
|
+
self._pending_display = None
|
|
47
|
+
|
|
48
|
+
self.content_layout = qtw.QHBoxLayout()
|
|
49
|
+
self.content_layout.setContentsMargins(2, 0, 2, 0)
|
|
50
|
+
self.content_layout.setSpacing(MEASUREMENT_PREVIEW_SPACING)
|
|
51
|
+
self.content_layout.setAlignment(
|
|
52
|
+
QtCore.Qt.AlignmentFlag.AlignLeft
|
|
53
|
+
| QtCore.Qt.AlignmentFlag.AlignVCenter
|
|
54
|
+
)
|
|
55
|
+
self.setLayout(self.content_layout)
|
|
56
|
+
self.setFixedHeight(self.icon_size + 6)
|
|
57
|
+
self.show_placeholders()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def show_placeholders(self, count=None, generating=None):
|
|
61
|
+
if self._defer_display_during_drag(("placeholders", count, generating)):
|
|
62
|
+
return
|
|
63
|
+
selected_parameter = self._clear_layout()
|
|
64
|
+
self._has_rendered_previews = False
|
|
65
|
+
self._previews = []
|
|
66
|
+
if generating is not None:
|
|
67
|
+
self._generating = bool(generating)
|
|
68
|
+
placeholder_count = self.placeholder_count if count is None else max(0, int(count))
|
|
69
|
+
for index in range(placeholder_count):
|
|
70
|
+
self.content_layout.addWidget(
|
|
71
|
+
self._placeholder_label(index, generating=self._generating)
|
|
72
|
+
)
|
|
73
|
+
self.content_layout.addStretch()
|
|
74
|
+
self._restore_selection(selected_parameter)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def show_previews(self, previews):
|
|
78
|
+
previews = list(previews or [])
|
|
79
|
+
if self._defer_display_during_drag(("previews", previews)):
|
|
80
|
+
return
|
|
81
|
+
self._generating = False
|
|
82
|
+
selected_parameter = self._clear_layout()
|
|
83
|
+
self._previews = list(previews or [])
|
|
84
|
+
|
|
85
|
+
preview_count = 0
|
|
86
|
+
parameters = self._placeholder_parameters
|
|
87
|
+
if parameters:
|
|
88
|
+
previews_by_parameter: dict[str, list[dict]] = {}
|
|
89
|
+
for preview in self._previews:
|
|
90
|
+
if not self._preview_is_displayable(preview):
|
|
91
|
+
continue
|
|
92
|
+
parameter = preview.get("parameter", "")
|
|
93
|
+
previews_by_parameter.setdefault(parameter, []).append(preview)
|
|
94
|
+
|
|
95
|
+
slot_count = max(self.placeholder_count, len(parameters))
|
|
96
|
+
for index in range(slot_count):
|
|
97
|
+
preview = None
|
|
98
|
+
if index < len(parameters):
|
|
99
|
+
matches = previews_by_parameter.get(parameters[index]) or []
|
|
100
|
+
if matches:
|
|
101
|
+
preview = matches.pop(0)
|
|
102
|
+
if preview is None:
|
|
103
|
+
self.content_layout.addWidget(self._placeholder_label(index))
|
|
104
|
+
else:
|
|
105
|
+
self._add_preview_label(preview)
|
|
106
|
+
preview_count += 1
|
|
107
|
+
else:
|
|
108
|
+
for preview in self._previews:
|
|
109
|
+
if not self._preview_is_displayable(preview):
|
|
110
|
+
continue
|
|
111
|
+
self._add_preview_label(preview)
|
|
112
|
+
preview_count += 1
|
|
113
|
+
for index in range(preview_count, self.placeholder_count):
|
|
114
|
+
self.content_layout.addWidget(self._placeholder_label(index))
|
|
115
|
+
self.content_layout.addStretch()
|
|
116
|
+
self._has_rendered_previews = preview_count > 0
|
|
117
|
+
self._restore_selection(selected_parameter)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@staticmethod
|
|
121
|
+
def _preview_is_displayable(preview):
|
|
122
|
+
return bool(preview.get("unsupported") or preview.get("image") is not None)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _add_preview_label(self, preview):
|
|
126
|
+
if preview.get("unsupported"):
|
|
127
|
+
label = unsupported_preview_label(
|
|
128
|
+
preview,
|
|
129
|
+
self.icon_size,
|
|
130
|
+
"measurementPreviewUnsupported",
|
|
131
|
+
)
|
|
132
|
+
self.content_layout.addWidget(label)
|
|
133
|
+
return
|
|
134
|
+
|
|
135
|
+
label = DraggablePreviewImageLabel(
|
|
136
|
+
self.guid,
|
|
137
|
+
preview.get("parameter", ""),
|
|
138
|
+
preview.get("axes") or [],
|
|
139
|
+
)
|
|
140
|
+
label.setObjectName("measurementPreviewImage")
|
|
141
|
+
label.setFixedSize(self.icon_size, self.icon_size)
|
|
142
|
+
label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
|
|
143
|
+
label.setToolTip(preview.get("title", ""))
|
|
144
|
+
label.set_preview_accessibility(preview.get("title"))
|
|
145
|
+
label.setPixmap(
|
|
146
|
+
QtGui.QPixmap.fromImage(preview["image"]).scaled(
|
|
147
|
+
self.icon_size,
|
|
148
|
+
self.icon_size,
|
|
149
|
+
QtCore.Qt.AspectRatioMode.KeepAspectRatio,
|
|
150
|
+
QtCore.Qt.TransformationMode.SmoothTransformation,
|
|
151
|
+
)
|
|
152
|
+
)
|
|
153
|
+
label.plotRequested.connect(self._emit_plot_requested)
|
|
154
|
+
label.exportRequested.connect(self._emit_export_requested)
|
|
155
|
+
label.dragFinished.connect(self._apply_pending_display)
|
|
156
|
+
self.content_layout.addWidget(label)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def set_generating(self, generating):
|
|
160
|
+
generating = bool(generating)
|
|
161
|
+
if generating:
|
|
162
|
+
if self._generating:
|
|
163
|
+
return
|
|
164
|
+
if self._has_rendered_previews:
|
|
165
|
+
self._generating = True
|
|
166
|
+
return
|
|
167
|
+
self.show_placeholders(generating=True)
|
|
168
|
+
return
|
|
169
|
+
|
|
170
|
+
self._generating = False
|
|
171
|
+
for label in self.findChildren(qtw.QLabel, "measurementPreviewPlaceholder"):
|
|
172
|
+
label.setStyleSheet("")
|
|
173
|
+
label.setToolTip("")
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def update_placeholder_metadata(self, metadata):
|
|
177
|
+
dimensions = preview_placeholder_dimensions(metadata)
|
|
178
|
+
parameters = list(metadata.get("measure_parameters") or [])
|
|
179
|
+
if (dimensions != self._placeholder_dimensions
|
|
180
|
+
or parameters != self._placeholder_parameters):
|
|
181
|
+
self._placeholder_dimensions = dimensions
|
|
182
|
+
self._placeholder_parameters = parameters
|
|
183
|
+
if self._has_rendered_previews:
|
|
184
|
+
self.show_previews(self._previews)
|
|
185
|
+
else:
|
|
186
|
+
self.show_placeholders()
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _placeholder_label(self, index=0, generating=False):
|
|
190
|
+
parameters = self._placeholder_parameters
|
|
191
|
+
parameter = parameters[index] if index < len(parameters) else ""
|
|
192
|
+
label = DraggablePreviewImageLabel(self.guid, parameter, axes_pending=True)
|
|
193
|
+
label.dragFinished.connect(self._apply_pending_display)
|
|
194
|
+
label.plotRequested.connect(self._emit_plot_requested)
|
|
195
|
+
label.exportRequested.connect(self._emit_export_requested)
|
|
196
|
+
dimensions = self._placeholder_dimensions
|
|
197
|
+
dimension = dimensions[index] if index < len(dimensions) else None
|
|
198
|
+
label.setText(f"{dimension}D" if dimension else "")
|
|
199
|
+
label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
|
|
200
|
+
font = label.font()
|
|
201
|
+
font.setPointSize(8)
|
|
202
|
+
label.setFont(font)
|
|
203
|
+
label.setObjectName("measurementPreviewPlaceholder")
|
|
204
|
+
label.setFixedSize(self.icon_size, self.icon_size)
|
|
205
|
+
label.setFrameShape(qtw.QFrame.Shape.Box)
|
|
206
|
+
label.setFrameShadow(qtw.QFrame.Shadow.Plain)
|
|
207
|
+
label.setLineWidth(1)
|
|
208
|
+
if generating:
|
|
209
|
+
color, border = self._generating_placeholder_colors(index)
|
|
210
|
+
label.setStyleSheet(
|
|
211
|
+
"QLabel#measurementPreviewPlaceholder {"
|
|
212
|
+
f"background-color: {color};"
|
|
213
|
+
f"border: 1px solid {border};"
|
|
214
|
+
"}"
|
|
215
|
+
)
|
|
216
|
+
label.setToolTip("Generating preview")
|
|
217
|
+
return label
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _defer_display_during_drag(self, display):
|
|
221
|
+
# QDrag runs a nested event loop. Preview publication in that loop
|
|
222
|
+
# must not replace/delete its source until the user has dropped it.
|
|
223
|
+
if any(label.drag_active for label in self.findChildren(DraggablePreviewImageLabel)):
|
|
224
|
+
self._pending_display = display
|
|
225
|
+
return True
|
|
226
|
+
return False
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _apply_pending_display(self):
|
|
230
|
+
display = self._pending_display
|
|
231
|
+
self._pending_display = None
|
|
232
|
+
if display is None:
|
|
233
|
+
return
|
|
234
|
+
if display[0] == "previews":
|
|
235
|
+
self.show_previews(display[1])
|
|
236
|
+
else:
|
|
237
|
+
self.show_placeholders(count=display[1], generating=display[2])
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _generating_placeholder_colors(self, index):
|
|
241
|
+
seed = sum(ord(char) for char in str(self.guid or ""))
|
|
242
|
+
palette_index = (seed + int(index or 0)) % len(MEASUREMENT_PREVIEW_GENERATING_COLORS)
|
|
243
|
+
return (
|
|
244
|
+
MEASUREMENT_PREVIEW_GENERATING_COLORS[palette_index],
|
|
245
|
+
MEASUREMENT_PREVIEW_GENERATING_BORDERS[palette_index],
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _emit_plot_requested(self, parameter):
|
|
250
|
+
self.plotRequested.emit(self.guid, parameter)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _emit_export_requested(self, parameter):
|
|
254
|
+
self.exportRequested.emit(self.guid, parameter)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _clear_layout(self):
|
|
258
|
+
selected_parameter = next(
|
|
259
|
+
(label.parameter for label in self.findChildren(PreviewImageLabel)
|
|
260
|
+
if label._selected),
|
|
261
|
+
None,
|
|
262
|
+
)
|
|
263
|
+
while self.content_layout.count():
|
|
264
|
+
item = self.content_layout.takeAt(0)
|
|
265
|
+
if item is None:
|
|
266
|
+
continue
|
|
267
|
+
widget = item.widget()
|
|
268
|
+
if widget is not None:
|
|
269
|
+
widget.setParent(None)
|
|
270
|
+
widget.deleteLater()
|
|
271
|
+
return selected_parameter
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _restore_selection(self, parameter):
|
|
275
|
+
if parameter:
|
|
276
|
+
for label in self.findChildren(PreviewImageLabel):
|
|
277
|
+
if label.parameter == parameter:
|
|
278
|
+
label.set_selected(True)
|
|
279
|
+
break
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
class EqualsAlignedDelegate(qtw.QStyledItemDelegate):
|
|
283
|
+
"""
|
|
284
|
+
Paints setpoint counts with equals signs aligned.
|
|
285
|
+
"""
|
|
286
|
+
|
|
287
|
+
right_text_alignment = (
|
|
288
|
+
QtCore.Qt.AlignmentFlag.AlignLeft
|
|
289
|
+
| QtCore.Qt.AlignmentFlag.AlignVCenter
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
def __init__(self, *args, **kwargs):
|
|
293
|
+
super().__init__(*args, **kwargs)
|
|
294
|
+
self._right_width_cache = {}
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def invalidate_width_cache(self):
|
|
298
|
+
self._right_width_cache.clear()
|
|
299
|
+
|
|
300
|
+
def paint(self, painter, option, index):
|
|
301
|
+
text = index.data(QtCore.Qt.ItemDataRole.DisplayRole)
|
|
302
|
+
if text is None or text == "":
|
|
303
|
+
super().paint(painter, option, index)
|
|
304
|
+
return
|
|
305
|
+
|
|
306
|
+
opt = qtw.QStyleOptionViewItem(option)
|
|
307
|
+
self.initStyleOption(opt, index)
|
|
308
|
+
text = str(text)
|
|
309
|
+
sections = self._display_sections(text)
|
|
310
|
+
if sections is None:
|
|
311
|
+
super().paint(painter, option, index)
|
|
312
|
+
return
|
|
313
|
+
left, right = sections
|
|
314
|
+
|
|
315
|
+
opt.text = ""
|
|
316
|
+
|
|
317
|
+
widget = opt.widget
|
|
318
|
+
style = widget.style() if widget else qtw.QApplication.style()
|
|
319
|
+
if style is None:
|
|
320
|
+
super().paint(painter, option, index)
|
|
321
|
+
return
|
|
322
|
+
style.drawControl(qtw.QStyle.ControlElement.CE_ItemViewItem, opt, painter, widget)
|
|
323
|
+
|
|
324
|
+
text_rect = style.subElementRect(
|
|
325
|
+
qtw.QStyle.SubElement.SE_ItemViewItemText,
|
|
326
|
+
opt,
|
|
327
|
+
widget
|
|
328
|
+
).adjusted(2, 0, -2, 0)
|
|
329
|
+
metrics = opt.fontMetrics
|
|
330
|
+
equals_text = " = "
|
|
331
|
+
equals_width = metrics.horizontalAdvance(equals_text)
|
|
332
|
+
max_right_width = self._max_right_width(index, metrics)
|
|
333
|
+
if right is None and max_right_width <= 0:
|
|
334
|
+
painter.save()
|
|
335
|
+
painter.setFont(opt.font)
|
|
336
|
+
painter.setPen(self._text_color(opt))
|
|
337
|
+
painter.drawText(
|
|
338
|
+
text_rect,
|
|
339
|
+
QtCore.Qt.AlignmentFlag.AlignRight
|
|
340
|
+
| QtCore.Qt.AlignmentFlag.AlignVCenter,
|
|
341
|
+
metrics.elidedText(left, QtCore.Qt.TextElideMode.ElideLeft, text_rect.width())
|
|
342
|
+
)
|
|
343
|
+
painter.restore()
|
|
344
|
+
return
|
|
345
|
+
|
|
346
|
+
equals_left = max(
|
|
347
|
+
text_rect.left(),
|
|
348
|
+
text_rect.right() - max_right_width - equals_width
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
left_rect = QtCore.QRect(
|
|
352
|
+
text_rect.left(),
|
|
353
|
+
text_rect.top(),
|
|
354
|
+
max(0, equals_left - text_rect.left()),
|
|
355
|
+
text_rect.height()
|
|
356
|
+
)
|
|
357
|
+
equals_rect = QtCore.QRect(
|
|
358
|
+
equals_left,
|
|
359
|
+
text_rect.top(),
|
|
360
|
+
equals_width,
|
|
361
|
+
text_rect.height()
|
|
362
|
+
)
|
|
363
|
+
right_rect = QtCore.QRect(
|
|
364
|
+
equals_left + equals_width,
|
|
365
|
+
text_rect.top(),
|
|
366
|
+
max(0, text_rect.right() - equals_left - equals_width + 1),
|
|
367
|
+
text_rect.height()
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
painter.save()
|
|
371
|
+
painter.setFont(opt.font)
|
|
372
|
+
painter.setPen(self._text_color(opt))
|
|
373
|
+
painter.drawText(
|
|
374
|
+
left_rect,
|
|
375
|
+
QtCore.Qt.AlignmentFlag.AlignRight | QtCore.Qt.AlignmentFlag.AlignVCenter,
|
|
376
|
+
metrics.elidedText(left, QtCore.Qt.TextElideMode.ElideLeft, left_rect.width())
|
|
377
|
+
)
|
|
378
|
+
if right is not None:
|
|
379
|
+
painter.drawText(equals_rect, QtCore.Qt.AlignmentFlag.AlignCenter, equals_text)
|
|
380
|
+
self._draw_right_text(painter, right_rect, right, metrics)
|
|
381
|
+
painter.restore()
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def _display_sections(self, text):
|
|
385
|
+
if " = " in text:
|
|
386
|
+
return str(text).split(" = ", 1)
|
|
387
|
+
if self._is_count_text(text):
|
|
388
|
+
return str(text), None
|
|
389
|
+
return None
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def _is_count_text(self, text):
|
|
393
|
+
try:
|
|
394
|
+
int(str(text).replace(",", ""))
|
|
395
|
+
except (TypeError, ValueError):
|
|
396
|
+
return False
|
|
397
|
+
return True
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def _draw_right_text(self, painter, right_rect, right, metrics):
|
|
401
|
+
painter.drawText(
|
|
402
|
+
right_rect,
|
|
403
|
+
self.right_text_alignment,
|
|
404
|
+
metrics.elidedText(right, QtCore.Qt.TextElideMode.ElideRight, right_rect.width())
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def _max_right_width(self, index, metrics):
|
|
409
|
+
view = self.parent()
|
|
410
|
+
if not isinstance(view, qtw.QTreeWidget):
|
|
411
|
+
return 0
|
|
412
|
+
|
|
413
|
+
column = index.column()
|
|
414
|
+
cached = self._right_width_cache.get(column)
|
|
415
|
+
if cached is not None:
|
|
416
|
+
return cached
|
|
417
|
+
|
|
418
|
+
max_width = 0
|
|
419
|
+
for row in range(view.topLevelItemCount()):
|
|
420
|
+
item = view.topLevelItem(row)
|
|
421
|
+
if item is None:
|
|
422
|
+
continue
|
|
423
|
+
|
|
424
|
+
sections = self._display_sections(item.text(column))
|
|
425
|
+
if sections is None:
|
|
426
|
+
continue
|
|
427
|
+
|
|
428
|
+
_, right = sections
|
|
429
|
+
if right is None:
|
|
430
|
+
right = one_dimensional_duplicate_point_count(
|
|
431
|
+
getattr(item, "run_metadata", {})
|
|
432
|
+
)
|
|
433
|
+
if right is None:
|
|
434
|
+
continue
|
|
435
|
+
|
|
436
|
+
max_width = max(max_width, metrics.horizontalAdvance(right))
|
|
437
|
+
self._right_width_cache[column] = max_width
|
|
438
|
+
return max_width
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def _text_color(self, option):
|
|
442
|
+
return option.palette.color(QtGui.QPalette.ColorRole.Text)
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
class SortableTreeWidgetItem(qtw.QTreeWidgetItem):
|
|
446
|
+
"""
|
|
447
|
+
QTreeWidgetItem with an overridden comparator that sorts numerical values
|
|
448
|
+
as numbers instead of sorting them alphabetically.
|
|
449
|
+
"""
|
|
450
|
+
def __init__(self, strings):
|
|
451
|
+
super().__init__(strings)
|
|
452
|
+
self.run_metadata = {}
|
|
453
|
+
self._guid = ""
|
|
454
|
+
|
|
455
|
+
def __lt__(self, other: qtw.QTreeWidgetItem) -> bool:
|
|
456
|
+
tree = self.treeWidget()
|
|
457
|
+
if tree is None:
|
|
458
|
+
return super().__lt__(other)
|
|
459
|
+
|
|
460
|
+
col = tree.sortColumn()
|
|
461
|
+
value1 = self.data(col, QtCore.Qt.ItemDataRole.UserRole)
|
|
462
|
+
value2 = other.data(col, QtCore.Qt.ItemDataRole.UserRole)
|
|
463
|
+
if value1 is not None and value2 is not None:
|
|
464
|
+
try:
|
|
465
|
+
return float(value1) < float(value2)
|
|
466
|
+
except (TypeError, ValueError):
|
|
467
|
+
pass
|
|
468
|
+
|
|
469
|
+
text1 = self.text(col)
|
|
470
|
+
text2 = other.text(col)
|
|
471
|
+
try:
|
|
472
|
+
return float(text1) < float(text2)
|
|
473
|
+
except ValueError:
|
|
474
|
+
return text1 < text2
|
|
475
|
+
|
|
476
|
+
@property
|
|
477
|
+
def guid(self):
|
|
478
|
+
return self._guid
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def set_guid(self, guid):
|
|
482
|
+
self._guid = guid
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def update_tooltip(self):
|
|
486
|
+
tooltip = run_tooltip_text(self.run_metadata)
|
|
487
|
+
for col in range(self.columnCount()):
|
|
488
|
+
self.setToolTip(col, tooltip)
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from PyQt6 import QtCore
|
|
4
|
+
from PyQt6 import QtWidgets as qtw
|
|
5
|
+
|
|
6
|
+
_NO_ALIGNMENT = QtCore.Qt.AlignmentFlag(0)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class QDock_context(qtw.QDockWidget):
|
|
10
|
+
"""
|
|
11
|
+
A custom QDockWidget designed to emulate a side QToolbar.
|
|
12
|
+
|
|
13
|
+
"""
|
|
14
|
+
def __init__(self, *args, **kargs):
|
|
15
|
+
super().__init__(*args, **kargs)
|
|
16
|
+
|
|
17
|
+
self.setTitleBarWidget(qtw.QWidget(self))
|
|
18
|
+
|
|
19
|
+
self.event_filter = contextMenuFilter(self, self.parent())
|
|
20
|
+
self.installEventFilter(self.event_filter)
|
|
21
|
+
|
|
22
|
+
core_widget = qtw.QFrame()
|
|
23
|
+
self.setWidget(core_widget)
|
|
24
|
+
|
|
25
|
+
self.content_layout = self.VBox_context(self.event_filter, core_widget)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
### Overwrite add functions to add to main layout
|
|
29
|
+
def addLayout(self, *args: Any, **kargs: Any) -> "QDock_context.HBox_context":
|
|
30
|
+
layout = self.HBox_context(self.event_filter, *args, **kargs)
|
|
31
|
+
self.content_layout.addLayout(layout)
|
|
32
|
+
return layout
|
|
33
|
+
|
|
34
|
+
def addWidget(self, *args: Any, **kargs: Any) -> None:
|
|
35
|
+
self.content_layout.addWidget(*args, **kargs)
|
|
36
|
+
|
|
37
|
+
### SUB CLASS LAYOUT TO CARRY CONTEXT MENU THROUGH ###
|
|
38
|
+
class VBox_context(qtw.QVBoxLayout):
|
|
39
|
+
def __init__(self, event_filter, *args, **kargs):
|
|
40
|
+
super().__init__(*args, **kargs)
|
|
41
|
+
self.event_filter = event_filter
|
|
42
|
+
|
|
43
|
+
def addWidget(
|
|
44
|
+
self,
|
|
45
|
+
widget: qtw.QWidget | None,
|
|
46
|
+
stretch: int = 0,
|
|
47
|
+
alignment: QtCore.Qt.AlignmentFlag = _NO_ALIGNMENT,
|
|
48
|
+
) -> None:
|
|
49
|
+
if widget is not None:
|
|
50
|
+
widget.installEventFilter(self.event_filter)
|
|
51
|
+
for child in self.findChildren(qtw.QWidget):
|
|
52
|
+
child.installEventFilter(self.event_filter)
|
|
53
|
+
|
|
54
|
+
super().addWidget(widget, stretch, alignment)
|
|
55
|
+
|
|
56
|
+
class HBox_context(qtw.QHBoxLayout):
|
|
57
|
+
def __init__(self, event_filter, *args, **kargs):
|
|
58
|
+
super().__init__(*args, **kargs)
|
|
59
|
+
self.event_filter = event_filter
|
|
60
|
+
|
|
61
|
+
def addWidget(
|
|
62
|
+
self,
|
|
63
|
+
widget: qtw.QWidget | None,
|
|
64
|
+
stretch: int = 0,
|
|
65
|
+
alignment: QtCore.Qt.AlignmentFlag = _NO_ALIGNMENT,
|
|
66
|
+
) -> None:
|
|
67
|
+
if widget is not None:
|
|
68
|
+
widget.installEventFilter(self.event_filter)
|
|
69
|
+
for child in self.findChildren(qtw.QWidget):
|
|
70
|
+
child.installEventFilter(self.event_filter)
|
|
71
|
+
|
|
72
|
+
super().addWidget(widget, stretch, alignment)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class contextMenuFilter(QtCore.QObject):
|
|
76
|
+
"""Filter to show toggle menu from any widget inside the dock."""
|
|
77
|
+
def __init__(self, parent, main_window):
|
|
78
|
+
super().__init__(parent)
|
|
79
|
+
self.main_window = main_window
|
|
80
|
+
|
|
81
|
+
def eventFilter(self, obj, event):
|
|
82
|
+
"""
|
|
83
|
+
Event handler for right-clicking on a QDock_context
|
|
84
|
+
Produces a context menu for toggling display of toolbars and dockwidgets
|
|
85
|
+
|
|
86
|
+
Parameters
|
|
87
|
+
----------
|
|
88
|
+
obj :
|
|
89
|
+
Unused by required slot.
|
|
90
|
+
event : PyQt6.QtCore.QEvent.Type.ContextMenu
|
|
91
|
+
|
|
92
|
+
Returns
|
|
93
|
+
-------
|
|
94
|
+
bool
|
|
95
|
+
Whether to show context menu.
|
|
96
|
+
|
|
97
|
+
"""
|
|
98
|
+
if event.type() == QtCore.QEvent.Type.ContextMenu:
|
|
99
|
+
menu = qtw.QMenu(self.main_window)
|
|
100
|
+
|
|
101
|
+
# Find toolbar and dock widgets to add to context menu
|
|
102
|
+
for toolbar in self.main_window.findChildren(qtw.QToolBar):
|
|
103
|
+
menu.addAction(toolbar.toggleViewAction())
|
|
104
|
+
|
|
105
|
+
for dock in self.main_window.findChildren(qtw.QDockWidget):
|
|
106
|
+
menu.addAction(dock.toggleViewAction())
|
|
107
|
+
|
|
108
|
+
# Display context menu
|
|
109
|
+
menu.exec(event.globalPos())
|
|
110
|
+
return True
|
|
111
|
+
return False
|