qplotter 1.6.0b3__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 +63 -0
- qplot/_metadata.py +12 -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 +573 -0
- qplot/configuration/config_schema.json +319 -0
- qplot/configuration/scripts.py +255 -0
- qplot/configuration/themes/__init__.py +13 -0
- qplot/configuration/themes/_base.py +579 -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 +2330 -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 +2561 -0
- qplot/datahandling/source_activity.py +21 -0
- qplot/datahandling/trusted_array_preview.py +127 -0
- qplot/datahandling/trusted_derived_cache.py +1605 -0
- qplot/datahandling/trusted_derived_rendering.py +868 -0
- qplot/datahandling/trusted_heatmap.py +241 -0
- qplot/datahandling/trusted_live.py +2808 -0
- qplot/datahandling/trusted_live_queries.py +5907 -0
- qplot/datahandling/trusted_live_service.py +1894 -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 +1733 -0
- qplot/tools/__init__.py +13 -0
- qplot/tools/finite_means.py +114 -0
- qplot/tools/general.py +134 -0
- qplot/tools/heatmap_geometry.py +381 -0
- qplot/tools/operation_registry.py +179 -0
- qplot/tools/plot_tools.py +625 -0
- qplot/tools/sample_statistics.py +56 -0
- qplot/tools/worker.py +2787 -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 +4904 -0
- qplot/windows/_dataset_handle.py +221 -0
- qplot/windows/_dragdrop.py +71 -0
- qplot/windows/_export_paths.py +1167 -0
- qplot/windows/_help.py +169 -0
- qplot/windows/_native_averaging.py +173 -0
- qplot/windows/_native_transforms.py +389 -0
- qplot/windows/_plot1d_snap.py +608 -0
- qplot/windows/_plot1d_traces.py +2183 -0
- qplot/windows/_plot2d_colorbar.py +1126 -0
- qplot/windows/_plot2d_colorbar_dialog.py +786 -0
- qplot/windows/_plot2d_layers.py +2197 -0
- qplot/windows/_plot2d_sweeps.py +712 -0
- qplot/windows/_plotWin.py +1993 -0
- qplot/windows/_plot_actions.py +2695 -0
- qplot/windows/_plot_appearance.py +276 -0
- qplot/windows/_plot_axis_scaling.py +1485 -0
- qplot/windows/_plot_export.py +1445 -0
- qplot/windows/_plot_feedback.py +110 -0
- qplot/windows/_plot_marquee.py +656 -0
- qplot/windows/_plot_refresh.py +942 -0
- qplot/windows/_plot_state.py +239 -0
- qplot/windows/_plot_transform_labels.py +60 -0
- qplot/windows/_preferences.py +635 -0
- qplot/windows/_refresh_interval.py +70 -0
- qplot/windows/_run_controls.py +828 -0
- qplot/windows/_shortcuts.py +47 -0
- qplot/windows/_subplots/__init__.py +8 -0
- qplot/windows/_subplots/subplot1d.py +531 -0
- qplot/windows/_subplots/subplot2d.py +918 -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 +413 -0
- qplot/windows/main.py +1635 -0
- qplot/windows/plot1d.py +311 -0
- qplot/windows/plot2d.py +1901 -0
- qplotter-1.6.0b3.dist-info/METADATA +318 -0
- qplotter-1.6.0b3.dist-info/RECORD +119 -0
- qplotter-1.6.0b3.dist-info/WHEEL +5 -0
- qplotter-1.6.0b3.dist-info/entry_points.txt +4 -0
- qplotter-1.6.0b3.dist-info/licenses/LICENSE +21 -0
- qplotter-1.6.0b3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1119 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any, cast
|
|
5
|
+
|
|
6
|
+
from PyQt6 import QtCore, QtGui
|
|
7
|
+
from PyQt6 import QtWidgets as qtw
|
|
8
|
+
|
|
9
|
+
from qplot.datahandling.trusted_presentation import (
|
|
10
|
+
TRUSTED_PRESENTATION_MAX_RENDERED_NODES,
|
|
11
|
+
TRUSTED_PRESENTATION_MAX_RENDERED_TEXT_BYTES,
|
|
12
|
+
TrustedPresentationView,
|
|
13
|
+
)
|
|
14
|
+
from qplot.datahandling.trusted_snapshot import (
|
|
15
|
+
TRUSTED_SNAPSHOT_LOAD_MORE_TEXT,
|
|
16
|
+
TRUSTED_SNAPSHOT_PAGE_MAX_CHILDREN,
|
|
17
|
+
TRUSTED_SNAPSHOT_PAGE_MAX_DATA_NODES_WITH_CONTINUATION,
|
|
18
|
+
TRUSTED_SNAPSHOT_PAGE_MAX_TEXT_BYTES,
|
|
19
|
+
TrustedSnapshotContainerHandle,
|
|
20
|
+
TrustedSnapshotContinuation,
|
|
21
|
+
TrustedSnapshotFullValueHandle,
|
|
22
|
+
TrustedSnapshotPage,
|
|
23
|
+
TrustedSnapshotView,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
from .._commands import command_spec
|
|
27
|
+
|
|
28
|
+
COPY_SELECTION_SHORTCUTS = command_spec("copy.selection").resolved_shortcuts()
|
|
29
|
+
COPY_CELL_SHORTCUTS = command_spec("copy.cell").resolved_shortcuts()
|
|
30
|
+
FULL_VALUE_ID_ROLE = int(QtCore.Qt.ItemDataRole.UserRole) + 40
|
|
31
|
+
FULL_VALUE_PATH_ROLE = FULL_VALUE_ID_ROLE + 1
|
|
32
|
+
SNAPSHOT_SESSION_ROLE = FULL_VALUE_ID_ROLE + 2
|
|
33
|
+
SNAPSHOT_CONTAINER_ROLE = FULL_VALUE_ID_ROLE + 3
|
|
34
|
+
SNAPSHOT_PARENT_ROLE = FULL_VALUE_ID_ROLE + 4
|
|
35
|
+
SNAPSHOT_REQUEST_CONTINUATION_ROLE = FULL_VALUE_ID_ROLE + 5
|
|
36
|
+
SNAPSHOT_CONTINUATION_ROLE = FULL_VALUE_ID_ROLE + 6
|
|
37
|
+
SNAPSHOT_FULL_VALUE_ROLE = FULL_VALUE_ID_ROLE + 7
|
|
38
|
+
SNAPSHOT_PAGE_PENDING_ROLE = FULL_VALUE_ID_ROLE + 8
|
|
39
|
+
SNAPSHOT_CONTAINER_LOADED_ROLE = FULL_VALUE_ID_ROLE + 9
|
|
40
|
+
SNAPSHOT_LOAD_MORE_ROLE = FULL_VALUE_ID_ROLE + 10
|
|
41
|
+
DEFAULT_EXPANDED_SNAPSHOT_PATH = "/Snapshot/station"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _snapshot_page_display_text_bytes(
|
|
45
|
+
rendered_text_bytes: int,
|
|
46
|
+
continuation: object | None,
|
|
47
|
+
) -> int:
|
|
48
|
+
return rendered_text_bytes + (
|
|
49
|
+
len(TRUSTED_SNAPSHOT_LOAD_MORE_TEXT.encode("utf-8"))
|
|
50
|
+
if continuation is not None
|
|
51
|
+
else 0
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass(frozen=True, slots=True)
|
|
56
|
+
class SnapshotTreePageRequest:
|
|
57
|
+
"""One bounded request emitted by the Snapshot tree.
|
|
58
|
+
|
|
59
|
+
Handles and continuations are opaque, immutable core values. In
|
|
60
|
+
particular, this envelope never owns the retained Snapshot source.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
session_token: str
|
|
64
|
+
parent_handle: TrustedSnapshotContainerHandle
|
|
65
|
+
continuation: TrustedSnapshotContinuation | None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass(frozen=True, slots=True)
|
|
69
|
+
class SnapshotTreeValueRequest:
|
|
70
|
+
"""One exact Snapshot scalar request tied to the page that displayed it."""
|
|
71
|
+
|
|
72
|
+
session_token: str
|
|
73
|
+
handle: TrustedSnapshotFullValueHandle
|
|
74
|
+
path: str
|
|
75
|
+
parent_handle: TrustedSnapshotContainerHandle | None
|
|
76
|
+
continuation: TrustedSnapshotContinuation | None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def copy_action(label, shortcuts, slot, parent):
|
|
80
|
+
action = QtGui.QAction(label, parent)
|
|
81
|
+
action.setShortcuts(shortcuts)
|
|
82
|
+
action.setShortcutContext(QtCore.Qt.ShortcutContext.WidgetWithChildrenShortcut)
|
|
83
|
+
if hasattr(action, "setShortcutVisibleInContextMenu"):
|
|
84
|
+
action.setShortcutVisibleInContextMenu(True)
|
|
85
|
+
action.triggered.connect(slot)
|
|
86
|
+
return action
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class WrappedValueDelegate(qtw.QStyledItemDelegate):
|
|
90
|
+
WRAP_FLAGS = (
|
|
91
|
+
QtCore.Qt.AlignmentFlag.AlignLeft
|
|
92
|
+
| QtCore.Qt.AlignmentFlag.AlignTop
|
|
93
|
+
| QtCore.Qt.TextFlag.TextWrapAnywhere
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
def paint(self, painter, option, index):
|
|
97
|
+
opt = qtw.QStyleOptionViewItem(option)
|
|
98
|
+
self.initStyleOption(opt, index)
|
|
99
|
+
|
|
100
|
+
widget = opt.widget
|
|
101
|
+
style = widget.style() if widget is not None else qtw.QApplication.style()
|
|
102
|
+
if style is None:
|
|
103
|
+
super().paint(painter, option, index)
|
|
104
|
+
return
|
|
105
|
+
text = opt.text
|
|
106
|
+
opt.text = ""
|
|
107
|
+
|
|
108
|
+
style.drawControl(
|
|
109
|
+
qtw.QStyle.ControlElement.CE_ItemViewItem, opt, painter, widget
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
text_rect = style.subElementRect(
|
|
113
|
+
qtw.QStyle.SubElement.SE_ItemViewItemText, opt, widget
|
|
114
|
+
)
|
|
115
|
+
text_rect.adjust(0, 2, 0, -2)
|
|
116
|
+
painter.save()
|
|
117
|
+
painter.setFont(opt.font)
|
|
118
|
+
role = (
|
|
119
|
+
QtGui.QPalette.ColorRole.HighlightedText
|
|
120
|
+
if opt.state & qtw.QStyle.StateFlag.State_Selected
|
|
121
|
+
else QtGui.QPalette.ColorRole.Text
|
|
122
|
+
)
|
|
123
|
+
painter.setPen(opt.palette.color(role))
|
|
124
|
+
painter.drawText(text_rect, self.WRAP_FLAGS, text)
|
|
125
|
+
painter.restore()
|
|
126
|
+
|
|
127
|
+
def sizeHint(self, option, index):
|
|
128
|
+
opt = qtw.QStyleOptionViewItem(option)
|
|
129
|
+
self.initStyleOption(opt, index)
|
|
130
|
+
|
|
131
|
+
width = opt.rect.width()
|
|
132
|
+
if width <= 0 and isinstance(opt.widget, qtw.QTreeView):
|
|
133
|
+
width = opt.widget.columnWidth(index.column())
|
|
134
|
+
width = max(24, width - 6)
|
|
135
|
+
|
|
136
|
+
metrics = QtGui.QFontMetrics(opt.font)
|
|
137
|
+
text_rect = metrics.boundingRect(
|
|
138
|
+
QtCore.QRect(0, 0, width, 100_000), self.WRAP_FLAGS, opt.text
|
|
139
|
+
)
|
|
140
|
+
base = super().sizeHint(option, index)
|
|
141
|
+
return QtCore.QSize(base.width(), max(base.height(), text_rect.height() + 6))
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class TrustedFullValueDialog(qtw.QDialog):
|
|
145
|
+
"""One reusable, read-only viewer for the current selected-run scalar."""
|
|
146
|
+
|
|
147
|
+
def __init__(self, parent=None):
|
|
148
|
+
super().__init__(parent)
|
|
149
|
+
self._exact_text: str | None = None
|
|
150
|
+
self.setObjectName("trustedFullValueDialog")
|
|
151
|
+
self.setWindowTitle("Complete metadata value")
|
|
152
|
+
self.resize(760, 520)
|
|
153
|
+
|
|
154
|
+
self.path_label = qtw.QLabel(self)
|
|
155
|
+
self.path_label.setTextInteractionFlags(
|
|
156
|
+
QtCore.Qt.TextInteractionFlag.TextSelectableByMouse
|
|
157
|
+
)
|
|
158
|
+
self.text_edit = qtw.QPlainTextEdit(self)
|
|
159
|
+
self.text_edit.setObjectName("trustedFullValueText")
|
|
160
|
+
self.text_edit.setReadOnly(True)
|
|
161
|
+
self.text_edit.setLineWrapMode(qtw.QPlainTextEdit.LineWrapMode.NoWrap)
|
|
162
|
+
|
|
163
|
+
self.select_all_button = qtw.QPushButton("Select All", self)
|
|
164
|
+
self.copy_button = qtw.QPushButton("Copy", self)
|
|
165
|
+
self.close_button = qtw.QPushButton("Close", self)
|
|
166
|
+
self.select_all_button.clicked.connect(self.text_edit.selectAll)
|
|
167
|
+
self.copy_button.clicked.connect(self.copy_value)
|
|
168
|
+
self.close_button.clicked.connect(self.close)
|
|
169
|
+
|
|
170
|
+
buttons = qtw.QHBoxLayout()
|
|
171
|
+
buttons.addStretch(1)
|
|
172
|
+
buttons.addWidget(self.select_all_button)
|
|
173
|
+
buttons.addWidget(self.copy_button)
|
|
174
|
+
buttons.addWidget(self.close_button)
|
|
175
|
+
layout = qtw.QVBoxLayout(self)
|
|
176
|
+
layout.addWidget(self.path_label)
|
|
177
|
+
layout.addWidget(self.text_edit, 1)
|
|
178
|
+
layout.addLayout(buttons)
|
|
179
|
+
|
|
180
|
+
def show_value(self, *, path: str, text: str, utf8_bytes: int) -> None:
|
|
181
|
+
self._exact_text = text
|
|
182
|
+
self.path_label.setText(f"{path} — {utf8_bytes} UTF-8 bytes")
|
|
183
|
+
self.text_edit.setPlainText(text)
|
|
184
|
+
cursor = self.text_edit.textCursor()
|
|
185
|
+
cursor.movePosition(QtGui.QTextCursor.MoveOperation.Start)
|
|
186
|
+
self.text_edit.setTextCursor(cursor)
|
|
187
|
+
self.show()
|
|
188
|
+
self.raise_()
|
|
189
|
+
self.activateWindow()
|
|
190
|
+
|
|
191
|
+
@QtCore.pyqtSlot()
|
|
192
|
+
def copy_value(self) -> None:
|
|
193
|
+
if self._exact_text is not None:
|
|
194
|
+
copy_to_clipboard(self._exact_text)
|
|
195
|
+
|
|
196
|
+
def discard_value(self) -> None:
|
|
197
|
+
self._exact_text = None
|
|
198
|
+
self.text_edit.clear()
|
|
199
|
+
self.path_label.clear()
|
|
200
|
+
self.close()
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class infoTree(qtw.QTreeWidget):
|
|
204
|
+
fullValueRequested = QtCore.pyqtSignal(str, str)
|
|
205
|
+
snapshotPageRequested = QtCore.pyqtSignal(object)
|
|
206
|
+
snapshotFullValueRequested = QtCore.pyqtSignal(object)
|
|
207
|
+
|
|
208
|
+
def __init__(
|
|
209
|
+
self,
|
|
210
|
+
expand_all=True,
|
|
211
|
+
truncate_values=False,
|
|
212
|
+
*,
|
|
213
|
+
expand_top_level=False,
|
|
214
|
+
):
|
|
215
|
+
super().__init__()
|
|
216
|
+
self.expand_all = expand_all
|
|
217
|
+
self.expand_top_level = expand_top_level
|
|
218
|
+
self.truncate_values = truncate_values
|
|
219
|
+
self._snapshot_session_token = ""
|
|
220
|
+
self._snapshot_root_handle: TrustedSnapshotContainerHandle | None = None
|
|
221
|
+
self._snapshot_pending_requests: list[SnapshotTreePageRequest] = []
|
|
222
|
+
self.setHeaderLabels(["Key", "Value"])
|
|
223
|
+
self.setColumnCount(2)
|
|
224
|
+
self.setWordWrap(True)
|
|
225
|
+
self.setTextElideMode(QtCore.Qt.TextElideMode.ElideNone)
|
|
226
|
+
self.setUniformRowHeights(False)
|
|
227
|
+
self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
|
228
|
+
self.setItemDelegateForColumn(1, WrappedValueDelegate(self))
|
|
229
|
+
header = self.header()
|
|
230
|
+
if header is not None:
|
|
231
|
+
header.setSectionResizeMode(0, qtw.QHeaderView.ResizeMode.ResizeToContents)
|
|
232
|
+
header.setSectionResizeMode(1, qtw.QHeaderView.ResizeMode.Stretch)
|
|
233
|
+
header.setStretchLastSection(True)
|
|
234
|
+
self.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.CustomContextMenu)
|
|
235
|
+
self.customContextMenuRequested.connect(self.openCopyMenu)
|
|
236
|
+
self.itemActivated.connect(self._activate_full_value)
|
|
237
|
+
self.itemDoubleClicked.connect(self._activate_full_value)
|
|
238
|
+
self.itemExpanded.connect(self._request_expanded_snapshot_container)
|
|
239
|
+
|
|
240
|
+
self.copy_value_action = copy_action(
|
|
241
|
+
"Copy Value", COPY_CELL_SHORTCUTS, self.copyValue, self
|
|
242
|
+
)
|
|
243
|
+
self.copy_selection_action = copy_action(
|
|
244
|
+
"Copy Selection", COPY_SELECTION_SHORTCUTS, self.copySelection, self
|
|
245
|
+
)
|
|
246
|
+
self.view_full_value_action = QtGui.QAction("View Complete Value…", self)
|
|
247
|
+
self.view_full_value_action.triggered.connect(self._request_current_full_value)
|
|
248
|
+
self.addActions(
|
|
249
|
+
[
|
|
250
|
+
self.view_full_value_action,
|
|
251
|
+
self.copy_value_action,
|
|
252
|
+
self.copy_selection_action,
|
|
253
|
+
]
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
@property
|
|
257
|
+
def snapshotSessionToken(self) -> str:
|
|
258
|
+
return self._snapshot_session_token
|
|
259
|
+
|
|
260
|
+
def expandDefaultSnapshotContainer(self) -> bool:
|
|
261
|
+
"""Open the QCoDeS station node and request only its first lazy page."""
|
|
262
|
+
|
|
263
|
+
self._require_owner_thread()
|
|
264
|
+
if not self._snapshot_session_token:
|
|
265
|
+
return False
|
|
266
|
+
for index in range(self.topLevelItemCount()):
|
|
267
|
+
item = self.topLevelItem(index)
|
|
268
|
+
if item is None:
|
|
269
|
+
continue
|
|
270
|
+
if (
|
|
271
|
+
item.data(0, FULL_VALUE_PATH_ROLE) != DEFAULT_EXPANDED_SNAPSHOT_PATH
|
|
272
|
+
or item.data(0, SNAPSHOT_CONTAINER_ROLE) is None
|
|
273
|
+
):
|
|
274
|
+
continue
|
|
275
|
+
if item.isExpanded():
|
|
276
|
+
self._request_expanded_snapshot_container(item)
|
|
277
|
+
else:
|
|
278
|
+
item.setExpanded(True)
|
|
279
|
+
return True
|
|
280
|
+
return False
|
|
281
|
+
|
|
282
|
+
def clear(self) -> None:
|
|
283
|
+
self._snapshot_session_token = ""
|
|
284
|
+
self._snapshot_root_handle = None
|
|
285
|
+
self._snapshot_pending_requests = []
|
|
286
|
+
super().clear()
|
|
287
|
+
|
|
288
|
+
def setInfo(self, info):
|
|
289
|
+
self.clear()
|
|
290
|
+
|
|
291
|
+
if not info:
|
|
292
|
+
self.addTopLevelItem(qtw.QTreeWidgetItem(["No data", ""]))
|
|
293
|
+
return
|
|
294
|
+
if not isinstance(info, dict):
|
|
295
|
+
item = qtw.QTreeWidgetItem(
|
|
296
|
+
["Value", format_value(info, 180 if self.truncate_values else None)]
|
|
297
|
+
)
|
|
298
|
+
item.setToolTip(1, format_value(info))
|
|
299
|
+
self.addTopLevelItem(item)
|
|
300
|
+
return
|
|
301
|
+
|
|
302
|
+
items = dictToTree(info, truncate_values=self.truncate_values)
|
|
303
|
+
for item in items:
|
|
304
|
+
self.addTopLevelItem(item)
|
|
305
|
+
item.setExpanded(True)
|
|
306
|
+
|
|
307
|
+
if self.expand_all:
|
|
308
|
+
self.expandAll()
|
|
309
|
+
header = self.header()
|
|
310
|
+
if header is not None:
|
|
311
|
+
header.setSectionResizeMode(0, qtw.QHeaderView.ResizeMode.ResizeToContents)
|
|
312
|
+
header.setSectionResizeMode(1, qtw.QHeaderView.ResizeMode.Stretch)
|
|
313
|
+
cast(Any, self).doItemsLayout()
|
|
314
|
+
|
|
315
|
+
def setBoundedView(
|
|
316
|
+
self,
|
|
317
|
+
view: TrustedSnapshotView | TrustedPresentationView,
|
|
318
|
+
*,
|
|
319
|
+
expanded_paths: set[str] | frozenset[str] | None = None,
|
|
320
|
+
expand_all: bool | None = None,
|
|
321
|
+
) -> None:
|
|
322
|
+
"""Construct one pre-bounded tree without Python recursion."""
|
|
323
|
+
if isinstance(view, TrustedSnapshotView):
|
|
324
|
+
self._set_snapshot_view(view)
|
|
325
|
+
return
|
|
326
|
+
if not isinstance(view, TrustedPresentationView):
|
|
327
|
+
raise TypeError("view must be a trusted bounded tree view")
|
|
328
|
+
|
|
329
|
+
self.clear()
|
|
330
|
+
if not view.nodes:
|
|
331
|
+
self.addTopLevelItem(qtw.QTreeWidgetItem(["No data", ""]))
|
|
332
|
+
return
|
|
333
|
+
|
|
334
|
+
qt_items: list[qtw.QTreeWidgetItem] = []
|
|
335
|
+
rendered_bytes = 0
|
|
336
|
+
for node_index, node in enumerate(
|
|
337
|
+
view.nodes[:TRUSTED_PRESENTATION_MAX_RENDERED_NODES]
|
|
338
|
+
):
|
|
339
|
+
node = cast(Any, node)
|
|
340
|
+
rendered_bytes += len(node.key.encode("utf-8")) + len(
|
|
341
|
+
node.value.encode("utf-8")
|
|
342
|
+
)
|
|
343
|
+
if rendered_bytes > TRUSTED_PRESENTATION_MAX_RENDERED_TEXT_BYTES:
|
|
344
|
+
break
|
|
345
|
+
item = qtw.QTreeWidgetItem([node.key, node.value])
|
|
346
|
+
item.setToolTip(1, getattr(node, "tooltip", node.value))
|
|
347
|
+
item.setToolTip(0, getattr(node, "tooltip", node.key))
|
|
348
|
+
path = str(getattr(node, "path", "") or "")
|
|
349
|
+
full_key_id = str(getattr(node, "full_key_id", "") or "")
|
|
350
|
+
full_value_id = str(getattr(node, "full_value_id", "") or "")
|
|
351
|
+
if path:
|
|
352
|
+
item.setData(0, FULL_VALUE_PATH_ROLE, path)
|
|
353
|
+
item.setData(1, FULL_VALUE_PATH_ROLE, path)
|
|
354
|
+
if full_key_id:
|
|
355
|
+
item.setData(0, FULL_VALUE_ID_ROLE, full_key_id)
|
|
356
|
+
if full_value_id:
|
|
357
|
+
item.setData(1, FULL_VALUE_ID_ROLE, full_value_id)
|
|
358
|
+
parent_index = node.parent_index
|
|
359
|
+
if (
|
|
360
|
+
parent_index is None
|
|
361
|
+
or parent_index < 0
|
|
362
|
+
or parent_index >= node_index
|
|
363
|
+
or parent_index >= len(qt_items)
|
|
364
|
+
):
|
|
365
|
+
self.addTopLevelItem(item)
|
|
366
|
+
else:
|
|
367
|
+
qt_items[parent_index].addChild(item)
|
|
368
|
+
qt_items.append(item)
|
|
369
|
+
|
|
370
|
+
if not qt_items:
|
|
371
|
+
self.addTopLevelItem(
|
|
372
|
+
qtw.QTreeWidgetItem(
|
|
373
|
+
["Snapshot unavailable", "The bounded view model is empty."]
|
|
374
|
+
)
|
|
375
|
+
)
|
|
376
|
+
return
|
|
377
|
+
|
|
378
|
+
should_expand_all = self.expand_all if expand_all is None else expand_all
|
|
379
|
+
if should_expand_all:
|
|
380
|
+
for item in qt_items:
|
|
381
|
+
item.setExpanded(True)
|
|
382
|
+
elif expanded_paths is not None:
|
|
383
|
+
for item in qt_items:
|
|
384
|
+
path = item.data(0, FULL_VALUE_PATH_ROLE)
|
|
385
|
+
if path:
|
|
386
|
+
item.setExpanded(str(path) in expanded_paths)
|
|
387
|
+
elif self.expand_top_level and item.parent() is None:
|
|
388
|
+
item.setExpanded(True)
|
|
389
|
+
elif self.expand_top_level:
|
|
390
|
+
for item in qt_items:
|
|
391
|
+
if item.parent() is None:
|
|
392
|
+
item.setExpanded(True)
|
|
393
|
+
header = self.header()
|
|
394
|
+
if header is not None:
|
|
395
|
+
header.setSectionResizeMode(0, qtw.QHeaderView.ResizeMode.ResizeToContents)
|
|
396
|
+
header.setSectionResizeMode(1, qtw.QHeaderView.ResizeMode.Stretch)
|
|
397
|
+
cast(Any, self).doItemsLayout()
|
|
398
|
+
|
|
399
|
+
def _set_snapshot_view(self, view: TrustedSnapshotView) -> None:
|
|
400
|
+
"""Install only the Snapshot root page; retained source stays off Qt."""
|
|
401
|
+
|
|
402
|
+
self._require_owner_thread()
|
|
403
|
+
session_token = getattr(view, "session_token", "")
|
|
404
|
+
root_handle = getattr(view, "root_handle", None)
|
|
405
|
+
continuation = getattr(view, "continuation", None)
|
|
406
|
+
nodes = tuple(view.nodes)
|
|
407
|
+
self.clear()
|
|
408
|
+
|
|
409
|
+
if not isinstance(session_token, str) or not session_token:
|
|
410
|
+
self._set_legacy_snapshot_nodes(nodes)
|
|
411
|
+
return
|
|
412
|
+
page_node_limit = (
|
|
413
|
+
TRUSTED_SNAPSHOT_PAGE_MAX_DATA_NODES_WITH_CONTINUATION
|
|
414
|
+
if continuation is not None
|
|
415
|
+
else TRUSTED_SNAPSHOT_PAGE_MAX_CHILDREN
|
|
416
|
+
)
|
|
417
|
+
if len(nodes) > page_node_limit:
|
|
418
|
+
self.addTopLevelItem(
|
|
419
|
+
qtw.QTreeWidgetItem(
|
|
420
|
+
["Snapshot unavailable", "The bounded view model is oversized."]
|
|
421
|
+
)
|
|
422
|
+
)
|
|
423
|
+
return
|
|
424
|
+
|
|
425
|
+
built = self._build_snapshot_items(
|
|
426
|
+
nodes,
|
|
427
|
+
session_token=session_token,
|
|
428
|
+
parent_handle=root_handle,
|
|
429
|
+
request_continuation=None,
|
|
430
|
+
parent_path="/Snapshot",
|
|
431
|
+
)
|
|
432
|
+
if built is None:
|
|
433
|
+
self.addTopLevelItem(
|
|
434
|
+
qtw.QTreeWidgetItem(
|
|
435
|
+
["Snapshot unavailable", "The bounded view model is invalid."]
|
|
436
|
+
)
|
|
437
|
+
)
|
|
438
|
+
return
|
|
439
|
+
|
|
440
|
+
roots, _all_items, rendered_bytes = built
|
|
441
|
+
if (
|
|
442
|
+
_snapshot_page_display_text_bytes(rendered_bytes, continuation)
|
|
443
|
+
> TRUSTED_SNAPSHOT_PAGE_MAX_TEXT_BYTES
|
|
444
|
+
):
|
|
445
|
+
self.addTopLevelItem(
|
|
446
|
+
qtw.QTreeWidgetItem(
|
|
447
|
+
["Snapshot unavailable", "The bounded view model is oversized."]
|
|
448
|
+
)
|
|
449
|
+
)
|
|
450
|
+
return
|
|
451
|
+
|
|
452
|
+
self._snapshot_session_token = session_token
|
|
453
|
+
self._snapshot_root_handle = root_handle
|
|
454
|
+
blocker = QtCore.QSignalBlocker(self)
|
|
455
|
+
try:
|
|
456
|
+
if roots:
|
|
457
|
+
self.addTopLevelItems(roots)
|
|
458
|
+
if continuation is not None:
|
|
459
|
+
self.addTopLevelItem(
|
|
460
|
+
self._snapshot_continuation_item(
|
|
461
|
+
session_token,
|
|
462
|
+
cast(TrustedSnapshotContainerHandle, root_handle),
|
|
463
|
+
cast(TrustedSnapshotContinuation, continuation),
|
|
464
|
+
"/Snapshot",
|
|
465
|
+
)
|
|
466
|
+
)
|
|
467
|
+
if not roots and continuation is None:
|
|
468
|
+
self.addTopLevelItem(qtw.QTreeWidgetItem(["No data", ""]))
|
|
469
|
+
finally:
|
|
470
|
+
del blocker
|
|
471
|
+
self._finish_bounded_layout()
|
|
472
|
+
|
|
473
|
+
def _set_legacy_snapshot_nodes(self, nodes: tuple[object, ...]) -> None:
|
|
474
|
+
"""Render old/terminal views while no lazy source handle is available."""
|
|
475
|
+
|
|
476
|
+
if not nodes:
|
|
477
|
+
self.addTopLevelItem(qtw.QTreeWidgetItem(["No data", ""]))
|
|
478
|
+
return
|
|
479
|
+
qt_items: list[qtw.QTreeWidgetItem] = []
|
|
480
|
+
rendered_bytes = 0
|
|
481
|
+
for node_index, raw_node in enumerate(
|
|
482
|
+
nodes[:TRUSTED_SNAPSHOT_PAGE_MAX_CHILDREN]
|
|
483
|
+
):
|
|
484
|
+
node = cast(Any, raw_node)
|
|
485
|
+
if not isinstance(node.key, str) or not isinstance(node.value, str):
|
|
486
|
+
break
|
|
487
|
+
rendered_bytes += len(node.key.encode("utf-8")) + len(
|
|
488
|
+
node.value.encode("utf-8")
|
|
489
|
+
)
|
|
490
|
+
if rendered_bytes > TRUSTED_SNAPSHOT_PAGE_MAX_TEXT_BYTES:
|
|
491
|
+
break
|
|
492
|
+
item = qtw.QTreeWidgetItem([node.key, node.value])
|
|
493
|
+
tooltip = getattr(node, "tooltip", node.value)
|
|
494
|
+
if isinstance(tooltip, str):
|
|
495
|
+
item.setToolTip(0, tooltip)
|
|
496
|
+
item.setToolTip(1, tooltip)
|
|
497
|
+
parent_index = node.parent_index
|
|
498
|
+
if (
|
|
499
|
+
parent_index is None
|
|
500
|
+
or type(parent_index) is not int
|
|
501
|
+
or parent_index < 0
|
|
502
|
+
or parent_index >= node_index
|
|
503
|
+
or parent_index >= len(qt_items)
|
|
504
|
+
):
|
|
505
|
+
self.addTopLevelItem(item)
|
|
506
|
+
else:
|
|
507
|
+
qt_items[parent_index].addChild(item)
|
|
508
|
+
qt_items.append(item)
|
|
509
|
+
if not qt_items:
|
|
510
|
+
self.addTopLevelItem(
|
|
511
|
+
qtw.QTreeWidgetItem(
|
|
512
|
+
["Snapshot unavailable", "The bounded view model is empty."]
|
|
513
|
+
)
|
|
514
|
+
)
|
|
515
|
+
return
|
|
516
|
+
self._finish_bounded_layout()
|
|
517
|
+
|
|
518
|
+
def _build_snapshot_items(
|
|
519
|
+
self,
|
|
520
|
+
nodes: tuple[object, ...],
|
|
521
|
+
*,
|
|
522
|
+
session_token: str,
|
|
523
|
+
parent_handle: object,
|
|
524
|
+
request_continuation: object | None,
|
|
525
|
+
parent_path: str,
|
|
526
|
+
) -> (
|
|
527
|
+
tuple[
|
|
528
|
+
list[qtw.QTreeWidgetItem],
|
|
529
|
+
list[qtw.QTreeWidgetItem],
|
|
530
|
+
int,
|
|
531
|
+
]
|
|
532
|
+
| None
|
|
533
|
+
):
|
|
534
|
+
"""Build a detached, bounded page so rejection never partially mutates Qt."""
|
|
535
|
+
|
|
536
|
+
if len(nodes) > TRUSTED_SNAPSHOT_PAGE_MAX_CHILDREN:
|
|
537
|
+
return None
|
|
538
|
+
roots: list[qtw.QTreeWidgetItem] = []
|
|
539
|
+
qt_items: list[qtw.QTreeWidgetItem] = []
|
|
540
|
+
rendered_bytes = 0
|
|
541
|
+
for node_index, raw_node in enumerate(nodes):
|
|
542
|
+
node = cast(Any, raw_node)
|
|
543
|
+
key = getattr(node, "key", None)
|
|
544
|
+
value = getattr(node, "value", None)
|
|
545
|
+
parent_index = getattr(node, "parent_index", None)
|
|
546
|
+
if not isinstance(key, str) or not isinstance(value, str):
|
|
547
|
+
return None
|
|
548
|
+
if parent_index is not None and (
|
|
549
|
+
type(parent_index) is not int
|
|
550
|
+
or parent_index < 0
|
|
551
|
+
or parent_index >= node_index
|
|
552
|
+
or parent_index >= len(qt_items)
|
|
553
|
+
):
|
|
554
|
+
return None
|
|
555
|
+
rendered_bytes += len(key.encode("utf-8")) + len(value.encode("utf-8"))
|
|
556
|
+
if rendered_bytes > TRUSTED_SNAPSHOT_PAGE_MAX_TEXT_BYTES:
|
|
557
|
+
return None
|
|
558
|
+
|
|
559
|
+
item = qtw.QTreeWidgetItem([key, value])
|
|
560
|
+
tooltip = getattr(node, "tooltip", value)
|
|
561
|
+
if not isinstance(tooltip, str):
|
|
562
|
+
return None
|
|
563
|
+
item.setToolTip(0, tooltip)
|
|
564
|
+
item.setToolTip(1, tooltip)
|
|
565
|
+
path_parent = (
|
|
566
|
+
parent_path
|
|
567
|
+
if parent_index is None
|
|
568
|
+
else str(
|
|
569
|
+
qt_items[parent_index].data(0, FULL_VALUE_PATH_ROLE) or parent_path
|
|
570
|
+
)
|
|
571
|
+
)
|
|
572
|
+
path = f"{path_parent.rstrip('/')}/{key}"
|
|
573
|
+
for column in (0, 1):
|
|
574
|
+
item.setData(column, SNAPSHOT_SESSION_ROLE, session_token)
|
|
575
|
+
item.setData(column, SNAPSHOT_PARENT_ROLE, parent_handle)
|
|
576
|
+
item.setData(
|
|
577
|
+
column,
|
|
578
|
+
SNAPSHOT_REQUEST_CONTINUATION_ROLE,
|
|
579
|
+
request_continuation,
|
|
580
|
+
)
|
|
581
|
+
item.setData(column, FULL_VALUE_PATH_ROLE, path)
|
|
582
|
+
|
|
583
|
+
container_handle = getattr(node, "container_handle", None)
|
|
584
|
+
if container_handle is not None:
|
|
585
|
+
item.setData(0, SNAPSHOT_CONTAINER_ROLE, container_handle)
|
|
586
|
+
item.setData(1, SNAPSHOT_CONTAINER_ROLE, container_handle)
|
|
587
|
+
item.setData(0, SNAPSHOT_CONTAINER_LOADED_ROLE, False)
|
|
588
|
+
item.setChildIndicatorPolicy(
|
|
589
|
+
qtw.QTreeWidgetItem.ChildIndicatorPolicy.ShowIndicator
|
|
590
|
+
)
|
|
591
|
+
|
|
592
|
+
full_key_handle = getattr(node, "full_key_handle", None)
|
|
593
|
+
full_value_handle = getattr(node, "full_value_handle", None)
|
|
594
|
+
if full_key_handle is not None:
|
|
595
|
+
item.setData(0, SNAPSHOT_FULL_VALUE_ROLE, full_key_handle)
|
|
596
|
+
if full_value_handle is not None:
|
|
597
|
+
item.setData(1, SNAPSHOT_FULL_VALUE_ROLE, full_value_handle)
|
|
598
|
+
|
|
599
|
+
if parent_index is None:
|
|
600
|
+
roots.append(item)
|
|
601
|
+
else:
|
|
602
|
+
qt_items[parent_index].addChild(item)
|
|
603
|
+
qt_items.append(item)
|
|
604
|
+
return roots, qt_items, rendered_bytes
|
|
605
|
+
|
|
606
|
+
@staticmethod
|
|
607
|
+
def _snapshot_continuation_item(
|
|
608
|
+
session_token: str,
|
|
609
|
+
parent_handle: TrustedSnapshotContainerHandle,
|
|
610
|
+
continuation: TrustedSnapshotContinuation,
|
|
611
|
+
parent_path: str,
|
|
612
|
+
) -> qtw.QTreeWidgetItem:
|
|
613
|
+
item = qtw.QTreeWidgetItem([TRUSTED_SNAPSHOT_LOAD_MORE_TEXT, ""])
|
|
614
|
+
for column in (0, 1):
|
|
615
|
+
item.setData(column, SNAPSHOT_SESSION_ROLE, session_token)
|
|
616
|
+
item.setData(column, SNAPSHOT_PARENT_ROLE, parent_handle)
|
|
617
|
+
item.setData(column, SNAPSHOT_CONTINUATION_ROLE, continuation)
|
|
618
|
+
item.setData(column, SNAPSHOT_LOAD_MORE_ROLE, True)
|
|
619
|
+
item.setData(column, FULL_VALUE_PATH_ROLE, parent_path)
|
|
620
|
+
item.setToolTip(0, "Load the next bounded page of Snapshot entries.")
|
|
621
|
+
item.setToolTip(1, item.toolTip(0))
|
|
622
|
+
return item
|
|
623
|
+
|
|
624
|
+
@QtCore.pyqtSlot(qtw.QTreeWidgetItem)
|
|
625
|
+
def _request_expanded_snapshot_container(self, item) -> None:
|
|
626
|
+
if not self._snapshot_session_token:
|
|
627
|
+
return
|
|
628
|
+
container_handle = item.data(0, SNAPSHOT_CONTAINER_ROLE)
|
|
629
|
+
if container_handle is None or bool(
|
|
630
|
+
item.data(0, SNAPSHOT_CONTAINER_LOADED_ROLE)
|
|
631
|
+
):
|
|
632
|
+
return
|
|
633
|
+
self._emit_snapshot_page_request(item, container_handle, None)
|
|
634
|
+
|
|
635
|
+
def _emit_snapshot_page_request(
|
|
636
|
+
self,
|
|
637
|
+
item: qtw.QTreeWidgetItem,
|
|
638
|
+
parent_handle: TrustedSnapshotContainerHandle,
|
|
639
|
+
continuation: TrustedSnapshotContinuation | None,
|
|
640
|
+
) -> bool:
|
|
641
|
+
self._require_owner_thread()
|
|
642
|
+
session_token = item.data(0, SNAPSHOT_SESSION_ROLE)
|
|
643
|
+
if (
|
|
644
|
+
not isinstance(session_token, str)
|
|
645
|
+
or not session_token
|
|
646
|
+
or session_token != self._snapshot_session_token
|
|
647
|
+
or parent_handle is None
|
|
648
|
+
or item.treeWidget() is not self
|
|
649
|
+
or any(
|
|
650
|
+
request.parent_handle == parent_handle
|
|
651
|
+
for request in self._snapshot_pending_requests
|
|
652
|
+
)
|
|
653
|
+
):
|
|
654
|
+
return False
|
|
655
|
+
if continuation is not None and (
|
|
656
|
+
not bool(item.data(0, SNAPSHOT_LOAD_MORE_ROLE))
|
|
657
|
+
or item.data(0, SNAPSHOT_CONTINUATION_ROLE) != continuation
|
|
658
|
+
):
|
|
659
|
+
return False
|
|
660
|
+
request = SnapshotTreePageRequest(
|
|
661
|
+
session_token,
|
|
662
|
+
parent_handle,
|
|
663
|
+
continuation,
|
|
664
|
+
)
|
|
665
|
+
self._snapshot_pending_requests.append(request)
|
|
666
|
+
item.setData(0, SNAPSHOT_PAGE_PENDING_ROLE, request)
|
|
667
|
+
item.setData(1, SNAPSHOT_PAGE_PENDING_ROLE, request)
|
|
668
|
+
self.snapshotPageRequested.emit(request)
|
|
669
|
+
return True
|
|
670
|
+
|
|
671
|
+
def acceptSnapshotPage(self, page: TrustedSnapshotPage) -> bool:
|
|
672
|
+
"""Atomically accept one exact pending Snapshot page on the owner thread."""
|
|
673
|
+
|
|
674
|
+
self._require_owner_thread()
|
|
675
|
+
if not isinstance(page, TrustedSnapshotPage):
|
|
676
|
+
return False
|
|
677
|
+
parent_handle = page.parent_handle
|
|
678
|
+
request_continuation = page.request_continuation
|
|
679
|
+
nodes = page.nodes
|
|
680
|
+
continuation = page.continuation
|
|
681
|
+
rendered_text_bytes = page.rendered_text_bytes
|
|
682
|
+
if (
|
|
683
|
+
not self._snapshot_session_token
|
|
684
|
+
or parent_handle is None
|
|
685
|
+
or not isinstance(nodes, tuple)
|
|
686
|
+
or type(rendered_text_bytes) is not int
|
|
687
|
+
or rendered_text_bytes < 0
|
|
688
|
+
or _snapshot_page_display_text_bytes(
|
|
689
|
+
rendered_text_bytes,
|
|
690
|
+
continuation,
|
|
691
|
+
)
|
|
692
|
+
> TRUSTED_SNAPSHOT_PAGE_MAX_TEXT_BYTES
|
|
693
|
+
):
|
|
694
|
+
return False
|
|
695
|
+
page_node_limit = (
|
|
696
|
+
TRUSTED_SNAPSHOT_PAGE_MAX_DATA_NODES_WITH_CONTINUATION
|
|
697
|
+
if continuation is not None
|
|
698
|
+
else TRUSTED_SNAPSHOT_PAGE_MAX_CHILDREN
|
|
699
|
+
)
|
|
700
|
+
if len(nodes) > page_node_limit:
|
|
701
|
+
return False
|
|
702
|
+
request = next(
|
|
703
|
+
(
|
|
704
|
+
candidate
|
|
705
|
+
for candidate in self._snapshot_pending_requests
|
|
706
|
+
if candidate.session_token == self._snapshot_session_token
|
|
707
|
+
and candidate.parent_handle == parent_handle
|
|
708
|
+
and candidate.continuation == request_continuation
|
|
709
|
+
),
|
|
710
|
+
None,
|
|
711
|
+
)
|
|
712
|
+
if request is None:
|
|
713
|
+
return False
|
|
714
|
+
|
|
715
|
+
parent_item = self._snapshot_item_for_container(parent_handle)
|
|
716
|
+
if parent_handle == self._snapshot_root_handle:
|
|
717
|
+
parent_item = None
|
|
718
|
+
elif parent_item is None:
|
|
719
|
+
return False
|
|
720
|
+
request_item = self._snapshot_request_item(request, parent_item)
|
|
721
|
+
if request_item is None:
|
|
722
|
+
return False
|
|
723
|
+
parent_path = (
|
|
724
|
+
"/Snapshot"
|
|
725
|
+
if parent_item is None
|
|
726
|
+
else str(parent_item.data(0, FULL_VALUE_PATH_ROLE) or "/Snapshot")
|
|
727
|
+
)
|
|
728
|
+
built = self._build_snapshot_items(
|
|
729
|
+
nodes,
|
|
730
|
+
session_token=self._snapshot_session_token,
|
|
731
|
+
parent_handle=parent_handle,
|
|
732
|
+
request_continuation=request_continuation,
|
|
733
|
+
parent_path=parent_path,
|
|
734
|
+
)
|
|
735
|
+
if built is None:
|
|
736
|
+
return False
|
|
737
|
+
roots, _all_items, built_bytes = built
|
|
738
|
+
if built_bytes != rendered_text_bytes:
|
|
739
|
+
return False
|
|
740
|
+
self._snapshot_pending_requests.remove(request)
|
|
741
|
+
blocker = QtCore.QSignalBlocker(self)
|
|
742
|
+
try:
|
|
743
|
+
if parent_item is not None:
|
|
744
|
+
parent_item.setData(0, SNAPSHOT_CONTAINER_LOADED_ROLE, True)
|
|
745
|
+
parent_item.setData(0, SNAPSHOT_PAGE_PENDING_ROLE, None)
|
|
746
|
+
parent_item.setData(1, SNAPSHOT_PAGE_PENDING_ROLE, None)
|
|
747
|
+
if request_continuation is not None:
|
|
748
|
+
self._remove_snapshot_item(request_item)
|
|
749
|
+
if roots:
|
|
750
|
+
if parent_item is None:
|
|
751
|
+
self.addTopLevelItems(roots)
|
|
752
|
+
else:
|
|
753
|
+
parent_item.addChildren(roots)
|
|
754
|
+
if continuation is not None:
|
|
755
|
+
more_item = self._snapshot_continuation_item(
|
|
756
|
+
self._snapshot_session_token,
|
|
757
|
+
parent_handle,
|
|
758
|
+
continuation,
|
|
759
|
+
parent_path,
|
|
760
|
+
)
|
|
761
|
+
if parent_item is None:
|
|
762
|
+
self.addTopLevelItem(more_item)
|
|
763
|
+
else:
|
|
764
|
+
parent_item.addChild(more_item)
|
|
765
|
+
if parent_item is not None and parent_item.childCount() == 0:
|
|
766
|
+
parent_item.setChildIndicatorPolicy(
|
|
767
|
+
qtw.QTreeWidgetItem.ChildIndicatorPolicy.DontShowIndicatorWhenChildless
|
|
768
|
+
)
|
|
769
|
+
finally:
|
|
770
|
+
del blocker
|
|
771
|
+
self._finish_bounded_layout()
|
|
772
|
+
return True
|
|
773
|
+
|
|
774
|
+
def rejectSnapshotPage(self, request: object) -> bool:
|
|
775
|
+
"""Release one exact pending page without accepting stale page content."""
|
|
776
|
+
|
|
777
|
+
self._require_owner_thread()
|
|
778
|
+
if not isinstance(request, SnapshotTreePageRequest):
|
|
779
|
+
return False
|
|
780
|
+
matched = next(
|
|
781
|
+
(
|
|
782
|
+
candidate
|
|
783
|
+
for candidate in self._snapshot_pending_requests
|
|
784
|
+
if candidate == request
|
|
785
|
+
and candidate.session_token == self._snapshot_session_token
|
|
786
|
+
),
|
|
787
|
+
None,
|
|
788
|
+
)
|
|
789
|
+
if matched is None:
|
|
790
|
+
return False
|
|
791
|
+
self._snapshot_pending_requests.remove(matched)
|
|
792
|
+
item = self._snapshot_request_item(matched, None)
|
|
793
|
+
if item is not None:
|
|
794
|
+
item.setData(0, SNAPSHOT_PAGE_PENDING_ROLE, None)
|
|
795
|
+
item.setData(1, SNAPSHOT_PAGE_PENDING_ROLE, None)
|
|
796
|
+
return True
|
|
797
|
+
|
|
798
|
+
def invalidateSnapshot(self) -> None:
|
|
799
|
+
"""Drop the page session and every item/opaque role it owned."""
|
|
800
|
+
|
|
801
|
+
self._require_owner_thread()
|
|
802
|
+
self.clear()
|
|
803
|
+
|
|
804
|
+
def _snapshot_item_for_container(
|
|
805
|
+
self,
|
|
806
|
+
container_handle: object,
|
|
807
|
+
) -> qtw.QTreeWidgetItem | None:
|
|
808
|
+
iterator = qtw.QTreeWidgetItemIterator(self)
|
|
809
|
+
while (item := iterator.value()) is not None:
|
|
810
|
+
if item.data(0, SNAPSHOT_CONTAINER_ROLE) == container_handle:
|
|
811
|
+
return item
|
|
812
|
+
iterator += 1
|
|
813
|
+
return None
|
|
814
|
+
|
|
815
|
+
def _snapshot_request_item(
|
|
816
|
+
self,
|
|
817
|
+
request: SnapshotTreePageRequest,
|
|
818
|
+
parent_item: qtw.QTreeWidgetItem | None,
|
|
819
|
+
) -> qtw.QTreeWidgetItem | None:
|
|
820
|
+
iterator = qtw.QTreeWidgetItemIterator(self)
|
|
821
|
+
while (item := iterator.value()) is not None:
|
|
822
|
+
if item.data(0, SNAPSHOT_PAGE_PENDING_ROLE) == request:
|
|
823
|
+
if (
|
|
824
|
+
parent_item is None
|
|
825
|
+
or item is parent_item
|
|
826
|
+
or item.parent() is parent_item
|
|
827
|
+
):
|
|
828
|
+
return item
|
|
829
|
+
iterator += 1
|
|
830
|
+
return None
|
|
831
|
+
|
|
832
|
+
def _remove_snapshot_item(self, item: qtw.QTreeWidgetItem) -> None:
|
|
833
|
+
parent = item.parent()
|
|
834
|
+
if parent is None:
|
|
835
|
+
index = self.indexOfTopLevelItem(item)
|
|
836
|
+
if index >= 0:
|
|
837
|
+
self.takeTopLevelItem(index)
|
|
838
|
+
else:
|
|
839
|
+
index = parent.indexOfChild(item)
|
|
840
|
+
if index >= 0:
|
|
841
|
+
parent.takeChild(index)
|
|
842
|
+
|
|
843
|
+
def _finish_bounded_layout(self) -> None:
|
|
844
|
+
header = self.header()
|
|
845
|
+
if header is not None:
|
|
846
|
+
header.setSectionResizeMode(0, qtw.QHeaderView.ResizeMode.ResizeToContents)
|
|
847
|
+
header.setSectionResizeMode(1, qtw.QHeaderView.ResizeMode.Stretch)
|
|
848
|
+
cast(Any, self).doItemsLayout()
|
|
849
|
+
|
|
850
|
+
def _require_owner_thread(self) -> None:
|
|
851
|
+
if QtCore.QThread.currentThread() != self.thread():
|
|
852
|
+
raise RuntimeError(
|
|
853
|
+
"Snapshot tree mutation must run on its Qt owner thread."
|
|
854
|
+
)
|
|
855
|
+
|
|
856
|
+
def expandedPaths(self) -> frozenset[str]:
|
|
857
|
+
expanded = set()
|
|
858
|
+
iterator = qtw.QTreeWidgetItemIterator(self)
|
|
859
|
+
while True:
|
|
860
|
+
item = iterator.value()
|
|
861
|
+
if item is None:
|
|
862
|
+
break
|
|
863
|
+
path = item.data(0, FULL_VALUE_PATH_ROLE)
|
|
864
|
+
if path and item.isExpanded():
|
|
865
|
+
expanded.add(str(path))
|
|
866
|
+
iterator += 1
|
|
867
|
+
return frozenset(expanded)
|
|
868
|
+
|
|
869
|
+
@QtCore.pyqtSlot(qtw.QTreeWidgetItem, int)
|
|
870
|
+
def _activate_full_value(self, item, column) -> None:
|
|
871
|
+
requested_column = 1 if int(column) == 1 else 0
|
|
872
|
+
if bool(item.data(0, SNAPSHOT_LOAD_MORE_ROLE)):
|
|
873
|
+
parent_handle = item.data(0, SNAPSHOT_PARENT_ROLE)
|
|
874
|
+
continuation = item.data(0, SNAPSHOT_CONTINUATION_ROLE)
|
|
875
|
+
if parent_handle is not None and continuation is not None:
|
|
876
|
+
self._emit_snapshot_page_request(
|
|
877
|
+
item,
|
|
878
|
+
parent_handle,
|
|
879
|
+
continuation,
|
|
880
|
+
)
|
|
881
|
+
return
|
|
882
|
+
if self._activate_snapshot_full_value(item, requested_column):
|
|
883
|
+
return
|
|
884
|
+
identifier = str(item.data(requested_column, FULL_VALUE_ID_ROLE) or "")
|
|
885
|
+
value_kind = "value" if requested_column == 1 else "key"
|
|
886
|
+
if not identifier:
|
|
887
|
+
requested_column = 1 - requested_column
|
|
888
|
+
identifier = str(item.data(requested_column, FULL_VALUE_ID_ROLE) or "")
|
|
889
|
+
value_kind = "value" if requested_column == 1 else "key"
|
|
890
|
+
if identifier:
|
|
891
|
+
path = str(
|
|
892
|
+
item.data(requested_column, FULL_VALUE_PATH_ROLE) or item.text(0)
|
|
893
|
+
)
|
|
894
|
+
self.fullValueRequested.emit(
|
|
895
|
+
identifier,
|
|
896
|
+
f"{path} ({value_kind})",
|
|
897
|
+
)
|
|
898
|
+
|
|
899
|
+
def _activate_snapshot_full_value(
|
|
900
|
+
self,
|
|
901
|
+
item: qtw.QTreeWidgetItem,
|
|
902
|
+
requested_column: int,
|
|
903
|
+
) -> bool:
|
|
904
|
+
handle = item.data(requested_column, SNAPSHOT_FULL_VALUE_ROLE)
|
|
905
|
+
value_kind = "value" if requested_column == 1 else "key"
|
|
906
|
+
if handle is None:
|
|
907
|
+
requested_column = 1 - requested_column
|
|
908
|
+
handle = item.data(requested_column, SNAPSHOT_FULL_VALUE_ROLE)
|
|
909
|
+
value_kind = "value" if requested_column == 1 else "key"
|
|
910
|
+
if handle is None:
|
|
911
|
+
return False
|
|
912
|
+
session_token = item.data(requested_column, SNAPSHOT_SESSION_ROLE)
|
|
913
|
+
parent_handle = item.data(requested_column, SNAPSHOT_PARENT_ROLE)
|
|
914
|
+
if (
|
|
915
|
+
not isinstance(session_token, str)
|
|
916
|
+
or session_token != self._snapshot_session_token
|
|
917
|
+
or item.treeWidget() is not self
|
|
918
|
+
):
|
|
919
|
+
return True
|
|
920
|
+
path = item.data(requested_column, FULL_VALUE_PATH_ROLE)
|
|
921
|
+
if not isinstance(path, str) or not path:
|
|
922
|
+
path = item.text(0)
|
|
923
|
+
self.snapshotFullValueRequested.emit(
|
|
924
|
+
SnapshotTreeValueRequest(
|
|
925
|
+
session_token,
|
|
926
|
+
handle,
|
|
927
|
+
f"{path} ({value_kind})",
|
|
928
|
+
parent_handle,
|
|
929
|
+
item.data(requested_column, SNAPSHOT_REQUEST_CONTINUATION_ROLE),
|
|
930
|
+
)
|
|
931
|
+
)
|
|
932
|
+
return True
|
|
933
|
+
|
|
934
|
+
@QtCore.pyqtSlot()
|
|
935
|
+
def _request_current_full_value(self) -> None:
|
|
936
|
+
item = self.currentItem()
|
|
937
|
+
if item is not None:
|
|
938
|
+
self._activate_full_value(item, self.currentColumn())
|
|
939
|
+
|
|
940
|
+
def resizeEvent(self, event):
|
|
941
|
+
super().resizeEvent(event)
|
|
942
|
+
cast(Any, self).doItemsLayout()
|
|
943
|
+
|
|
944
|
+
def openCopyMenu(self, pos):
|
|
945
|
+
item = self.itemAt(pos)
|
|
946
|
+
if item is None:
|
|
947
|
+
return
|
|
948
|
+
|
|
949
|
+
index = self.indexAt(pos)
|
|
950
|
+
column = index.column() if index.isValid() else 0
|
|
951
|
+
self.setCurrentItem(item, column)
|
|
952
|
+
|
|
953
|
+
menu = qtw.QMenu(self)
|
|
954
|
+
if any(
|
|
955
|
+
item.data(col, FULL_VALUE_ID_ROLE)
|
|
956
|
+
or item.data(col, SNAPSHOT_FULL_VALUE_ROLE) is not None
|
|
957
|
+
for col in (0, 1)
|
|
958
|
+
):
|
|
959
|
+
menu.addAction(self.view_full_value_action)
|
|
960
|
+
menu.addSeparator()
|
|
961
|
+
menu.addAction(self.copy_value_action)
|
|
962
|
+
|
|
963
|
+
copy_row = QtGui.QAction("Copy Row", menu)
|
|
964
|
+
copy_row.triggered.connect(lambda: copy_to_clipboard(row_text(item)))
|
|
965
|
+
menu.addAction(copy_row)
|
|
966
|
+
|
|
967
|
+
if self.selectedItems():
|
|
968
|
+
menu.addAction(self.copy_selection_action)
|
|
969
|
+
|
|
970
|
+
viewport = self.viewport()
|
|
971
|
+
if viewport is not None:
|
|
972
|
+
menu.exec(viewport.mapToGlobal(pos))
|
|
973
|
+
|
|
974
|
+
def copyValue(self):
|
|
975
|
+
item = self.currentItem()
|
|
976
|
+
if item is not None:
|
|
977
|
+
copy_to_clipboard(item.text(1))
|
|
978
|
+
|
|
979
|
+
def copySelection(self):
|
|
980
|
+
items = self.selectedItems()
|
|
981
|
+
current_item = self.currentItem()
|
|
982
|
+
if not items and current_item is not None:
|
|
983
|
+
items = [current_item]
|
|
984
|
+
copy_to_clipboard("\n".join(row_text(item) for item in items))
|
|
985
|
+
|
|
986
|
+
|
|
987
|
+
class CopyableTableWidget(qtw.QTableWidget):
|
|
988
|
+
def __init__(self, *args, **kwargs):
|
|
989
|
+
super().__init__(*args, **kwargs)
|
|
990
|
+
self.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.CustomContextMenu)
|
|
991
|
+
self.customContextMenuRequested.connect(self.openCopyMenu)
|
|
992
|
+
|
|
993
|
+
self.copy_cell_action = copy_action(
|
|
994
|
+
"Copy Cell", COPY_CELL_SHORTCUTS, self.copyCell, self
|
|
995
|
+
)
|
|
996
|
+
self.copy_selection_action = copy_action(
|
|
997
|
+
"Copy Selection", COPY_SELECTION_SHORTCUTS, self.copySelection, self
|
|
998
|
+
)
|
|
999
|
+
self.addActions([self.copy_cell_action, self.copy_selection_action])
|
|
1000
|
+
|
|
1001
|
+
def openCopyMenu(self, pos):
|
|
1002
|
+
item = self.itemAt(pos)
|
|
1003
|
+
if item is None:
|
|
1004
|
+
return
|
|
1005
|
+
|
|
1006
|
+
self.setCurrentItem(item)
|
|
1007
|
+
|
|
1008
|
+
menu = qtw.QMenu(self)
|
|
1009
|
+
menu.addAction(self.copy_cell_action)
|
|
1010
|
+
menu.addAction(self.copy_selection_action)
|
|
1011
|
+
|
|
1012
|
+
viewport = self.viewport()
|
|
1013
|
+
if viewport is not None:
|
|
1014
|
+
menu.exec(viewport.mapToGlobal(pos))
|
|
1015
|
+
|
|
1016
|
+
def copyCell(self):
|
|
1017
|
+
item = self.currentItem()
|
|
1018
|
+
if item is not None:
|
|
1019
|
+
copy_to_clipboard(item.text())
|
|
1020
|
+
|
|
1021
|
+
def copySelection(self):
|
|
1022
|
+
ranges = self.selectedRanges()
|
|
1023
|
+
if not ranges and self.currentItem() is not None:
|
|
1024
|
+
self.copyCell()
|
|
1025
|
+
return
|
|
1026
|
+
|
|
1027
|
+
sections = []
|
|
1028
|
+
for selected_range in ranges:
|
|
1029
|
+
rows = []
|
|
1030
|
+
for row in range(selected_range.topRow(), selected_range.bottomRow() + 1):
|
|
1031
|
+
values = []
|
|
1032
|
+
for col in range(
|
|
1033
|
+
selected_range.leftColumn(),
|
|
1034
|
+
selected_range.rightColumn() + 1,
|
|
1035
|
+
):
|
|
1036
|
+
item = self.item(row, col)
|
|
1037
|
+
values.append(item.text() if item is not None else "")
|
|
1038
|
+
rows.append("\t".join(values))
|
|
1039
|
+
sections.append("\n".join(rows))
|
|
1040
|
+
|
|
1041
|
+
copy_to_clipboard("\n".join(section for section in sections if section))
|
|
1042
|
+
|
|
1043
|
+
|
|
1044
|
+
def dictToTree(d: dict, truncate_values=False):
|
|
1045
|
+
items = []
|
|
1046
|
+
for k, v in d.items():
|
|
1047
|
+
if not isinstance(v, dict):
|
|
1048
|
+
item = qtw.QTreeWidgetItem(
|
|
1049
|
+
[str(k), format_value(v, 180 if truncate_values else None)]
|
|
1050
|
+
)
|
|
1051
|
+
item.setToolTip(1, format_value(v))
|
|
1052
|
+
else:
|
|
1053
|
+
item = qtw.QTreeWidgetItem([k, ""])
|
|
1054
|
+
for child in dictToTree(v, truncate_values=truncate_values):
|
|
1055
|
+
item.addChild(child)
|
|
1056
|
+
items.append(item)
|
|
1057
|
+
return items
|
|
1058
|
+
|
|
1059
|
+
|
|
1060
|
+
def snapshot_parameters(snapshot):
|
|
1061
|
+
if not isinstance(snapshot, dict):
|
|
1062
|
+
return {}
|
|
1063
|
+
|
|
1064
|
+
out = {}
|
|
1065
|
+
parameter_dicts = []
|
|
1066
|
+
|
|
1067
|
+
params = snapshot.get("parameters")
|
|
1068
|
+
if isinstance(params, dict):
|
|
1069
|
+
parameter_dicts.append(params)
|
|
1070
|
+
|
|
1071
|
+
station = snapshot.get("station")
|
|
1072
|
+
if isinstance(station, dict):
|
|
1073
|
+
params = station.get("parameters")
|
|
1074
|
+
if isinstance(params, dict):
|
|
1075
|
+
parameter_dicts.append(params)
|
|
1076
|
+
|
|
1077
|
+
instruments = station.get("instruments")
|
|
1078
|
+
if isinstance(instruments, dict):
|
|
1079
|
+
for instrument in instruments.values():
|
|
1080
|
+
if isinstance(instrument, dict) and isinstance(
|
|
1081
|
+
instrument.get("parameters"), dict
|
|
1082
|
+
):
|
|
1083
|
+
parameter_dicts.append(instrument["parameters"])
|
|
1084
|
+
|
|
1085
|
+
for params in parameter_dicts:
|
|
1086
|
+
for key, details in params.items():
|
|
1087
|
+
if not isinstance(details, dict):
|
|
1088
|
+
continue
|
|
1089
|
+
for name in (key, details.get("name"), details.get("full_name")):
|
|
1090
|
+
if name:
|
|
1091
|
+
out[str(name)] = details
|
|
1092
|
+
|
|
1093
|
+
return out
|
|
1094
|
+
|
|
1095
|
+
|
|
1096
|
+
def format_value(value, max_len=None):
|
|
1097
|
+
if value is None:
|
|
1098
|
+
text = ""
|
|
1099
|
+
elif isinstance(value, float):
|
|
1100
|
+
text = f"{value:.6g}"
|
|
1101
|
+
elif isinstance(value, (list, tuple)):
|
|
1102
|
+
text = ", ".join(format_value(item) for item in value)
|
|
1103
|
+
else:
|
|
1104
|
+
text = str(value)
|
|
1105
|
+
|
|
1106
|
+
text = text.replace("\n", " ")
|
|
1107
|
+
if max_len is not None and len(text) > max_len:
|
|
1108
|
+
return text[: max_len - 3] + "..."
|
|
1109
|
+
return text
|
|
1110
|
+
|
|
1111
|
+
|
|
1112
|
+
def row_text(item):
|
|
1113
|
+
return "\t".join(item.text(col) for col in range(item.columnCount()))
|
|
1114
|
+
|
|
1115
|
+
|
|
1116
|
+
def copy_to_clipboard(text):
|
|
1117
|
+
clipboard = qtw.QApplication.clipboard()
|
|
1118
|
+
if clipboard is not None:
|
|
1119
|
+
clipboard.setText(text)
|