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