"""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."""