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,656 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from typing import TYPE_CHECKING, Any, Literal, TypedDict
|
|
3
|
+
|
|
4
|
+
import pyqtgraph as pg
|
|
5
|
+
from PyQt6 import QtCore, QtGui
|
|
6
|
+
from PyQt6 import QtWidgets as qtw
|
|
7
|
+
|
|
8
|
+
from qplot.tools.sample_statistics import finite_mean, finite_standard_deviation
|
|
9
|
+
|
|
10
|
+
from ._widgets import CopyableTableWidget
|
|
11
|
+
|
|
12
|
+
_MarqueeHandle = Literal["nw", "n", "ne", "e", "se", "s", "sw", "w"]
|
|
13
|
+
_MarqueeDragMode = Literal["new", "nw", "n", "ne", "e", "se", "s", "sw", "w"]
|
|
14
|
+
_MarqueeAction = Callable[[], bool]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class _MarqueeDragState(TypedDict):
|
|
18
|
+
anchor: QtCore.QPointF
|
|
19
|
+
handle_offset: QtCore.QPointF
|
|
20
|
+
mode: _MarqueeDragMode
|
|
21
|
+
rect: QtCore.QRectF | None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
class _PlotMarqueeBase(qtw.QMainWindow):
|
|
26
|
+
_marquee_drag_state: _MarqueeDragState | None
|
|
27
|
+
_marquee_stats_dialog: qtw.QDialog
|
|
28
|
+
marquee: QtCore.QRectF | None
|
|
29
|
+
marquee_handles: Any
|
|
30
|
+
marquee_highlight: qtw.QGraphicsRectItem
|
|
31
|
+
marquee_outline: qtw.QGraphicsRectItem
|
|
32
|
+
plot: Any
|
|
33
|
+
vb: Any
|
|
34
|
+
|
|
35
|
+
def formatNum(self, value: float) -> str: ...
|
|
36
|
+
def _view_range_changed_programmatically(self) -> None: ...
|
|
37
|
+
else:
|
|
38
|
+
class _PlotMarqueeBase:
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class PlotMarqueeMixin(_PlotMarqueeBase):
|
|
43
|
+
"""
|
|
44
|
+
Shared marquee selection behavior for plot windows.
|
|
45
|
+
|
|
46
|
+
Plot type subclasses can customise selection snapping, statistics, and
|
|
47
|
+
context-menu entries by overriding the small hook methods near the end of
|
|
48
|
+
this mixin.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def _init_marquee(self) -> None:
|
|
52
|
+
"""
|
|
53
|
+
Create the reusable marquee graphics shown after Alt-dragging.
|
|
54
|
+
|
|
55
|
+
"""
|
|
56
|
+
self.marquee = None
|
|
57
|
+
self._marquee_drag_state = None
|
|
58
|
+
|
|
59
|
+
self.marquee_highlight = qtw.QGraphicsRectItem()
|
|
60
|
+
highlight_pen = QtGui.QPen(QtGui.QColor(255, 255, 255, 135))
|
|
61
|
+
highlight_pen.setWidthF(3)
|
|
62
|
+
highlight_pen.setCosmetic(True)
|
|
63
|
+
self.marquee_highlight.setPen(highlight_pen)
|
|
64
|
+
self.marquee_highlight.setBrush(QtGui.QBrush(QtCore.Qt.BrushStyle.NoBrush))
|
|
65
|
+
self.marquee_highlight.setZValue(18)
|
|
66
|
+
self.marquee_highlight.hide()
|
|
67
|
+
self.marquee_highlight.setAcceptedMouseButtons(QtCore.Qt.MouseButton.NoButton)
|
|
68
|
+
self._add_plot_overlay(self.marquee_highlight)
|
|
69
|
+
|
|
70
|
+
self.marquee_outline = qtw.QGraphicsRectItem()
|
|
71
|
+
pen = QtGui.QPen(QtGui.QColor(65, 65, 65, 220))
|
|
72
|
+
pen.setWidthF(1.2)
|
|
73
|
+
pen.setCosmetic(True)
|
|
74
|
+
pen.setStyle(QtCore.Qt.PenStyle.DashLine)
|
|
75
|
+
self.marquee_outline.setPen(pen)
|
|
76
|
+
self.marquee_outline.setBrush(QtGui.QBrush(QtGui.QColor(40, 40, 40, 24)))
|
|
77
|
+
self.marquee_outline.setZValue(19)
|
|
78
|
+
self.marquee_outline.hide()
|
|
79
|
+
self.marquee_outline.setAcceptedMouseButtons(QtCore.Qt.MouseButton.NoButton)
|
|
80
|
+
self._add_plot_overlay(self.marquee_outline)
|
|
81
|
+
|
|
82
|
+
self.marquee_handles = pg.ScatterPlotItem(
|
|
83
|
+
symbol="s",
|
|
84
|
+
size=6,
|
|
85
|
+
pen=pg.mkPen((20, 20, 20, 230), width=1, cosmetic=True),
|
|
86
|
+
brush=pg.mkBrush(245, 245, 245, 230),
|
|
87
|
+
)
|
|
88
|
+
self.marquee_handles.setZValue(20)
|
|
89
|
+
self.marquee_handles.hide()
|
|
90
|
+
self.marquee_handles.setAcceptedMouseButtons(QtCore.Qt.MouseButton.NoButton)
|
|
91
|
+
self._add_plot_overlay(self.marquee_handles)
|
|
92
|
+
|
|
93
|
+
def _add_plot_overlay(self, item: Any, viewbox: Any = None) -> None:
|
|
94
|
+
"""Attach decorations without registering plot data or affecting bounds."""
|
|
95
|
+
|
|
96
|
+
viewbox = viewbox if viewbox is not None else self._marquee_viewbox()
|
|
97
|
+
item._qplot_ui_overlay = True
|
|
98
|
+
viewbox.addItem(item, ignoreBounds=True)
|
|
99
|
+
self.__dict__.setdefault("_plot_overlay_viewboxes", {})[id(item)] = viewbox
|
|
100
|
+
|
|
101
|
+
def _marquee_viewbox(self) -> Any:
|
|
102
|
+
"""Return the coordinate owner for marquee graphics and navigation."""
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
heatmap_viewbox = getattr(self, "_primary_heatmap_viewbox", None)
|
|
106
|
+
except RuntimeError:
|
|
107
|
+
heatmap_viewbox = None
|
|
108
|
+
if callable(heatmap_viewbox):
|
|
109
|
+
viewbox = heatmap_viewbox()
|
|
110
|
+
if viewbox is not None:
|
|
111
|
+
return viewbox
|
|
112
|
+
return self.__dict__.get("vb") or self.plot.vb
|
|
113
|
+
|
|
114
|
+
def _marquee_zoom_viewbox(self, axis: Literal["x", "y"]) -> Any:
|
|
115
|
+
"""Return the range owner for one dimension of the selection."""
|
|
116
|
+
|
|
117
|
+
return self._marquee_viewbox()
|
|
118
|
+
|
|
119
|
+
def _marquee_axis_assignment_changed(self) -> None:
|
|
120
|
+
"""Discard old coordinates and move decorations to their new owner."""
|
|
121
|
+
|
|
122
|
+
self.finish_marquee_drag()
|
|
123
|
+
if "marquee_outline" not in self.__dict__:
|
|
124
|
+
return
|
|
125
|
+
self.clear_marquee()
|
|
126
|
+
target = self._marquee_viewbox()
|
|
127
|
+
overlays = self.__dict__.get("_plot_overlay_viewboxes", {})
|
|
128
|
+
for item in (self.marquee_highlight, self.marquee_outline, self.marquee_handles):
|
|
129
|
+
current = overlays.get(id(item))
|
|
130
|
+
if current is not target:
|
|
131
|
+
if current is not None:
|
|
132
|
+
current.removeItem(item)
|
|
133
|
+
self._add_plot_overlay(item, target)
|
|
134
|
+
|
|
135
|
+
def is_marquee_dragging(self) -> bool:
|
|
136
|
+
return self._marquee_drag_state is not None
|
|
137
|
+
|
|
138
|
+
def current_marquee_drag_mode(self) -> _MarqueeDragMode | None:
|
|
139
|
+
if self._marquee_drag_state is None:
|
|
140
|
+
return None
|
|
141
|
+
|
|
142
|
+
return self._marquee_drag_state["mode"]
|
|
143
|
+
|
|
144
|
+
def begin_marquee_drag(
|
|
145
|
+
self,
|
|
146
|
+
start: QtCore.QPointF,
|
|
147
|
+
mode: _MarqueeDragMode | None = None,
|
|
148
|
+
) -> None:
|
|
149
|
+
"""
|
|
150
|
+
Start creating or resizing a marquee in plot coordinates.
|
|
151
|
+
|
|
152
|
+
"""
|
|
153
|
+
start = QtCore.QPointF(start)
|
|
154
|
+
if mode is None:
|
|
155
|
+
mode = "new"
|
|
156
|
+
|
|
157
|
+
handle_offset = QtCore.QPointF()
|
|
158
|
+
if mode != "new" and self.marquee is not None:
|
|
159
|
+
handle_point = self._marquee_handle_points_for_rect(self.marquee)[mode]
|
|
160
|
+
handle_offset = start - handle_point
|
|
161
|
+
|
|
162
|
+
self._marquee_drag_state = {
|
|
163
|
+
"anchor": QtCore.QPointF(start),
|
|
164
|
+
"handle_offset": QtCore.QPointF(handle_offset),
|
|
165
|
+
"mode": mode,
|
|
166
|
+
"rect": QtCore.QRectF(self.marquee) if self.marquee is not None else None,
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if mode == "new":
|
|
170
|
+
self.set_marquee_rect(QtCore.QRectF(start, start))
|
|
171
|
+
|
|
172
|
+
def drag_marquee_to(
|
|
173
|
+
self,
|
|
174
|
+
point: QtCore.QPointF,
|
|
175
|
+
modifiers: QtCore.Qt.KeyboardModifier = (
|
|
176
|
+
QtCore.Qt.KeyboardModifier.NoModifier
|
|
177
|
+
),
|
|
178
|
+
) -> None:
|
|
179
|
+
"""
|
|
180
|
+
Update the marquee during an active drag.
|
|
181
|
+
|
|
182
|
+
"""
|
|
183
|
+
point = QtCore.QPointF(point)
|
|
184
|
+
state = self._marquee_drag_state
|
|
185
|
+
if state is None:
|
|
186
|
+
return
|
|
187
|
+
|
|
188
|
+
if state["mode"] == "new" or state["rect"] is None:
|
|
189
|
+
rect = QtCore.QRectF(state["anchor"], point)
|
|
190
|
+
else:
|
|
191
|
+
rect = QtCore.QRectF(state["rect"])
|
|
192
|
+
offset = state.get("handle_offset", QtCore.QPointF())
|
|
193
|
+
point = QtCore.QPointF(
|
|
194
|
+
point.x() - offset.x(),
|
|
195
|
+
point.y() - offset.y(),
|
|
196
|
+
)
|
|
197
|
+
self._resize_marquee_rect(rect, state["mode"], point, modifiers)
|
|
198
|
+
|
|
199
|
+
self.set_marquee_rect(rect)
|
|
200
|
+
|
|
201
|
+
def finish_marquee_drag(self) -> None:
|
|
202
|
+
self._marquee_drag_state = None
|
|
203
|
+
|
|
204
|
+
def marquee_contains_scene_pos(self, scene_pos: QtCore.QPointF) -> bool:
|
|
205
|
+
"""
|
|
206
|
+
Return whether a scene position is inside the current marquee.
|
|
207
|
+
|
|
208
|
+
"""
|
|
209
|
+
if self.marquee is None:
|
|
210
|
+
return False
|
|
211
|
+
|
|
212
|
+
point = self._marquee_viewbox().mapSceneToView(scene_pos)
|
|
213
|
+
return self.marquee.normalized().contains(point)
|
|
214
|
+
|
|
215
|
+
def open_marquee_context_menu(
|
|
216
|
+
self,
|
|
217
|
+
scene_pos: QtCore.QPointF,
|
|
218
|
+
global_pos: QtCore.QPoint | QtCore.QPointF | None = None,
|
|
219
|
+
) -> bool:
|
|
220
|
+
"""
|
|
221
|
+
Open the marquee context menu when a right-click lands inside it.
|
|
222
|
+
|
|
223
|
+
"""
|
|
224
|
+
if not self.marquee_contains_scene_pos(scene_pos):
|
|
225
|
+
return False
|
|
226
|
+
|
|
227
|
+
menu = self._new_marquee_context_menu()
|
|
228
|
+
if global_pos is None:
|
|
229
|
+
global_pos = QtGui.QCursor.pos()
|
|
230
|
+
elif isinstance(global_pos, QtCore.QPointF):
|
|
231
|
+
global_pos = global_pos.toPoint()
|
|
232
|
+
|
|
233
|
+
menu.exec(global_pos)
|
|
234
|
+
return True
|
|
235
|
+
|
|
236
|
+
def _new_marquee_context_menu(self) -> qtw.QMenu:
|
|
237
|
+
menu = qtw.QMenu()
|
|
238
|
+
if hasattr(menu, "setToolTipsVisible"):
|
|
239
|
+
menu.setToolTipsVisible(True)
|
|
240
|
+
|
|
241
|
+
self._add_marquee_context_action(
|
|
242
|
+
menu,
|
|
243
|
+
"Zoom",
|
|
244
|
+
lambda: self.zoom_marquee("xy"),
|
|
245
|
+
)
|
|
246
|
+
self._add_marquee_context_action(
|
|
247
|
+
menu,
|
|
248
|
+
"Zoom X",
|
|
249
|
+
lambda: self.zoom_marquee("x"),
|
|
250
|
+
)
|
|
251
|
+
self._add_marquee_context_action(
|
|
252
|
+
menu,
|
|
253
|
+
"Zoom Y",
|
|
254
|
+
lambda: self.zoom_marquee("y"),
|
|
255
|
+
)
|
|
256
|
+
self._add_marquee_color_context_action(menu)
|
|
257
|
+
self._add_marquee_stats_context_action(menu)
|
|
258
|
+
return menu
|
|
259
|
+
|
|
260
|
+
def _add_marquee_context_action(
|
|
261
|
+
self,
|
|
262
|
+
menu: qtw.QMenu,
|
|
263
|
+
text: str,
|
|
264
|
+
callback: _MarqueeAction,
|
|
265
|
+
) -> QtGui.QAction:
|
|
266
|
+
action = menu.addAction(text)
|
|
267
|
+
if action is None:
|
|
268
|
+
raise RuntimeError("Marquee context action could not be created.")
|
|
269
|
+
action.triggered.connect(
|
|
270
|
+
lambda _checked=False, callback=callback: self._execute_marquee_action(
|
|
271
|
+
callback
|
|
272
|
+
)
|
|
273
|
+
)
|
|
274
|
+
return action
|
|
275
|
+
|
|
276
|
+
def _add_marquee_color_context_action(
|
|
277
|
+
self,
|
|
278
|
+
menu: qtw.QMenu,
|
|
279
|
+
) -> QtGui.QAction | None:
|
|
280
|
+
return None
|
|
281
|
+
|
|
282
|
+
def _add_marquee_stats_context_action(self, menu: qtw.QMenu) -> QtGui.QAction:
|
|
283
|
+
stats_text = self._marquee_stats_text()
|
|
284
|
+
|
|
285
|
+
def open_stats_dialog(stats_text: str | None = stats_text) -> bool:
|
|
286
|
+
return self.show_marquee_stats_dialog(stats_text)
|
|
287
|
+
|
|
288
|
+
action = self._add_marquee_context_action(
|
|
289
|
+
menu,
|
|
290
|
+
"Stats...",
|
|
291
|
+
open_stats_dialog,
|
|
292
|
+
)
|
|
293
|
+
if stats_text is None:
|
|
294
|
+
action.setEnabled(False)
|
|
295
|
+
action.setToolTip("No data points inside the marquee.")
|
|
296
|
+
else:
|
|
297
|
+
action.setToolTip("Show statistics for the marquee selection.")
|
|
298
|
+
action.setStatusTip("Show statistics for the marquee selection.")
|
|
299
|
+
return action
|
|
300
|
+
|
|
301
|
+
def _execute_marquee_action(self, callback: _MarqueeAction) -> None:
|
|
302
|
+
if callback():
|
|
303
|
+
self.clear_marquee()
|
|
304
|
+
|
|
305
|
+
def zoom_marquee(self, axes: str) -> bool:
|
|
306
|
+
rect = self.marquee.normalized() if self.marquee is not None else None
|
|
307
|
+
if rect is None:
|
|
308
|
+
return False
|
|
309
|
+
|
|
310
|
+
if "x" in axes:
|
|
311
|
+
self._marquee_zoom_viewbox("x").setXRange(
|
|
312
|
+
rect.left(),
|
|
313
|
+
rect.right(),
|
|
314
|
+
padding=0,
|
|
315
|
+
)
|
|
316
|
+
if "y" in axes:
|
|
317
|
+
self._marquee_zoom_viewbox("y").setYRange(
|
|
318
|
+
rect.top(),
|
|
319
|
+
rect.bottom(),
|
|
320
|
+
padding=0,
|
|
321
|
+
)
|
|
322
|
+
self._view_range_changed_programmatically()
|
|
323
|
+
return True
|
|
324
|
+
|
|
325
|
+
def _marquee_stats_text(self) -> str | None:
|
|
326
|
+
return None
|
|
327
|
+
|
|
328
|
+
def show_marquee_stats_dialog(self, stats_text: str | None = None) -> bool:
|
|
329
|
+
if stats_text is None:
|
|
330
|
+
stats_text = self._marquee_stats_text()
|
|
331
|
+
if stats_text is None:
|
|
332
|
+
return False
|
|
333
|
+
|
|
334
|
+
dialog = self._new_marquee_stats_dialog(stats_text)
|
|
335
|
+
self._marquee_stats_dialog = dialog
|
|
336
|
+
dialog.show()
|
|
337
|
+
dialog.raise_()
|
|
338
|
+
dialog.activateWindow()
|
|
339
|
+
return True
|
|
340
|
+
|
|
341
|
+
def _new_marquee_stats_dialog(self, stats_text: str) -> qtw.QDialog:
|
|
342
|
+
dialog = qtw.QDialog(self)
|
|
343
|
+
dialog.setWindowTitle("Marquee stats")
|
|
344
|
+
|
|
345
|
+
layout = qtw.QVBoxLayout(dialog)
|
|
346
|
+
stats_table = self._new_marquee_stats_table(stats_text)
|
|
347
|
+
layout.addWidget(stats_table)
|
|
348
|
+
|
|
349
|
+
buttons = qtw.QDialogButtonBox(qtw.QDialogButtonBox.StandardButton.Close)
|
|
350
|
+
copy_button = buttons.addButton("Copy", qtw.QDialogButtonBox.ButtonRole.ActionRole)
|
|
351
|
+
if copy_button is None:
|
|
352
|
+
raise RuntimeError("Marquee stats copy button is not available.")
|
|
353
|
+
|
|
354
|
+
def copy_stats(_checked: bool = False) -> None:
|
|
355
|
+
self.copy_marquee_stats_to_clipboard(stats_text)
|
|
356
|
+
|
|
357
|
+
copy_button.clicked.connect(copy_stats)
|
|
358
|
+
buttons.rejected.connect(dialog.close)
|
|
359
|
+
layout.addWidget(buttons)
|
|
360
|
+
|
|
361
|
+
return dialog
|
|
362
|
+
|
|
363
|
+
def _new_marquee_stats_table(self, stats_text: str) -> CopyableTableWidget:
|
|
364
|
+
rows = self._marquee_stats_table_rows(stats_text)
|
|
365
|
+
stats_table = CopyableTableWidget()
|
|
366
|
+
stats_table.setObjectName("detailsTable")
|
|
367
|
+
stats_table.setColumnCount(2)
|
|
368
|
+
stats_table.setHorizontalHeaderLabels(["Field", "Value"])
|
|
369
|
+
stats_table.setRowCount(len(rows))
|
|
370
|
+
stats_table.setAlternatingRowColors(True)
|
|
371
|
+
stats_table.setEditTriggers(qtw.QAbstractItemView.EditTrigger.NoEditTriggers)
|
|
372
|
+
stats_table.setSelectionBehavior(qtw.QAbstractItemView.SelectionBehavior.SelectRows)
|
|
373
|
+
stats_table.setSelectionMode(qtw.QAbstractItemView.SelectionMode.ExtendedSelection)
|
|
374
|
+
stats_table.setTextElideMode(QtCore.Qt.TextElideMode.ElideNone)
|
|
375
|
+
stats_table.setWordWrap(False)
|
|
376
|
+
vertical_header = stats_table.verticalHeader()
|
|
377
|
+
horizontal_header = stats_table.horizontalHeader()
|
|
378
|
+
if vertical_header is None or horizontal_header is None:
|
|
379
|
+
raise RuntimeError("Marquee stats table headers are not available.")
|
|
380
|
+
|
|
381
|
+
vertical_header.hide()
|
|
382
|
+
vertical_header.setMinimumSectionSize(16)
|
|
383
|
+
vertical_header.setDefaultSectionSize(20)
|
|
384
|
+
horizontal_header.setFixedHeight(22)
|
|
385
|
+
horizontal_header.setStretchLastSection(True)
|
|
386
|
+
horizontal_header.setSectionResizeMode(
|
|
387
|
+
0,
|
|
388
|
+
qtw.QHeaderView.ResizeMode.ResizeToContents
|
|
389
|
+
)
|
|
390
|
+
horizontal_header.setSectionResizeMode(
|
|
391
|
+
1,
|
|
392
|
+
qtw.QHeaderView.ResizeMode.Stretch
|
|
393
|
+
)
|
|
394
|
+
stats_table.setMinimumWidth(280)
|
|
395
|
+
stats_table.setMinimumHeight(170)
|
|
396
|
+
|
|
397
|
+
for row, (field, value) in enumerate(rows):
|
|
398
|
+
field_item = qtw.QTableWidgetItem(field)
|
|
399
|
+
value_item = qtw.QTableWidgetItem(value)
|
|
400
|
+
stats_table.setItem(row, 0, field_item)
|
|
401
|
+
stats_table.setItem(row, 1, value_item)
|
|
402
|
+
|
|
403
|
+
return stats_table
|
|
404
|
+
|
|
405
|
+
def _marquee_stats_table_rows(self, stats_text: str) -> list[tuple[str, str]]:
|
|
406
|
+
rows: list[tuple[str, str]] = []
|
|
407
|
+
for line in stats_text.splitlines():
|
|
408
|
+
line = line.strip()
|
|
409
|
+
if not line:
|
|
410
|
+
continue
|
|
411
|
+
|
|
412
|
+
if ":" in line:
|
|
413
|
+
field, value = line.split(":", 1)
|
|
414
|
+
rows.append((field.strip(), value.strip()))
|
|
415
|
+
else:
|
|
416
|
+
rows.append(("Selection", line))
|
|
417
|
+
|
|
418
|
+
return rows
|
|
419
|
+
|
|
420
|
+
def _format_marquee_stats_text(
|
|
421
|
+
self,
|
|
422
|
+
size_text: str,
|
|
423
|
+
values: Any,
|
|
424
|
+
rect: QtCore.QRectF | None = None,
|
|
425
|
+
*,
|
|
426
|
+
x_axis: str = "x",
|
|
427
|
+
y_axis: str = "y",
|
|
428
|
+
) -> str:
|
|
429
|
+
marquee = self.marquee
|
|
430
|
+
if rect is None and marquee is not None:
|
|
431
|
+
rect = marquee.normalized()
|
|
432
|
+
|
|
433
|
+
lines = [size_text]
|
|
434
|
+
if rect is not None:
|
|
435
|
+
view_to_data = getattr(self, "view_to_data", None)
|
|
436
|
+
if callable(view_to_data):
|
|
437
|
+
x_low = view_to_data(x_axis, rect.left())
|
|
438
|
+
x_high = view_to_data(x_axis, rect.right())
|
|
439
|
+
y_low = view_to_data(y_axis, rect.top())
|
|
440
|
+
y_high = view_to_data(y_axis, rect.bottom())
|
|
441
|
+
else:
|
|
442
|
+
x_low, x_high = rect.left(), rect.right()
|
|
443
|
+
y_low, y_high = rect.top(), rect.bottom()
|
|
444
|
+
lines.extend((
|
|
445
|
+
f"X range: {self.formatNum(x_low)} to {self.formatNum(x_high)}",
|
|
446
|
+
f"Y range: {self.formatNum(y_low)} to {self.formatNum(y_high)}",
|
|
447
|
+
))
|
|
448
|
+
|
|
449
|
+
lines.extend((
|
|
450
|
+
f"Average: {self.formatNum(finite_mean(values))}",
|
|
451
|
+
f"Standard deviation: {self.formatNum(finite_standard_deviation(values))}",
|
|
452
|
+
f"Max: {self.formatNum(float(values.max()))}",
|
|
453
|
+
f"Min: {self.formatNum(float(values.min()))}",
|
|
454
|
+
))
|
|
455
|
+
|
|
456
|
+
return "\n".join(lines)
|
|
457
|
+
|
|
458
|
+
def copy_marquee_stats_to_clipboard(self, stats_text: str | None = None) -> bool:
|
|
459
|
+
if stats_text is None:
|
|
460
|
+
stats_text = self._marquee_stats_text()
|
|
461
|
+
if stats_text is None:
|
|
462
|
+
return False
|
|
463
|
+
|
|
464
|
+
clipboard = qtw.QApplication.clipboard()
|
|
465
|
+
if clipboard is None:
|
|
466
|
+
return False
|
|
467
|
+
|
|
468
|
+
clipboard.setText(stats_text)
|
|
469
|
+
return True
|
|
470
|
+
|
|
471
|
+
def _resize_marquee_rect(
|
|
472
|
+
self,
|
|
473
|
+
rect: QtCore.QRectF,
|
|
474
|
+
handle: _MarqueeHandle,
|
|
475
|
+
point: QtCore.QPointF,
|
|
476
|
+
modifiers: QtCore.Qt.KeyboardModifier = (
|
|
477
|
+
QtCore.Qt.KeyboardModifier.NoModifier
|
|
478
|
+
),
|
|
479
|
+
) -> None:
|
|
480
|
+
symmetric = bool(modifiers & QtCore.Qt.KeyboardModifier.AltModifier)
|
|
481
|
+
asymmetric = bool(modifiers & QtCore.Qt.KeyboardModifier.ShiftModifier) and not symmetric
|
|
482
|
+
original = QtCore.QRectF(rect)
|
|
483
|
+
anchor = self._marquee_handle_points_for_rect(original)[handle]
|
|
484
|
+
|
|
485
|
+
if "w" in handle:
|
|
486
|
+
rect.setLeft(point.x())
|
|
487
|
+
if "e" in handle:
|
|
488
|
+
rect.setRight(point.x())
|
|
489
|
+
if "s" in handle:
|
|
490
|
+
rect.setTop(point.y())
|
|
491
|
+
if "n" in handle:
|
|
492
|
+
rect.setBottom(point.y())
|
|
493
|
+
|
|
494
|
+
dx = point.x() - anchor.x()
|
|
495
|
+
dy = point.y() - anchor.y()
|
|
496
|
+
|
|
497
|
+
if ("w" in handle or "e" in handle) and (symmetric or asymmetric):
|
|
498
|
+
offset = -dx if symmetric else dx
|
|
499
|
+
if "w" in handle:
|
|
500
|
+
rect.setRight(original.right() + offset)
|
|
501
|
+
else:
|
|
502
|
+
rect.setLeft(original.left() + offset)
|
|
503
|
+
|
|
504
|
+
if ("n" in handle or "s" in handle) and (symmetric or asymmetric):
|
|
505
|
+
offset = -dy if symmetric else dy
|
|
506
|
+
if "s" in handle:
|
|
507
|
+
rect.setBottom(original.bottom() + offset)
|
|
508
|
+
else:
|
|
509
|
+
rect.setTop(original.top() + offset)
|
|
510
|
+
|
|
511
|
+
if asymmetric:
|
|
512
|
+
self._snap_translated_marquee_rect(rect, original, handle)
|
|
513
|
+
|
|
514
|
+
def _snap_translated_marquee_rect(
|
|
515
|
+
self,
|
|
516
|
+
rect: QtCore.QRectF,
|
|
517
|
+
original: QtCore.QRectF,
|
|
518
|
+
handle: _MarqueeHandle,
|
|
519
|
+
) -> None:
|
|
520
|
+
snapped = self._snap_marquee_rect(QtCore.QRectF(rect).normalized())
|
|
521
|
+
if snapped is None:
|
|
522
|
+
return
|
|
523
|
+
|
|
524
|
+
adjusted = QtCore.QRectF(snapped)
|
|
525
|
+
if "w" in handle or "e" in handle:
|
|
526
|
+
width = original.width()
|
|
527
|
+
if "w" in handle:
|
|
528
|
+
adjusted.setLeft(snapped.left())
|
|
529
|
+
adjusted.setRight(snapped.left() + width)
|
|
530
|
+
else:
|
|
531
|
+
adjusted.setRight(snapped.right())
|
|
532
|
+
adjusted.setLeft(snapped.right() - width)
|
|
533
|
+
|
|
534
|
+
if "n" in handle or "s" in handle:
|
|
535
|
+
height = original.height()
|
|
536
|
+
if "s" in handle:
|
|
537
|
+
adjusted.setTop(snapped.top())
|
|
538
|
+
adjusted.setBottom(snapped.top() + height)
|
|
539
|
+
else:
|
|
540
|
+
adjusted.setBottom(snapped.bottom())
|
|
541
|
+
adjusted.setTop(snapped.bottom() - height)
|
|
542
|
+
|
|
543
|
+
rect.setRect(adjusted.left(), adjusted.top(), adjusted.width(), adjusted.height())
|
|
544
|
+
|
|
545
|
+
def set_marquee_rect(self, rect: QtCore.QRectF) -> None:
|
|
546
|
+
"""
|
|
547
|
+
Snap, store, and draw the marquee rectangle.
|
|
548
|
+
|
|
549
|
+
"""
|
|
550
|
+
snapped = self._snap_marquee_rect(rect.normalized())
|
|
551
|
+
if snapped is None or snapped.width() <= 0 or snapped.height() <= 0:
|
|
552
|
+
self.clear_marquee()
|
|
553
|
+
return
|
|
554
|
+
|
|
555
|
+
rect = snapped
|
|
556
|
+
self.marquee = QtCore.QRectF(rect)
|
|
557
|
+
self.marquee_highlight.setRect(rect)
|
|
558
|
+
self.marquee_outline.setRect(rect)
|
|
559
|
+
self.marquee_highlight.show()
|
|
560
|
+
self.marquee_outline.show()
|
|
561
|
+
self._update_marquee_handles()
|
|
562
|
+
|
|
563
|
+
def clear_marquee(self) -> None:
|
|
564
|
+
self.marquee = None
|
|
565
|
+
self.marquee_highlight.hide()
|
|
566
|
+
self.marquee_outline.hide()
|
|
567
|
+
self.marquee_handles.hide()
|
|
568
|
+
|
|
569
|
+
def _snap_marquee_rect(self, rect: QtCore.QRectF) -> QtCore.QRectF | None:
|
|
570
|
+
return rect
|
|
571
|
+
|
|
572
|
+
def marquee_drag_mode_at(self, scene_pos: QtCore.QPointF) -> _MarqueeHandle | None:
|
|
573
|
+
"""
|
|
574
|
+
Return the resize handle under a scene position, if there is one.
|
|
575
|
+
|
|
576
|
+
"""
|
|
577
|
+
if self.marquee is None or not self.marquee_handles.isVisible():
|
|
578
|
+
return None
|
|
579
|
+
|
|
580
|
+
threshold = 8
|
|
581
|
+
for handle, point in self._marquee_handle_points().items():
|
|
582
|
+
handle_scene_pos = self._marquee_viewbox().mapViewToScene(point)
|
|
583
|
+
distance = (
|
|
584
|
+
(handle_scene_pos.x() - scene_pos.x()) ** 2
|
|
585
|
+
+ (handle_scene_pos.y() - scene_pos.y()) ** 2
|
|
586
|
+
)
|
|
587
|
+
if distance <= threshold ** 2:
|
|
588
|
+
return handle
|
|
589
|
+
|
|
590
|
+
return None
|
|
591
|
+
|
|
592
|
+
def marquee_cursor_shape_at(
|
|
593
|
+
self,
|
|
594
|
+
scene_pos: QtCore.QPointF,
|
|
595
|
+
modifiers: QtCore.Qt.KeyboardModifier = (
|
|
596
|
+
QtCore.Qt.KeyboardModifier.NoModifier
|
|
597
|
+
),
|
|
598
|
+
) -> QtCore.Qt.CursorShape | None:
|
|
599
|
+
mode = self.current_marquee_drag_mode()
|
|
600
|
+
if mode == "new":
|
|
601
|
+
return QtCore.Qt.CursorShape.CrossCursor
|
|
602
|
+
if mode is not None:
|
|
603
|
+
return self._marquee_cursor_shape_for_handle(mode)
|
|
604
|
+
|
|
605
|
+
handle = self.marquee_drag_mode_at(scene_pos)
|
|
606
|
+
if handle is not None:
|
|
607
|
+
return self._marquee_cursor_shape_for_handle(handle)
|
|
608
|
+
if modifiers & QtCore.Qt.KeyboardModifier.AltModifier:
|
|
609
|
+
return QtCore.Qt.CursorShape.CrossCursor
|
|
610
|
+
|
|
611
|
+
return None
|
|
612
|
+
|
|
613
|
+
def _marquee_cursor_shape_for_handle(
|
|
614
|
+
self,
|
|
615
|
+
handle: _MarqueeDragMode,
|
|
616
|
+
) -> QtCore.Qt.CursorShape:
|
|
617
|
+
if handle in ("e", "w"):
|
|
618
|
+
return QtCore.Qt.CursorShape.SizeHorCursor
|
|
619
|
+
if handle in ("n", "s"):
|
|
620
|
+
return QtCore.Qt.CursorShape.SizeVerCursor
|
|
621
|
+
if handle in ("nw", "se"):
|
|
622
|
+
return QtCore.Qt.CursorShape.SizeFDiagCursor
|
|
623
|
+
if handle in ("ne", "sw"):
|
|
624
|
+
return QtCore.Qt.CursorShape.SizeBDiagCursor
|
|
625
|
+
|
|
626
|
+
return QtCore.Qt.CursorShape.CrossCursor
|
|
627
|
+
|
|
628
|
+
def _update_marquee_handles(self) -> None:
|
|
629
|
+
points = list(self._marquee_handle_points().values())
|
|
630
|
+
self.marquee_handles.setData(
|
|
631
|
+
[point.x() for point in points],
|
|
632
|
+
[point.y() for point in points],
|
|
633
|
+
)
|
|
634
|
+
self.marquee_handles.show()
|
|
635
|
+
|
|
636
|
+
def _marquee_handle_points(self) -> dict[_MarqueeHandle, QtCore.QPointF]:
|
|
637
|
+
if self.marquee is None:
|
|
638
|
+
return {}
|
|
639
|
+
return self._marquee_handle_points_for_rect(self.marquee)
|
|
640
|
+
|
|
641
|
+
def _marquee_handle_points_for_rect(
|
|
642
|
+
self,
|
|
643
|
+
rect: QtCore.QRectF,
|
|
644
|
+
) -> dict[_MarqueeHandle, QtCore.QPointF]:
|
|
645
|
+
centre_x = rect.center().x()
|
|
646
|
+
centre_y = rect.center().y()
|
|
647
|
+
return {
|
|
648
|
+
"nw": QtCore.QPointF(rect.left(), rect.bottom()),
|
|
649
|
+
"n": QtCore.QPointF(centre_x, rect.bottom()),
|
|
650
|
+
"ne": QtCore.QPointF(rect.right(), rect.bottom()),
|
|
651
|
+
"e": QtCore.QPointF(rect.right(), centre_y),
|
|
652
|
+
"se": QtCore.QPointF(rect.right(), rect.top()),
|
|
653
|
+
"s": QtCore.QPointF(centre_x, rect.top()),
|
|
654
|
+
"sw": QtCore.QPointF(rect.left(), rect.top()),
|
|
655
|
+
"w": QtCore.QPointF(rect.left(), centre_y),
|
|
656
|
+
}
|