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.
Files changed (119) hide show
  1. qplot/__init__.py +46 -0
  2. qplot/__main__.py +232 -0
  3. qplot/_auxiliary_paths.py +63 -0
  4. qplot/_metadata.py +12 -0
  5. qplot/_repair.py +14 -0
  6. qplot/_shutdown_supervisor.py +4049 -0
  7. qplot/_version.py +12 -0
  8. qplot/_windows_shutdown_job.py +1166 -0
  9. qplot/configuration/__init__.py +3 -0
  10. qplot/configuration/config.py +573 -0
  11. qplot/configuration/config_schema.json +319 -0
  12. qplot/configuration/scripts.py +255 -0
  13. qplot/configuration/themes/__init__.py +13 -0
  14. qplot/configuration/themes/_base.py +579 -0
  15. qplot/configuration/themes/blank.py +17 -0
  16. qplot/configuration/themes/dark.py +54 -0
  17. qplot/configuration/themes/light.py +54 -0
  18. qplot/datahandling/LoadFromDB.py +224 -0
  19. qplot/datahandling/__init__.py +47 -0
  20. qplot/datahandling/_trusted_live_helper.py +812 -0
  21. qplot/datahandling/_trusted_live_protocol.py +1745 -0
  22. qplot/datahandling/database.py +2330 -0
  23. qplot/datahandling/dimensions.py +43 -0
  24. qplot/datahandling/file_identity.py +461 -0
  25. qplot/datahandling/parameter_data.py +149 -0
  26. qplot/datahandling/plot_progress.py +17 -0
  27. qplot/datahandling/qcodes_cache.py +277 -0
  28. qplot/datahandling/qcodes_compat.py +14 -0
  29. qplot/datahandling/readSQL.py +2442 -0
  30. qplot/datahandling/readonly.py +2561 -0
  31. qplot/datahandling/source_activity.py +21 -0
  32. qplot/datahandling/trusted_array_preview.py +127 -0
  33. qplot/datahandling/trusted_derived_cache.py +1605 -0
  34. qplot/datahandling/trusted_derived_rendering.py +868 -0
  35. qplot/datahandling/trusted_heatmap.py +241 -0
  36. qplot/datahandling/trusted_live.py +2808 -0
  37. qplot/datahandling/trusted_live_queries.py +5907 -0
  38. qplot/datahandling/trusted_live_service.py +1894 -0
  39. qplot/datahandling/trusted_live_supervisor.py +2161 -0
  40. qplot/datahandling/trusted_plot.py +283 -0
  41. qplot/datahandling/trusted_presentation.py +1065 -0
  42. qplot/datahandling/trusted_snapshot.py +1132 -0
  43. qplot/datahandling/trusted_work_coordinator.py +2157 -0
  44. qplot/datahandling/trusted_work_scheduler.py +1204 -0
  45. qplot/diagnostics.py +241 -0
  46. qplot/resources/testdata/qplot_test_db_01_10mb.csv +11 -0
  47. qplot/resources/testdata/qplot_test_db_02_25mb.csv +14 -0
  48. qplot/resources/testdata/qplot_test_db_03_50mb.csv +17 -0
  49. qplot/resources/testdata/qplot_test_db_04_100mb.csv +20 -0
  50. qplot/resources/testdata/qplot_test_db_05_250mb.csv +23 -0
  51. qplot/resources/testdata/qplot_test_db_06_500mb.csv +26 -0
  52. qplot/resources/testdata/qplot_test_db_07_1gb.csv +29 -0
  53. qplot/resources/testdata/qplot_test_db_08_5gb.csv +32 -0
  54. qplot/resources/testdata/qplot_test_db_09_10gb.csv +35 -0
  55. qplot/resources/testdata/qplot_test_db_10_30gb.csv +38 -0
  56. qplot/testdata.py +1733 -0
  57. qplot/tools/__init__.py +13 -0
  58. qplot/tools/finite_means.py +114 -0
  59. qplot/tools/general.py +134 -0
  60. qplot/tools/heatmap_geometry.py +381 -0
  61. qplot/tools/operation_registry.py +179 -0
  62. qplot/tools/plot_tools.py +625 -0
  63. qplot/tools/sample_statistics.py +56 -0
  64. qplot/tools/worker.py +2787 -0
  65. qplot/windows/__init__.py +9 -0
  66. qplot/windows/_colorbar.py +422 -0
  67. qplot/windows/_commands.py +523 -0
  68. qplot/windows/_config_persistence.py +123 -0
  69. qplot/windows/_database_actions.py +4904 -0
  70. qplot/windows/_dataset_handle.py +221 -0
  71. qplot/windows/_dragdrop.py +71 -0
  72. qplot/windows/_export_paths.py +1167 -0
  73. qplot/windows/_help.py +169 -0
  74. qplot/windows/_native_averaging.py +173 -0
  75. qplot/windows/_native_transforms.py +389 -0
  76. qplot/windows/_plot1d_snap.py +608 -0
  77. qplot/windows/_plot1d_traces.py +2183 -0
  78. qplot/windows/_plot2d_colorbar.py +1126 -0
  79. qplot/windows/_plot2d_colorbar_dialog.py +786 -0
  80. qplot/windows/_plot2d_layers.py +2197 -0
  81. qplot/windows/_plot2d_sweeps.py +712 -0
  82. qplot/windows/_plotWin.py +1993 -0
  83. qplot/windows/_plot_actions.py +2695 -0
  84. qplot/windows/_plot_appearance.py +276 -0
  85. qplot/windows/_plot_axis_scaling.py +1485 -0
  86. qplot/windows/_plot_export.py +1445 -0
  87. qplot/windows/_plot_feedback.py +110 -0
  88. qplot/windows/_plot_marquee.py +656 -0
  89. qplot/windows/_plot_refresh.py +942 -0
  90. qplot/windows/_plot_state.py +239 -0
  91. qplot/windows/_plot_transform_labels.py +60 -0
  92. qplot/windows/_preferences.py +635 -0
  93. qplot/windows/_refresh_interval.py +70 -0
  94. qplot/windows/_run_controls.py +828 -0
  95. qplot/windows/_shortcuts.py +47 -0
  96. qplot/windows/_subplots/__init__.py +8 -0
  97. qplot/windows/_subplots/subplot1d.py +531 -0
  98. qplot/windows/_subplots/subplot2d.py +918 -0
  99. qplot/windows/_svg_export.py +209 -0
  100. qplot/windows/_trusted_derived_qt.py +1759 -0
  101. qplot/windows/_widgets/__init__.py +23 -0
  102. qplot/windows/_widgets/_run_formatting.py +387 -0
  103. qplot/windows/_widgets/details_tables.py +1119 -0
  104. qplot/windows/_widgets/dropbox.py +290 -0
  105. qplot/windows/_widgets/operations.py +460 -0
  106. qplot/windows/_widgets/preview.py +2997 -0
  107. qplot/windows/_widgets/run_list_items.py +488 -0
  108. qplot/windows/_widgets/toolbar.py +111 -0
  109. qplot/windows/_widgets/treeWidgets.py +2572 -0
  110. qplot/windows/_window_controls.py +413 -0
  111. qplot/windows/main.py +1635 -0
  112. qplot/windows/plot1d.py +311 -0
  113. qplot/windows/plot2d.py +1901 -0
  114. qplotter-1.6.0b3.dist-info/METADATA +318 -0
  115. qplotter-1.6.0b3.dist-info/RECORD +119 -0
  116. qplotter-1.6.0b3.dist-info/WHEEL +5 -0
  117. qplotter-1.6.0b3.dist-info/entry_points.txt +4 -0
  118. qplotter-1.6.0b3.dist-info/licenses/LICENSE +21 -0
  119. qplotter-1.6.0b3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,47 @@
