Files
templates/python/set_devices/qt_ports/plot_drag.py
T

127 lines
5.6 KiB
Python

"""Shared marker picking and axis-constrained plot drags (Qt 5/6).
The widget supplies plot/value rectangles, a PlotMath core, viewport/markers,
axis locks and refresh hooks. Geometry and numeric transforms use protocan.plot.
"""
from dataclasses import replace
try:
from PySide6.QtCore import QPointF, Qt
from PySide6.QtWidgets import QApplication
except ImportError:
from PySide2.QtCore import QPointF, Qt
from PySide2.QtWidgets import QApplication
DRAG_HINT = ('ЛКМ рядом с маркером — двигать маркер; ЛКМ на свободном поле — '
'перемещение по X; Ctrl + ЛКМ или Shift + ЛКМ — перемещение по Y. '
'Щелчок мимо маркеров — одиночный курсор.')
def vertical_pan_requested(event):
return bool(event.modifiers() & (Qt.ShiftModifier | Qt.ControlModifier))
def cursor_hit(plot, position, value, event, radius=8):
"""Ctrl/Shift always pan; cursors are grabbed near their visible lines."""
return (not vertical_pan_requested(event) and value is not None
and plot.core is not None and plot.has_plot_data
and plot.visible_bounds.left <= value <= plot.visible_bounds.right
and plot.plot_rect().contains(position)
and abs(plot.project(value, 0, plot.plot_rect()).x() - position.x()) <= radius)
class PlotDragInteraction:
"""Capture a hit marker or pan one axis, chosen at the start of the drag."""
def _capture_y_pan(self):
return self.viewport
def _apply_y_pan(self, fraction, initial):
if self.core and not self.lock_y:
self.viewport = initial.transform(self.core, pan_y=fraction)
def _navigation_pan_y(self, fraction):
self._apply_y_pan(fraction, self._capture_y_pan())
self._refresh_controls()
self.update()
self.view_changed.emit()
def mousePressEvent(self, event):
if event.button() != Qt.LeftButton or not self.core:
return super().mousePressEvent(event)
pos = event.position() if hasattr(event, 'position') else event.localPos()
rect, values = self.plot_rect(), self.value_rect()
self._press = self._dragged = None
if not rect.contains(pos):
return
self.setFocus(Qt.MouseFocusReason)
self._press = QPointF(pos)
self._press_viewport = self.viewport
self._press_y_pan = self._capture_y_pan()
self._press_markers = self.markers
self._press_bounds = self.visible_bounds
self._pan_vertical = vertical_pan_requested(event)
self._pan_moved = False
if self.marker_mode.isChecked() and not self._pan_vertical and self.has_plot_data:
# X lines span digital lanes; Y lines only occupy the analog pane.
markers = self.markers if values.contains(pos) else replace(self.markers, y_enabled=False)
self._dragged = markers.hit(self.core, pos.x() - rect.left(),
pos.y() - values.top() if values.contains(pos) else 0,
rect.width(), values.height(), 12, self._press_bounds)
if self._dragged is not None:
self.markers = replace(self.markers, selected=self._dragged)
self._refresh_controls()
self.update()
event.accept()
def mouseMoveEvent(self, event):
if not event.buttons() & Qt.LeftButton:
self._press = self._dragged = None
return super().mouseMoveEvent(event)
if self._press is None or not self.core:
return super().mouseMoveEvent(event)
pos = event.position() if hasattr(event, 'position') else event.localPos()
self._update_drag(pos)
event.accept()
def _update_drag(self, pos):
delta = pos - self._press
self._pan_moved |= delta.manhattanLength() >= QApplication.startDragDistance()
rect, values = self.plot_rect(), self.value_rect()
if self._dragged is not None:
marker = self._dragged
self.markers = self._press_markers.drag(self.core, marker,
delta.y() if marker.horizontal else delta.x(),
values.height() if marker.horizontal else rect.width(), self._press_bounds)
elif self._pan_vertical:
self._apply_y_pan(delta.y() / values.height(), self._press_y_pan)
else:
self.viewport = self._press_viewport.transform(self.core,
pan_x=delta.x() / rect.width() if not self.lock_x else 0)
self._refresh_controls()
if self._dragged is not None:
getattr(self, "update_overlay", self.update)()
else:
self.update()
def mouseReleaseEvent(self, event):
if event.button() != Qt.LeftButton:
return super().mouseReleaseEvent(event)
if self._press is not None and self.core:
# Qt may coalesce moves; always commit the final release position.
pos = event.position() if hasattr(event, 'position') else event.localPos()
self._update_drag(pos)
single_click = (self._dragged is None and not self._pan_vertical
and not self._pan_moved and self.plot_rect().contains(pos))
if single_click:
# A click with small hand jitter must not pan the time axis.
self.viewport = self._press_viewport
self._single_cursor_click(pos)
self.view_changed.emit()
self._press = self._dragged = None
super().mouseReleaseEvent(event)
def _single_cursor_click(self, pos):
"""Optional widget hook: return from paired markers to a single cursor."""