qplotter 1.6.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. qplot/__init__.py +46 -0
  2. qplot/__main__.py +232 -0
  3. qplot/_auxiliary_paths.py +67 -0
  4. qplot/_build_info.json +1 -0
  5. qplot/_metadata.py +75 -0
  6. qplot/_repair.py +14 -0
  7. qplot/_shutdown_supervisor.py +4049 -0
  8. qplot/_version.py +12 -0
  9. qplot/_windows_shutdown_job.py +1166 -0
  10. qplot/configuration/__init__.py +3 -0
  11. qplot/configuration/config.py +578 -0
  12. qplot/configuration/config_schema.json +357 -0
  13. qplot/configuration/fonts.py +3 -0
  14. qplot/configuration/scripts.py +255 -0
  15. qplot/configuration/themes/__init__.py +13 -0
  16. qplot/configuration/themes/_base.py +592 -0
  17. qplot/configuration/themes/blank.py +17 -0
  18. qplot/configuration/themes/dark.py +54 -0
  19. qplot/configuration/themes/light.py +54 -0
  20. qplot/datahandling/LoadFromDB.py +224 -0
  21. qplot/datahandling/__init__.py +47 -0
  22. qplot/datahandling/_trusted_live_helper.py +812 -0
  23. qplot/datahandling/_trusted_live_protocol.py +1745 -0
  24. qplot/datahandling/database.py +2340 -0
  25. qplot/datahandling/dimensions.py +43 -0
  26. qplot/datahandling/file_identity.py +461 -0
  27. qplot/datahandling/parameter_data.py +149 -0
  28. qplot/datahandling/plot_progress.py +17 -0
  29. qplot/datahandling/qcodes_cache.py +277 -0
  30. qplot/datahandling/qcodes_compat.py +14 -0
  31. qplot/datahandling/readSQL.py +2442 -0
  32. qplot/datahandling/readonly.py +2566 -0
  33. qplot/datahandling/source_activity.py +21 -0
  34. qplot/datahandling/trusted_array_preview.py +148 -0
  35. qplot/datahandling/trusted_derived_cache.py +1605 -0
  36. qplot/datahandling/trusted_derived_rendering.py +876 -0
  37. qplot/datahandling/trusted_heatmap.py +246 -0
  38. qplot/datahandling/trusted_live.py +2808 -0
  39. qplot/datahandling/trusted_live_queries.py +5907 -0
  40. qplot/datahandling/trusted_live_service.py +1919 -0
  41. qplot/datahandling/trusted_live_supervisor.py +2161 -0
  42. qplot/datahandling/trusted_plot.py +283 -0
  43. qplot/datahandling/trusted_presentation.py +1065 -0
  44. qplot/datahandling/trusted_snapshot.py +1132 -0
  45. qplot/datahandling/trusted_work_coordinator.py +2157 -0
  46. qplot/datahandling/trusted_work_scheduler.py +1204 -0
  47. qplot/diagnostics.py +241 -0
  48. qplot/resources/testdata/qplot_test_db_01_10mb.csv +11 -0
  49. qplot/resources/testdata/qplot_test_db_02_25mb.csv +14 -0
  50. qplot/resources/testdata/qplot_test_db_03_50mb.csv +17 -0
  51. qplot/resources/testdata/qplot_test_db_04_100mb.csv +20 -0
  52. qplot/resources/testdata/qplot_test_db_05_250mb.csv +23 -0
  53. qplot/resources/testdata/qplot_test_db_06_500mb.csv +26 -0
  54. qplot/resources/testdata/qplot_test_db_07_1gb.csv +29 -0
  55. qplot/resources/testdata/qplot_test_db_08_5gb.csv +32 -0
  56. qplot/resources/testdata/qplot_test_db_09_10gb.csv +35 -0
  57. qplot/resources/testdata/qplot_test_db_10_30gb.csv +38 -0
  58. qplot/testdata.py +1804 -0
  59. qplot/tools/__init__.py +13 -0
  60. qplot/tools/finite_means.py +123 -0
  61. qplot/tools/general.py +139 -0
  62. qplot/tools/heatmap_geometry.py +391 -0
  63. qplot/tools/operation_registry.py +179 -0
  64. qplot/tools/plot_tools.py +650 -0
  65. qplot/tools/sample_statistics.py +111 -0
  66. qplot/tools/worker.py +2838 -0
  67. qplot/windows/__init__.py +9 -0
  68. qplot/windows/_colorbar.py +422 -0
  69. qplot/windows/_commands.py +523 -0
  70. qplot/windows/_config_persistence.py +123 -0
  71. qplot/windows/_database_actions.py +4933 -0
  72. qplot/windows/_dataset_handle.py +221 -0
  73. qplot/windows/_display_precision.py +48 -0
  74. qplot/windows/_dragdrop.py +70 -0
  75. qplot/windows/_export_paths.py +1167 -0
  76. qplot/windows/_help.py +210 -0
  77. qplot/windows/_mixed_appearance.py +303 -0
  78. qplot/windows/_mixed_plots.py +461 -0
  79. qplot/windows/_native_averaging.py +323 -0
  80. qplot/windows/_native_transforms.py +551 -0
  81. qplot/windows/_plot1d_snap.py +612 -0
  82. qplot/windows/_plot1d_traces.py +2236 -0
  83. qplot/windows/_plot2d_colorbar.py +1127 -0
  84. qplot/windows/_plot2d_colorbar_dialog.py +786 -0
  85. qplot/windows/_plot2d_layers.py +2291 -0
  86. qplot/windows/_plot2d_sweeps.py +712 -0
  87. qplot/windows/_plotWin.py +2082 -0
  88. qplot/windows/_plot_actions.py +2703 -0
  89. qplot/windows/_plot_annotation.py +186 -0
  90. qplot/windows/_plot_appearance.py +283 -0
  91. qplot/windows/_plot_axis_scaling.py +1521 -0
  92. qplot/windows/_plot_data_viewbox.py +94 -0
  93. qplot/windows/_plot_drop_axes.py +224 -0
  94. qplot/windows/_plot_export.py +1445 -0
  95. qplot/windows/_plot_feedback.py +112 -0
  96. qplot/windows/_plot_marquee.py +659 -0
  97. qplot/windows/_plot_refresh.py +950 -0
  98. qplot/windows/_plot_source_promotion.py +178 -0
  99. qplot/windows/_plot_state.py +237 -0
  100. qplot/windows/_plot_status.py +115 -0
  101. qplot/windows/_plot_tiling.py +35 -0
  102. qplot/windows/_plot_transform_labels.py +60 -0
  103. qplot/windows/_preferences.py +724 -0
  104. qplot/windows/_refresh_interval.py +172 -0
  105. qplot/windows/_run_controls.py +789 -0
  106. qplot/windows/_shortcuts.py +47 -0
  107. qplot/windows/_subplots/__init__.py +8 -0
  108. qplot/windows/_subplots/subplot1d.py +532 -0
  109. qplot/windows/_subplots/subplot2d.py +919 -0
  110. qplot/windows/_svg_export.py +209 -0
  111. qplot/windows/_trusted_derived_qt.py +1759 -0
  112. qplot/windows/_widgets/__init__.py +23 -0
  113. qplot/windows/_widgets/_run_formatting.py +387 -0
  114. qplot/windows/_widgets/details_tables.py +1119 -0
  115. qplot/windows/_widgets/dropbox.py +290 -0
  116. qplot/windows/_widgets/operations.py +460 -0
  117. qplot/windows/_widgets/preview.py +2997 -0
  118. qplot/windows/_widgets/run_list_items.py +488 -0
  119. qplot/windows/_widgets/toolbar.py +111 -0
  120. qplot/windows/_widgets/treeWidgets.py +2572 -0
  121. qplot/windows/_window_controls.py +414 -0
  122. qplot/windows/main.py +1632 -0
  123. qplot/windows/plot1d.py +313 -0
  124. qplot/windows/plot2d.py +1935 -0
  125. qplotter-1.6.0.dist-info/METADATA +322 -0
  126. qplotter-1.6.0.dist-info/RECORD +130 -0
  127. qplotter-1.6.0.dist-info/WHEEL +5 -0
  128. qplotter-1.6.0.dist-info/entry_points.txt +4 -0
  129. qplotter-1.6.0.dist-info/licenses/LICENSE +21 -0
  130. qplotter-1.6.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,659 @@
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 = (
106
+ getattr(self, "_primary_heatmap_viewbox", None)
107
+ if getattr(self, "operation_kind", None) == "plot2d" else None
108
+ )
109
+ except RuntimeError:
110
+ heatmap_viewbox = None
111
+ if callable(heatmap_viewbox):
112
+ viewbox = heatmap_viewbox()
113
+ if viewbox is not None:
114
+ return viewbox
115
+ return self.__dict__.get("vb") or self.plot.vb
116
+
117
+ def _marquee_zoom_viewbox(self, axis: Literal["x", "y"]) -> Any:
118
+ """Return the range owner for one dimension of the selection."""
119
+
120
+ return self._marquee_viewbox()
121
+
122
+ def _marquee_axis_assignment_changed(self) -> None:
123
+ """Discard old coordinates and move decorations to their new owner."""
124
+
125
+ self.finish_marquee_drag()
126
+ if "marquee_outline" not in self.__dict__:
127
+ return
128
+ self.clear_marquee()
129
+ target = self._marquee_viewbox()
130
+ overlays = self.__dict__.get("_plot_overlay_viewboxes", {})
131
+ for item in (self.marquee_highlight, self.marquee_outline, self.marquee_handles):
132
+ current = overlays.get(id(item))
133
+ if current is not target:
134
+ if current is not None:
135
+ current.removeItem(item)
136
+ self._add_plot_overlay(item, target)
137
+
138
+ def is_marquee_dragging(self) -> bool:
139
+ return self._marquee_drag_state is not None
140
+
141
+ def current_marquee_drag_mode(self) -> _MarqueeDragMode | None:
142
+ if self._marquee_drag_state is None:
143
+ return None
144
+
145
+ return self._marquee_drag_state["mode"]
146
+
147
+ def begin_marquee_drag(
148
+ self,
149
+ start: QtCore.QPointF,
150
+ mode: _MarqueeDragMode | None = None,
151
+ ) -> None:
152
+ """
153
+ Start creating or resizing a marquee in plot coordinates.
154
+
155
+ """
156
+ start = QtCore.QPointF(start)
157
+ if mode is None:
158
+ mode = "new"
159
+
160
+ handle_offset = QtCore.QPointF()
161
+ if mode != "new" and self.marquee is not None:
162
+ handle_point = self._marquee_handle_points_for_rect(self.marquee)[mode]
163
+ handle_offset = start - handle_point
164
+
165
+ self._marquee_drag_state = {
166
+ "anchor": QtCore.QPointF(start),
167
+ "handle_offset": QtCore.QPointF(handle_offset),
168
+ "mode": mode,
169
+ "rect": QtCore.QRectF(self.marquee) if self.marquee is not None else None,
170
+ }
171
+
172
+ if mode == "new":
173
+ self.set_marquee_rect(QtCore.QRectF(start, start))
174
+
175
+ def drag_marquee_to(
176
+ self,
177
+ point: QtCore.QPointF,
178
+ modifiers: QtCore.Qt.KeyboardModifier = (
179
+ QtCore.Qt.KeyboardModifier.NoModifier
180
+ ),
181
+ ) -> None:
182
+ """
183
+ Update the marquee during an active drag.
184
+
185
+ """
186
+ point = QtCore.QPointF(point)
187
+ state = self._marquee_drag_state
188
+ if state is None:
189
+ return
190
+
191
+ if state["mode"] == "new" or state["rect"] is None:
192
+ rect = QtCore.QRectF(state["anchor"], point)
193
+ else:
194
+ rect = QtCore.QRectF(state["rect"])
195
+ offset = state.get("handle_offset", QtCore.QPointF())
196
+ point = QtCore.QPointF(
197
+ point.x() - offset.x(),
198
+ point.y() - offset.y(),
199
+ )
200
+ self._resize_marquee_rect(rect, state["mode"], point, modifiers)
201
+
202
+ self.set_marquee_rect(rect)
203
+
204
+ def finish_marquee_drag(self) -> None:
205
+ self._marquee_drag_state = None
206
+
207
+ def marquee_contains_scene_pos(self, scene_pos: QtCore.QPointF) -> bool:
208
+ """
209
+ Return whether a scene position is inside the current marquee.
210
+
211
+ """
212
+ if self.marquee is None:
213
+ return False
214
+
215
+ point = self._marquee_viewbox().mapSceneToView(scene_pos)
216
+ return self.marquee.normalized().contains(point)
217
+
218
+ def open_marquee_context_menu(
219
+ self,
220
+ scene_pos: QtCore.QPointF,
221
+ global_pos: QtCore.QPoint | QtCore.QPointF | None = None,
222
+ ) -> bool:
223
+ """
224
+ Open the marquee context menu when a right-click lands inside it.
225
+
226
+ """
227
+ if not self.marquee_contains_scene_pos(scene_pos):
228
+ return False
229
+
230
+ menu = self._new_marquee_context_menu()
231
+ if global_pos is None:
232
+ global_pos = QtGui.QCursor.pos()
233
+ elif isinstance(global_pos, QtCore.QPointF):
234
+ global_pos = global_pos.toPoint()
235
+
236
+ menu.exec(global_pos)
237
+ return True
238
+
239
+ def _new_marquee_context_menu(self) -> qtw.QMenu:
240
+ menu = qtw.QMenu()
241
+ if hasattr(menu, "setToolTipsVisible"):
242
+ menu.setToolTipsVisible(True)
243
+
244
+ self._add_marquee_context_action(
245
+ menu,
246
+ "Zoom",
247
+ lambda: self.zoom_marquee("xy"),
248
+ )
249
+ self._add_marquee_context_action(
250
+ menu,
251
+ "Zoom X",
252
+ lambda: self.zoom_marquee("x"),
253
+ )
254
+ self._add_marquee_context_action(
255
+ menu,
256
+ "Zoom Y",
257
+ lambda: self.zoom_marquee("y"),
258
+ )
259
+ self._add_marquee_color_context_action(menu)
260
+ self._add_marquee_stats_context_action(menu)
261
+ return menu
262
+
263
+ def _add_marquee_context_action(
264
+ self,
265
+ menu: qtw.QMenu,
266
+ text: str,
267
+ callback: _MarqueeAction,
268
+ ) -> QtGui.QAction:
269
+ action = menu.addAction(text)
270
+ if action is None:
271
+ raise RuntimeError("Marquee context action could not be created.")
272
+ action.triggered.connect(
273
+ lambda _checked=False, callback=callback: self._execute_marquee_action(
274
+ callback
275
+ )
276
+ )
277
+ return action
278
+
279
+ def _add_marquee_color_context_action(
280
+ self,
281
+ menu: qtw.QMenu,
282
+ ) -> QtGui.QAction | None:
283
+ return None
284
+
285
+ def _add_marquee_stats_context_action(self, menu: qtw.QMenu) -> QtGui.QAction:
286
+ stats_text = self._marquee_stats_text()
287
+
288
+ def open_stats_dialog(stats_text: str | None = stats_text) -> bool:
289
+ return self.show_marquee_stats_dialog(stats_text)
290
+
291
+ action = self._add_marquee_context_action(
292
+ menu,
293
+ "Stats...",
294
+ open_stats_dialog,
295
+ )
296
+ if stats_text is None:
297
+ action.setEnabled(False)
298
+ action.setToolTip("No data points inside the marquee.")
299
+ else:
300
+ action.setToolTip("Show statistics for the marquee selection.")
301
+ action.setStatusTip("Show statistics for the marquee selection.")
302
+ return action
303
+
304
+ def _execute_marquee_action(self, callback: _MarqueeAction) -> None:
305
+ if callback():
306
+ self.clear_marquee()
307
+
308
+ def zoom_marquee(self, axes: str) -> bool:
309
+ rect = self.marquee.normalized() if self.marquee is not None else None
310
+ if rect is None:
311
+ return False
312
+
313
+ if "x" in axes:
314
+ self._marquee_zoom_viewbox("x").setXRange(
315
+ rect.left(),
316
+ rect.right(),
317
+ padding=0,
318
+ )
319
+ if "y" in axes:
320
+ self._marquee_zoom_viewbox("y").setYRange(
321
+ rect.top(),
322
+ rect.bottom(),
323
+ padding=0,
324
+ )
325
+ self._view_range_changed_programmatically()
326
+ return True
327
+
328
+ def _marquee_stats_text(self) -> str | None:
329
+ return None
330
+
331
+ def show_marquee_stats_dialog(self, stats_text: str | None = None) -> bool:
332
+ if stats_text is None:
333
+ stats_text = self._marquee_stats_text()
334
+ if stats_text is None:
335
+ return False
336
+
337
+ dialog = self._new_marquee_stats_dialog(stats_text)
338
+ self._marquee_stats_dialog = dialog
339
+ dialog.show()
340
+ dialog.raise_()
341
+ dialog.activateWindow()
342
+ return True
343
+
344
+ def _new_marquee_stats_dialog(self, stats_text: str) -> qtw.QDialog:
345
+ dialog = qtw.QDialog(self)
346
+ dialog.setWindowTitle("Marquee stats")
347
+
348
+ layout = qtw.QVBoxLayout(dialog)
349
+ stats_table = self._new_marquee_stats_table(stats_text)
350
+ layout.addWidget(stats_table)
351
+
352
+ buttons = qtw.QDialogButtonBox(qtw.QDialogButtonBox.StandardButton.Close)
353
+ copy_button = buttons.addButton("Copy", qtw.QDialogButtonBox.ButtonRole.ActionRole)
354
+ if copy_button is None:
355
+ raise RuntimeError("Marquee stats copy button is not available.")
356
+
357
+ def copy_stats(_checked: bool = False) -> None:
358
+ self.copy_marquee_stats_to_clipboard(stats_text)
359
+
360
+ copy_button.clicked.connect(copy_stats)
361
+ buttons.rejected.connect(dialog.close)
362
+ layout.addWidget(buttons)
363
+
364
+ return dialog
365
+
366
+ def _new_marquee_stats_table(self, stats_text: str) -> CopyableTableWidget:
367
+ rows = self._marquee_stats_table_rows(stats_text)
368
+ stats_table = CopyableTableWidget()
369
+ stats_table.setObjectName("detailsTable")
370
+ stats_table.setColumnCount(2)
371
+ stats_table.setHorizontalHeaderLabels(["Field", "Value"])
372
+ stats_table.setRowCount(len(rows))
373
+ stats_table.setAlternatingRowColors(True)
374
+ stats_table.setEditTriggers(qtw.QAbstractItemView.EditTrigger.NoEditTriggers)
375
+ stats_table.setSelectionBehavior(qtw.QAbstractItemView.SelectionBehavior.SelectRows)
376
+ stats_table.setSelectionMode(qtw.QAbstractItemView.SelectionMode.ExtendedSelection)
377
+ stats_table.setTextElideMode(QtCore.Qt.TextElideMode.ElideNone)
378
+ stats_table.setWordWrap(False)
379
+ vertical_header = stats_table.verticalHeader()
380
+ horizontal_header = stats_table.horizontalHeader()
381
+ if vertical_header is None or horizontal_header is None:
382
+ raise RuntimeError("Marquee stats table headers are not available.")
383
+
384
+ vertical_header.hide()
385
+ vertical_header.setMinimumSectionSize(16)
386
+ vertical_header.setDefaultSectionSize(20)
387
+ horizontal_header.setFixedHeight(22)
388
+ horizontal_header.setStretchLastSection(True)
389
+ horizontal_header.setSectionResizeMode(
390
+ 0,
391
+ qtw.QHeaderView.ResizeMode.ResizeToContents
392
+ )
393
+ horizontal_header.setSectionResizeMode(
394
+ 1,
395
+ qtw.QHeaderView.ResizeMode.Stretch
396
+ )
397
+ stats_table.setMinimumWidth(280)
398
+ stats_table.setMinimumHeight(170)
399
+
400
+ for row, (field, value) in enumerate(rows):
401
+ field_item = qtw.QTableWidgetItem(field)
402
+ value_item = qtw.QTableWidgetItem(value)
403
+ stats_table.setItem(row, 0, field_item)
404
+ stats_table.setItem(row, 1, value_item)
405
+
406
+ return stats_table
407
+
408
+ def _marquee_stats_table_rows(self, stats_text: str) -> list[tuple[str, str]]:
409
+ rows: list[tuple[str, str]] = []
410
+ for line in stats_text.splitlines():
411
+ line = line.strip()
412
+ if not line:
413
+ continue
414
+
415
+ if ":" in line:
416
+ field, value = line.split(":", 1)
417
+ rows.append((field.strip(), value.strip()))
418
+ else:
419
+ rows.append(("Selection", line))
420
+
421
+ return rows
422
+
423
+ def _format_marquee_stats_text(
424
+ self,
425
+ size_text: str,
426
+ values: Any,
427
+ rect: QtCore.QRectF | None = None,
428
+ *,
429
+ x_axis: str = "x",
430
+ y_axis: str = "y",
431
+ ) -> str:
432
+ marquee = self.marquee
433
+ if rect is None and marquee is not None:
434
+ rect = marquee.normalized()
435
+
436
+ lines = [size_text]
437
+ if rect is not None:
438
+ view_to_data = getattr(self, "view_to_data", None)
439
+ if callable(view_to_data):
440
+ x_low = view_to_data(x_axis, rect.left())
441
+ x_high = view_to_data(x_axis, rect.right())
442
+ y_low = view_to_data(y_axis, rect.top())
443
+ y_high = view_to_data(y_axis, rect.bottom())
444
+ else:
445
+ x_low, x_high = rect.left(), rect.right()
446
+ y_low, y_high = rect.top(), rect.bottom()
447
+ lines.extend((
448
+ f"X range: {self.formatNum(x_low)} to {self.formatNum(x_high)}",
449
+ f"Y range: {self.formatNum(y_low)} to {self.formatNum(y_high)}",
450
+ ))
451
+
452
+ lines.extend((
453
+ f"Average: {self.formatNum(finite_mean(values))}",
454
+ f"Standard deviation: {self.formatNum(finite_standard_deviation(values))}",
455
+ f"Max: {self.formatNum(float(values.max()))}",
456
+ f"Min: {self.formatNum(float(values.min()))}",
457
+ ))
458
+
459
+ return "\n".join(lines)
460
+
461
+ def copy_marquee_stats_to_clipboard(self, stats_text: str | None = None) -> bool:
462
+ if stats_text is None:
463
+ stats_text = self._marquee_stats_text()
464
+ if stats_text is None:
465
+ return False
466
+
467
+ clipboard = qtw.QApplication.clipboard()
468
+ if clipboard is None:
469
+ return False
470
+
471
+ clipboard.setText(stats_text)
472
+ return True
473
+
474
+ def _resize_marquee_rect(
475
+ self,
476
+ rect: QtCore.QRectF,
477
+ handle: _MarqueeHandle,
478
+ point: QtCore.QPointF,
479
+ modifiers: QtCore.Qt.KeyboardModifier = (
480
+ QtCore.Qt.KeyboardModifier.NoModifier
481
+ ),
482
+ ) -> None:
483
+ symmetric = bool(modifiers & QtCore.Qt.KeyboardModifier.AltModifier)
484
+ asymmetric = bool(modifiers & QtCore.Qt.KeyboardModifier.ShiftModifier) and not symmetric
485
+ original = QtCore.QRectF(rect)
486
+ anchor = self._marquee_handle_points_for_rect(original)[handle]
487
+
488
+ if "w" in handle:
489
+ rect.setLeft(point.x())
490
+ if "e" in handle:
491
+ rect.setRight(point.x())
492
+ if "s" in handle:
493
+ rect.setTop(point.y())
494
+ if "n" in handle:
495
+ rect.setBottom(point.y())
496
+
497
+ dx = point.x() - anchor.x()
498
+ dy = point.y() - anchor.y()
499
+
500
+ if ("w" in handle or "e" in handle) and (symmetric or asymmetric):
501
+ offset = -dx if symmetric else dx
502
+ if "w" in handle:
503
+ rect.setRight(original.right() + offset)
504
+ else:
505
+ rect.setLeft(original.left() + offset)
506
+
507
+ if ("n" in handle or "s" in handle) and (symmetric or asymmetric):
508
+ offset = -dy if symmetric else dy
509
+ if "s" in handle:
510
+ rect.setBottom(original.bottom() + offset)
511
+ else:
512
+ rect.setTop(original.top() + offset)
513
+
514
+ if asymmetric:
515
+ self._snap_translated_marquee_rect(rect, original, handle)
516
+
517
+ def _snap_translated_marquee_rect(
518
+ self,
519
+ rect: QtCore.QRectF,
520
+ original: QtCore.QRectF,
521
+ handle: _MarqueeHandle,
522
+ ) -> None:
523
+ snapped = self._snap_marquee_rect(QtCore.QRectF(rect).normalized())
524
+ if snapped is None:
525
+ return
526
+
527
+ adjusted = QtCore.QRectF(snapped)
528
+ if "w" in handle or "e" in handle:
529
+ width = original.width()
530
+ if "w" in handle:
531
+ adjusted.setLeft(snapped.left())
532
+ adjusted.setRight(snapped.left() + width)
533
+ else:
534
+ adjusted.setRight(snapped.right())
535
+ adjusted.setLeft(snapped.right() - width)
536
+
537
+ if "n" in handle or "s" in handle:
538
+ height = original.height()
539
+ if "s" in handle:
540
+ adjusted.setTop(snapped.top())
541
+ adjusted.setBottom(snapped.top() + height)
542
+ else:
543
+ adjusted.setBottom(snapped.bottom())
544
+ adjusted.setTop(snapped.bottom() - height)
545
+
546
+ rect.setRect(adjusted.left(), adjusted.top(), adjusted.width(), adjusted.height())
547
+
548
+ def set_marquee_rect(self, rect: QtCore.QRectF) -> None:
549
+ """
550
+ Snap, store, and draw the marquee rectangle.
551
+
552
+ """
553
+ snapped = self._snap_marquee_rect(rect.normalized())
554
+ if snapped is None or snapped.width() <= 0 or snapped.height() <= 0:
555
+ self.clear_marquee()
556
+ return
557
+
558
+ rect = snapped
559
+ self.marquee = QtCore.QRectF(rect)
560
+ self.marquee_highlight.setRect(rect)
561
+ self.marquee_outline.setRect(rect)
562
+ self.marquee_highlight.show()
563
+ self.marquee_outline.show()
564
+ self._update_marquee_handles()
565
+
566
+ def clear_marquee(self) -> None:
567
+ self.marquee = None
568
+ self.marquee_highlight.hide()
569
+ self.marquee_outline.hide()
570
+ self.marquee_handles.hide()
571
+
572
+ def _snap_marquee_rect(self, rect: QtCore.QRectF) -> QtCore.QRectF | None:
573
+ return rect
574
+
575
+ def marquee_drag_mode_at(self, scene_pos: QtCore.QPointF) -> _MarqueeHandle | None:
576
+ """
577
+ Return the resize handle under a scene position, if there is one.
578
+
579
+ """
580
+ if self.marquee is None or not self.marquee_handles.isVisible():
581
+ return None
582
+
583
+ threshold = 8
584
+ for handle, point in self._marquee_handle_points().items():
585
+ handle_scene_pos = self._marquee_viewbox().mapViewToScene(point)
586
+ distance = (
587
+ (handle_scene_pos.x() - scene_pos.x()) ** 2
588
+ + (handle_scene_pos.y() - scene_pos.y()) ** 2
589
+ )
590
+ if distance <= threshold ** 2:
591
+ return handle
592
+
593
+ return None
594
+
595
+ def marquee_cursor_shape_at(
596
+ self,
597
+ scene_pos: QtCore.QPointF,
598
+ modifiers: QtCore.Qt.KeyboardModifier = (
599
+ QtCore.Qt.KeyboardModifier.NoModifier
600
+ ),
601
+ ) -> QtCore.Qt.CursorShape | None:
602
+ mode = self.current_marquee_drag_mode()
603
+ if mode == "new":
604
+ return QtCore.Qt.CursorShape.CrossCursor
605
+ if mode is not None:
606
+ return self._marquee_cursor_shape_for_handle(mode)
607
+
608
+ handle = self.marquee_drag_mode_at(scene_pos)
609
+ if handle is not None:
610
+ return self._marquee_cursor_shape_for_handle(handle)
611
+ if modifiers & QtCore.Qt.KeyboardModifier.AltModifier:
612
+ return QtCore.Qt.CursorShape.CrossCursor
613
+
614
+ return None
615
+
616
+ def _marquee_cursor_shape_for_handle(
617
+ self,
618
+ handle: _MarqueeDragMode,
619
+ ) -> QtCore.Qt.CursorShape:
620
+ if handle in ("e", "w"):
621
+ return QtCore.Qt.CursorShape.SizeHorCursor
622
+ if handle in ("n", "s"):
623
+ return QtCore.Qt.CursorShape.SizeVerCursor
624
+ if handle in ("nw", "se"):
625
+ return QtCore.Qt.CursorShape.SizeFDiagCursor
626
+ if handle in ("ne", "sw"):
627
+ return QtCore.Qt.CursorShape.SizeBDiagCursor
628
+
629
+ return QtCore.Qt.CursorShape.CrossCursor
630
+
631
+ def _update_marquee_handles(self) -> None:
632
+ points = list(self._marquee_handle_points().values())
633
+ self.marquee_handles.setData(
634
+ [point.x() for point in points],
635
+ [point.y() for point in points],
636
+ )
637
+ self.marquee_handles.show()
638
+
639
+ def _marquee_handle_points(self) -> dict[_MarqueeHandle, QtCore.QPointF]:
640
+ if self.marquee is None:
641
+ return {}
642
+ return self._marquee_handle_points_for_rect(self.marquee)
643
+
644
+ def _marquee_handle_points_for_rect(
645
+ self,
646
+ rect: QtCore.QRectF,
647
+ ) -> dict[_MarqueeHandle, QtCore.QPointF]:
648
+ centre_x = rect.center().x()
649
+ centre_y = rect.center().y()
650
+ return {
651
+ "nw": QtCore.QPointF(rect.left(), rect.bottom()),
652
+ "n": QtCore.QPointF(centre_x, rect.bottom()),
653
+ "ne": QtCore.QPointF(rect.right(), rect.bottom()),
654
+ "e": QtCore.QPointF(rect.right(), centre_y),
655
+ "se": QtCore.QPointF(rect.right(), rect.top()),
656
+ "s": QtCore.QPointF(centre_x, rect.top()),
657
+ "sw": QtCore.QPointF(rect.left(), rect.top()),
658
+ "w": QtCore.QPointF(rect.left(), centre_y),
659
+ }