1
+ import sys
2
+
3
+ from PyQt6 import QtWidgets as qtw
4
+ from PyQt6.QtGui import QKeySequence
5
+
6
+
7
+ def key_sequences(sequences):
8
+ """
9
+ Converts shortcut text to QKeySequence instances.
10
+
11
+ """
12
+ return [
13
+ sequence if isinstance(sequence, QKeySequence) else QKeySequence(sequence)
14
+ for sequence in sequences
15
+ ]
16
+
17
+
18
+ def platform_key_sequences(mac=None, windows=None, other=None):
19
+ """
20
+ Returns shortcuts for the current platform.
21
+
22
+ """
23
+ if sys.platform == "darwin":
24
+ sequences = mac or []
25
+ elif sys.platform.startswith("win"):
26
+ sequences = windows or []
27
+ else:
28
+ sequences = other if other is not None else windows or []
29
+
30
+ return key_sequences(sequences)
31
+
32
+
33
+ def standard_key_sequences(standard_key, fallback):
34
+ """
35
+ Returns Qt's platform-specific shortcuts, with explicit fallbacks.
36
+
37
+ """
38
+ shortcuts = []
39
+ if qtw.QApplication.instance() is not None:
40
+ shortcuts = QKeySequence.keyBindings(standard_key)
41
+
42
+ for sequence in reversed(key_sequences(fallback)):
43
+ if sequence in shortcuts:
44
+ shortcuts.remove(sequence)
45
+ shortcuts.insert(0, sequence)
46
+
47
+ return shortcuts
@@ -0,0 +1,8 @@
1
+ from .subplot1d import custom_viewbox, subplot1d
2
+
3
+ # Importing .subplot2d causes circular import.
4
+
5
+ __all__ = [
6
+ "subplot1d",
7
+ "custom_viewbox",
8
+ ]
@@ -0,0 +1,531 @@
1
+ from collections.abc import Callable
2
+
3
+ import pyqtgraph as pg
4
+ from PyQt6 import QtCore
5
+ from PyQt6.QtGui import QColor
6
+
7
+ from .._native_transforms import NativePlotDataItem
8
+ from .._plot_refresh import plot_refresh_required
9
+ from .._refresh_interval import refresh_interval_value, set_refresh_interval
10
+
11
+
12
+ def _subplot_axis_order(
13
+ parent_options: dict[str, str],
14
+ source_options: dict[str, str],
15
+ *,
16
+ source_is_cut: bool = False,
17
+ shared_parameter: str | None = None,
18
+ ) -> tuple[str, str] | None:
19
+ """Map source data axes onto a host plot, or reject incompatible axes."""
20
+
21
+ if shared_parameter:
22
+ parent_axes = [
23
+ axis for axis in ("x", "y")
24
+ if parent_options.get(axis) == shared_parameter
25
+ ]
26
+ source_axes = [
27
+ axis for axis in ("x", "y")
28
+ if source_options.get(axis) == shared_parameter
29
+ ]
30
+ if len(parent_axes) != 1 or len(source_axes) != 1:
31
+ return None
32
+
33
+ parent_axis = parent_axes[0]
34
+ source_axis = source_axes[0]
35
+ # A cut's axis_options['y'] is its fixed setpoint selection, while its
36
+ # axis_data['y'] remains the dependent cut values. It therefore cannot
37
+ # act as the shared coordinate.
38
+ if source_is_cut and source_axis != "x":
39
+ return None
40
+ return ("x", "y") if parent_axis == source_axis else ("y", "x")
41
+
42
+ shared_x = parent_options.get("x")
43
+ if not shared_x:
44
+ return None
45
+ if shared_x == source_options.get("x"):
46
+ return "x", "y"
47
+ if not source_is_cut and shared_x == source_options.get("y"):
48
+ return "y", "x"
49
+
50
+ return None
51
+
52
+
53
+ def _subplot_shared_parameter(parent) -> str | None:
54
+ """Return the host plot's canonical 1D independent parameter name."""
55
+
56
+ default_names = getattr(parent, "_default_plot_axis_names", None)
57
+ if callable(default_names):
58
+ names = default_names()
59
+ if names is not None:
60
+ return names[0]
61
+
62
+ independent = tuple(getattr(getattr(parent, "param", None), "depends_on_", ()))
63
+ if len(independent) == 1:
64
+ return str(independent[0])
65
+ return None
66
+
67
+
68
+ class subplot1d(NativePlotDataItem):
69
+ """
70
+ Class for handling secondary line plots on plot1d
71
+ """
72
+ def __init__(self, parent, from_win, *args, **kargs):
73
+ super().__init__(*args, **kargs)
74
+
75
+ self.label = from_win.label
76
+ self.param_dict = from_win.param_dict
77
+ self.running = plot_refresh_required(from_win)
78
+
79
+ self.parent = parent
80
+ self.from_win = from_win
81
+ self._source_consumer_registered = False
82
+ self._source_interval_signal = None
83
+ self._source_interval_slot = None
84
+
85
+ self.choose_from: tuple[str, str] | None = None
86
+ self.axis_param = {}
87
+ self._source_update_signal = getattr(from_win, "trace_updated", None)
88
+ if self._source_update_signal is not None:
89
+ self._source_update_signal.connect(self._source_trace_updated)
90
+ self._source_compatibility_signal = getattr(
91
+ from_win,
92
+ "merge_compatibility_changed",
93
+ None,
94
+ )
95
+ if self._source_compatibility_signal is not None:
96
+ self._source_compatibility_signal.connect(
97
+ self._source_compatibility_changed
98
+ )
99
+
100
+ self.refresh()
101
+
102
+ self.side = "left"
103
+ self.parent.plot.addItem(self)
104
+ self._register_source_consumer()
105
+
106
+
107
+ def refresh(self, *, source_ready: bool = False):
108
+ """
109
+ Fetches data from source window and updates view on parent window
110
+
111
+ """
112
+ parent = self.parent
113
+ from_win = self.from_win
114
+
115
+ self._disconnect_pending_update()
116
+
117
+ # Update live state
118
+ self.running = plot_refresh_required(from_win)
119
+
120
+ # Get which data is on which axis
121
+ parent_options = parent.axis_options
122
+ from_win_options = from_win.axis_options
123
+
124
+ self.choose_from = _subplot_axis_order(
125
+ parent_options,
126
+ from_win_options,
127
+ source_is_cut=hasattr(from_win, "sweep_id"),
128
+ shared_parameter=_subplot_shared_parameter(parent),
129
+ )
130
+ if self.choose_from is None:
131
+ self.setData(x=[], y=[])
132
+ return
133
+
134
+ # Wait for data to finish
135
+ if from_win.worker.running and not source_ready:
136
+ if self._source_update_signal is None:
137
+ from_win.end_wait.connect(self.call_update)
138
+ return
139
+
140
+ self.call_update()
141
+
142
+
143
+ @QtCore.pyqtSlot()
144
+ def _source_trace_updated(self):
145
+ """Refresh immediately after the source publishes ready-to-use data."""
146
+
147
+ self.refresh(source_ready=True)
148
+
149
+
150
+ @QtCore.pyqtSlot()
151
+ def _source_compatibility_changed(self):
152
+ """Clear old cut data until the changed axes publish replacement data."""
153
+
154
+ self._disconnect_pending_update()
155
+ self.choose_from = _subplot_axis_order(
156
+ self.parent.axis_options,
157
+ self.from_win.axis_options,
158
+ source_is_cut=hasattr(self.from_win, "sweep_id"),
159
+ shared_parameter=_subplot_shared_parameter(self.parent),
160
+ )
161
+ self.setData(x=[], y=[])
162
+
163
+
164
+ def _disconnect_pending_update(self):
165
+ try:
166
+ self.from_win.end_wait.disconnect(self.call_update)
167
+ except (TypeError, RuntimeError): # Signal was absent or already gone.
168
+ pass
169
+
170
+
171
+ def _register_source_consumer(self):
172
+ if self._source_consumer_registered:
173
+ return
174
+
175
+ source = self.from_win
176
+ source._merged_trace_users = max(
177
+ int(getattr(source, "_merged_trace_users", 0)),
178
+ 0,
179
+ ) + 1
180
+ self._source_consumer_registered = True
181
+
182
+ if not getattr(source, "visible", True):
183
+ parent_spinbox = getattr(self.parent, "spinBox", None)
184
+ source_spinbox = getattr(source, "spinBox", None)
185
+ if parent_spinbox is not None and source_spinbox is not None:
186
+ def sync_interval(interval):
187
+ interval = refresh_interval_value(parent_spinbox)
188
+ set_refresh_interval(source_spinbox, interval)
189
+ source.monitorIntervalChanged(interval)
190
+
191
+ sync_interval(refresh_interval_value(parent_spinbox))
192
+ interval_signal = getattr(parent_spinbox, "valueChanged", None)
193
+ if interval_signal is not None:
194
+ interval_slot = sync_interval
195
+ interval_signal.connect(interval_slot)
196
+ self._source_interval_signal = interval_signal
197
+ self._source_interval_slot = interval_slot
198
+
199
+ if (
200
+ getattr(source, "_closed", False)
201
+ and plot_refresh_required(source)
202
+ and not source.monitor.isActive()
203
+ ):
204
+ source.monitorIntervalChanged(refresh_interval_value(source.spinBox))
205
+
206
+
207
+ def _release_source_consumer(self):
208
+ if not self._source_consumer_registered:
209
+ return
210
+
211
+ source = self.from_win
212
+ remaining = max(int(getattr(source, "_merged_trace_users", 1)) - 1, 0)
213
+ source._merged_trace_users = remaining
214
+ self._source_consumer_registered = False
215
+
216
+ if self._source_interval_signal is not None:
217
+ try:
218
+ self._source_interval_signal.disconnect(self._source_interval_slot)
219
+ except (TypeError, RuntimeError): # Signal was absent or already gone.
220
+ pass
221
+ self._source_interval_signal = None
222
+ self._source_interval_slot = None
223
+
224
+ if remaining == 0 and not getattr(source, "visible", True):
225
+ monitor = getattr(source, "monitor", None)
226
+ if monitor is not None:
227
+ monitor.stop()
228
+
229
+
230
+ def disconnect_source_updates(self):
231
+ """Release source-update connections when a trace is removed."""
232
+
233
+ if self._source_update_signal is not None:
234
+ try:
235
+ self._source_update_signal.disconnect(self._source_trace_updated)
236
+ except (TypeError, RuntimeError): # Signal was absent or already gone.
237
+ pass
238
+ self._source_update_signal = None
239
+ if self._source_compatibility_signal is not None:
240
+ try:
241
+ self._source_compatibility_signal.disconnect(
242
+ self._source_compatibility_changed
243
+ )
244
+ except (TypeError, RuntimeError): # Signal was absent or already gone.
245
+ pass
246
+ self._source_compatibility_signal = None
247
+ self._disconnect_pending_update()
248
+ self._release_source_consumer()
249
+
250
+ @QtCore.pyqtSlot()
251
+ def call_update(self):
252
+ """
253
+ Event handler for self.refresh/from_win.worker finish
254
+ Updates the subplot line data.
255
+
256
+ """
257
+ data = {}
258
+
259
+ choose_from = self.choose_from
260
+ if choose_from is None:
261
+ self.setData(x=[], y=[])
262
+ self._disconnect_pending_update()
263
+ return
264
+
265
+ # Assign data to correct axis
266
+ for itr, axis in enumerate(["x", "y"]):
267
+ data[axis] = self.from_win.axis_data[choose_from[itr]]
268
+
269
+ # Updates display
270
+ self.setData(
271
+ x=data["x"],
272
+ y=data["y"],
273
+ )
274
+
275
+ # Keep metadata with the samples we actually copied. Axis selections
276
+ # and operation controls can change before replacement data is ready.
277
+ source_params = getattr(self.from_win, "axis_param", {})
278
+ self.axis_param = {
279
+ axis: source_params[source_axis]
280
+ for axis, source_axis in zip(("x", "y"), choose_from, strict=True)
281
+ if source_axis in source_params
282
+ }
283
+ sync_labels = getattr(self.parent, "_sync_trace_axis_labels", None)
284
+ if callable(sync_labels):
285
+ sync_labels()
286
+
287
+ self._disconnect_pending_update()
288
+
289
+ @QtCore.pyqtSlot(QColor)
290
+ def set_color(self, col):
291
+ """
292
+ Event handler connect to qplot.windows._widgets.dropbox.picker_1d.color_box
293
+ Updates the display color of line based on color_box selection
294
+
295
+ Parameters
296
+ ----------
297
+ col : PyQt6.QtGui.QColor
298
+ The color to change line to.
299
+
300
+ """
301
+ self.setPen(col)
302
+
303
+
304
+ @QtCore.pyqtSlot(str)
305
+ def set_side(self, side):
306
+ """
307
+ Event handler connect to qplot.windows._widgets.dropbox.picker_1d.axis_side
308
+ Changes the axis the line is attached to on axis_side selection
309
+
310
+ Parameters
311
+ ----------
312
+ side : str
313
+ 'left' or 'right', connects plot display to the corresponding y axis.
314
+
315
+ """
316
+ side = side.lower()
317
+ parent = self.parent
318
+
319
+ # Change cancelled
320
+ if self.side == side:
321
+ return
322
+
323
+ # Use the same registration-preserving path as Trace Appearance.
324
+ parent._set_trace_y_axis(parent._window_trace_key(self.from_win), side)
325
+
326
+ parent.vb.enableAutoRange()
327
+ self.side = side
328
+
329
+
330
+ class custom_viewbox(pg.ViewBox):
331
+ """
332
+ A custom view box used in qplot.windows.plotWin.PlotWidget which builds on
333
+ the default viewbox of the plotItem
334
+
335
+ The additional functions allow the main (left) viewbox to control the right
336
+ viewbox and scale by the same relative amount.
337
+
338
+ Each function emits a signal which tells qplot.windows.plot1d.plot1d.right_vb
339
+ to do the same.
340
+ """
341
+ main_moved = QtCore.pyqtSignal([object])
342
+ autoRange_triggered = QtCore.pyqtSignal()
343
+
344
+ def __init__(self, *args, **kargs):
345
+ super().__init__(*args, **kargs)
346
+ self._marquee_owner = None
347
+ self._auto_range_handler: Callable[[], None] | None = None
348
+ self._shift_pan_axis_constraint = True
349
+ self._shift_pan_axis = None
350
+ self._main_moved_axis = None
351
+ self.setAcceptHoverEvents(True)
352
+
353
+
354
+ def set_marquee_owner(self, owner):
355
+ self._marquee_owner = owner
356
+
357
+ def set_auto_range_handler(self, handler: Callable[[], None] | None) -> None:
358
+ """Let a line-plot owner validate full-view requests before mutation."""
359
+ self._auto_range_handler = handler
360
+
361
+
362
+ def set_shift_pan_axis_constraint(self, enabled):
363
+ """Enable Shift-drag axis locking for panning in this view box."""
364
+
365
+ self._shift_pan_axis_constraint = bool(enabled)
366
+ self._shift_pan_axis = None
367
+
368
+
369
+ def _shift_pan_axis_for_event(self, ev):
370
+ """Return the axis selected by a Shift-pan drag, if applicable."""
371
+
372
+ if (
373
+ not self._shift_pan_axis_constraint
374
+ or self.state["mouseMode"] != self.PanMode
375
+ or ev.button() not in {
376
+ QtCore.Qt.MouseButton.LeftButton,
377
+ QtCore.Qt.MouseButton.MiddleButton,
378
+ }
379
+ ):
380
+ return None
381
+
382
+ if not ev.modifiers() & QtCore.Qt.KeyboardModifier.ShiftModifier:
383
+ self._shift_pan_axis = None
384
+ return None
385
+
386
+ if ev.isStart():
387
+ self._shift_pan_axis = None
388
+
389
+ if self._shift_pan_axis is None:
390
+ drag = ev.pos() - ev.buttonDownPos(ev.button())
391
+ if drag.x() == 0 and drag.y() == 0:
392
+ return None
393
+ self._shift_pan_axis = 0 if abs(drag.x()) >= abs(drag.y()) else 1
394
+
395
+ return self._shift_pan_axis
396
+
397
+
398
+ def _handle_marquee_mouse_drag(self, ev):
399
+ owner = self._marquee_owner
400
+ if owner is None or ev.button() != QtCore.Qt.MouseButton.LeftButton:
401
+ return False
402
+
403
+ if ev.isStart():
404
+ mode = owner.marquee_drag_mode_at(ev.buttonDownScenePos())
405
+ if mode is None and not ev.modifiers() & QtCore.Qt.KeyboardModifier.AltModifier:
406
+ return False
407
+
408
+ owner.begin_marquee_drag(
409
+ owner._marquee_viewbox().mapSceneToView(ev.buttonDownScenePos()),
410
+ mode,
411
+ )
412
+
413
+ if not owner.is_marquee_dragging():
414
+ return False
415
+
416
+ self._update_marquee_cursor(ev.scenePos(), ev.modifiers())
417
+ owner.drag_marquee_to(
418
+ owner._marquee_viewbox().mapSceneToView(ev.scenePos()),
419
+ ev.modifiers(),
420
+ )
421
+ if ev.isFinish():
422
+ owner.finish_marquee_drag()
423
+ self._update_marquee_cursor(ev.scenePos(), ev.modifiers())
424
+
425
+ ev.accept()
426
+ return True
427
+
428
+
429
+ def _update_marquee_cursor(self, scene_pos, modifiers=QtCore.Qt.KeyboardModifier.NoModifier):
430
+ owner = self._marquee_owner
431
+ if owner is None:
432
+ self.unsetCursor()
433
+ return
434
+
435
+ cursor_shape = owner.marquee_cursor_shape_at(scene_pos, modifiers)
436
+ if cursor_shape is None:
437
+ self.unsetCursor()
438
+ else:
439
+ self.setCursor(cursor_shape)
440
+
441
+
442
+ def hoverMoveEvent(self, ev):
443
+ self._update_marquee_cursor(ev.scenePos(), ev.modifiers())
444
+ super().hoverMoveEvent(ev)
445
+
446
+
447
+ def hoverLeaveEvent(self, ev):
448
+ owner = self._marquee_owner
449
+ if owner is None or not owner.is_marquee_dragging():
450
+ self.unsetCursor()
451
+ super().hoverLeaveEvent(ev)
452
+
453
+
454
+ def mouseDoubleClickEvent(self, ev):
455
+ owner = self._marquee_owner
456
+ if owner is not None and getattr(owner, "marquee", None) is not None:
457
+ owner.clear_marquee()
458
+ self.unsetCursor()
459
+ ev.accept()
460
+ return
461
+
462
+ super().mouseDoubleClickEvent(ev)
463
+
464
+
465
+ def mouseClickEvent(self, ev):
466
+ owner = self._marquee_owner
467
+ if (
468
+ owner is not None
469
+ and ev.button() == QtCore.Qt.MouseButton.RightButton
470
+ and owner.open_marquee_context_menu(
471
+ ev.scenePos(),
472
+ self._mouse_event_global_pos(ev),
473
+ )
474
+ ):
475
+ ev.accept()
476
+ return
477
+
478
+ super().mouseClickEvent(ev)
479
+
480
+
481
+ def _mouse_event_global_pos(self, ev):
482
+ for attr_name in ("screenPos", "globalPos"):
483
+ attr = getattr(ev, attr_name, None)
484
+ if attr is None:
485
+ continue
486
+ pos = attr() if callable(attr) else attr
487
+ if isinstance(pos, QtCore.QPointF):
488
+ return pos.toPoint()
489
+ if isinstance(pos, QtCore.QPoint):
490
+ return pos
491
+
492
+ return None
493
+
494
+
495
+ def mouseDragEvent(self, ev, axis=None):
496
+ if axis is None and self._handle_marquee_mouse_drag(ev):
497
+ return
498
+
499
+ constrained_axis = axis
500
+ if axis is None:
501
+ constrained_axis = self._shift_pan_axis_for_event(ev)
502
+
503
+ super().mouseDragEvent(ev, axis=constrained_axis)
504
+
505
+ if axis is None:
506
+ self._main_moved_axis = constrained_axis
507
+ try:
508
+ self.main_moved.emit(ev)
509
+ finally:
510
+ self._main_moved_axis = None
511
+
512
+ if ev.isFinish():
513
+ self._shift_pan_axis = None
514
+
515
+ def wheelEvent(self, ev, axis=None):
516
+ super().wheelEvent(ev, axis=axis)
517
+
518
+ if axis is None:
519
+ self.main_moved.emit(ev)
520
+
521
+ def autoRange(self, padding=None, items=None, item=None):
522
+ if (
523
+ self._auto_range_handler is not None
524
+ and padding is None and items is None and item is None
525
+ ):
526
+ self._auto_range_handler()
527
+ self.autoRange_triggered.emit()
528
+ return
529
+ super().autoRange(padding=padding, items=items, item=item)
530
+
531
+ self.autoRange_triggered.emit()