109 lines
5.0 KiB
Python
109 lines
5.0 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; 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."""
|