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

109 lines
5.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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; Shift + ЛКМ — перемещение по Y. '
'Щелчок мимо маркеров — одиночный курсор.')
def vertical_pan_requested(event):
return bool(event.modifiers() & Qt.ShiftModifier)
def cursor_hit(plot, position, value, event, radius=8):
"""Shift always pans; a cursor can only be grabbed near its visible line."""
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 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_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)
else:
self.viewport = self._press_viewport.transform(self.core,
pan_x=delta.x() / rect.width() if not self._pan_vertical and not self.lock_x else 0,
pan_y=delta.y() / values.height() if self._pan_vertical and not self.lock_y else 0)
self._refresh_controls()
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."""