# -*- coding: utf-8 -*-
#
# Licensed under the terms of the BSD 3-Clause
# (see plotpy/LICENSE for details)
# pylint: disable-msg=W0622,R0903
# W0622: complains about filter being a builtin
# R0903: complains about too few public methods which is the purpose here
"""
Event handling
--------------
Overview
^^^^^^^^
The :mod:`plotpy.events` module provides classes to handle events on a
:class:`plotpy.plot.PlotWidget`.
The following classes are available:
* :class:`.StatefulEventFilter`: stateful event filter
* :class:`.KeyEventMatch`: key event match
* :class:`.StandardKeyMatch`: standard key event match
* :class:`.MouseEventMatch`: mouse event match
* :class:`.GestureEventMatch`: gesture event match
* :class:`.DragHandler`: drag handler
* :class:`.ClickHandler`: click handler
* :class:`.PanHandler`: pan handler
* :class:`.ZoomHandler`: zoom handler
* :class:`.GestureHandler`: gesture handler
* :class:`.PinchPanGestureHandler`: pinch and pan gesture handler
* :class:`.MenuHandler`: menu handler
* :class:`.QtDragHandler`: Qt drag handler
* :class:`.AutoZoomHandler`: auto zoom handler
* :class:`.MoveHandler`: move handler
* :class:`.ObjectHandler`: object handler
* :class:`.RectangularSelectionHandler`: rectangular selection handler
* :class:`.PointSelectionHandler`: point selection handler
* :class:`.ZoomRectHandler`: zoom rectangle handler
Reference
^^^^^^^^^
.. autoclass:: StatefulEventFilter
:members:
.. autoclass:: KeyEventMatch
:members:
.. autoclass:: StandardKeyMatch
:members:
.. autoclass:: MouseEventMatch
:members:
.. autoclass:: GestureEventMatch
:members:
.. autoclass:: DragHandler
:members:
.. autoclass:: ClickHandler
:members:
.. autoclass:: PanHandler
:members:
.. autoclass:: ZoomHandler
:members:
.. autoclass:: GestureHandler
:members:
.. autoclass:: PinchPanGestureHandler
:members:
.. autoclass:: MenuHandler
:members:
.. autoclass:: QtDragHandler
:members:
.. autoclass:: AutoZoomHandler
:members:
.. autoclass:: MoveHandler
:members:
.. autoclass:: ObjectHandler
:members:
.. autoclass:: RectangularSelectionHandler
:members:
.. autoclass:: PointSelectionHandler
:members:
.. autoclass:: ZoomRectHandler
:members:
"""
from __future__ import annotations
import weakref
from typing import TYPE_CHECKING, Any, Callable
import numpy as np
from qtpy import QtCore as QC
from qtpy import QtGui as QG
from qtpy import QtWidgets as QW
from plotpy.config import CONF
from plotpy.constants import SHAPE_Z_OFFSET
from plotpy.coords import axes_to_canvas, canvas_to_axes
from plotpy.items import AnnotatedPolygon, Marker, PolygonShape
if TYPE_CHECKING:
from qtpy.QtCore import QPoint
from qtpy.QtGui import QMouseEvent
from plotpy.plot.base import BasePlot
def buttons_to_str(buttons: int) -> str:
"""Conversion of Qt flags to string
Args:
buttons: Qt flags (e.g. `Qt.LeftButton`, `Qt.RightButton`, etc.)
Returns:
Human readable string for the button flags
"""
string = ""
if buttons & QC.Qt.LeftButton:
string += "L"
if buttons & QC.Qt.MidButton: # Do not use QC.Qt.MouseButton.MidButton (Qt6!)
string += "M"
if buttons & QC.Qt.RightButton:
string += "R"
return string
def evt_type_to_str(type: int) -> str:
"""Convert event type to human readable string
Args:
type: event type
Returns:
Human readable string for the event type
"""
if type == QC.QEvent.MouseButtonPress:
return "Mpress"
elif type == QC.QEvent.MouseButtonRelease:
return "Mrelease"
elif type == QC.QEvent.MouseMove:
return "Mmove"
elif type == QC.QEvent.ContextMenu:
return "Context"
return f"{type:d}"
# MARK: Event handlers -----------------------------------------------------------------
class EventMatch:
"""A callable returning true if it matches an event"""
def __call__(self, event: QC.QEvent) -> bool:
"""Returns True if the event matches the event match
Args:
event: event to match
"""
raise NotImplementedError
def get_event_types(self) -> frozenset[int]:
"""Returns a set of event types handled by this
EventMatch.
This is used to quickly optimize events not handled by any event matchers
"""
return frozenset()
[docs]
class KeyEventMatch(EventMatch):
"""
A callable returning True if it matches a key event
Args:
keys: list of keys or couples (key, modifier)
"""
def __init__(self, keys: list[int | tuple[int, int]]) -> None:
super().__init__()
key_list, mod_list = [], []
for item in keys:
if isinstance(item, (tuple, list)):
k, m = item
else:
k = item
# Avoid bad arguments: modifier instead of key
assert k not in (
QC.Qt.ControlModifier,
QC.Qt.ShiftModifier,
QC.Qt.AltModifier,
QC.Qt.NoModifier,
)
m = QC.Qt.NoModifier
key_list.append(k)
mod_list.append(m)
self.keys = key_list
self.mods = mod_list
[docs]
def get_event_types(self) -> frozenset[int]:
"""Return the set of event types handled by this event match"""
return frozenset((QC.QEvent.KeyPress,))
def __call__(self, event: QG.QKeyEvent) -> bool:
"""Returns True if the event matches the event match
Args:
event: event to match
"""
if event.type() == QC.QEvent.KeyPress:
my_key = event.key()
my_mod = event.modifiers()
if my_key in self.keys:
mod = self.mods[self.keys.index(my_key)]
if mod == QC.Qt.NoModifier or my_mod & mod:
return True
return False
[docs]
class StandardKeyMatch(EventMatch):
"""
A callable returning True if it matches a key event
Args:
keysequence: QKeySequence.StandardKey integer
"""
def __init__(self, keysequence: int) -> None:
super().__init__()
assert isinstance(keysequence, (int, QG.QKeySequence.StandardKey))
self.keyseq = keysequence
[docs]
def get_event_types(self) -> frozenset[int]:
"""Return the set of event types handled by this event match"""
return frozenset((QC.QEvent.KeyPress,))
def __call__(self, event: QG.QKeyEvent) -> bool:
"""Returns True if the event matches the event match
Args:
event: event to match
"""
return event.type() == QC.QEvent.KeyPress and event.matches(self.keyseq)
[docs]
class MouseEventMatch(EventMatch):
"""Base class for matching mouse events
Args:
evt_type: event type
btn: button to match
modifiers: keyboard modifiers to match
"""
def __init__(
self, evt_type: int, btn: int, modifiers: int = QC.Qt.NoModifier
) -> None:
super().__init__()
assert isinstance(modifiers, (int, QC.Qt.KeyboardModifier))
self.evt_type = evt_type
self.button = btn
self.modifiers = modifiers
[docs]
def get_event_types(self) -> frozenset[int]:
"""Return the set of event types handled by this event match"""
return frozenset((self.evt_type,))
def __call__(self, event: QMouseEvent) -> bool:
"""Returns True if the event matches the event match
Args:
event: event to match
"""
if event.type() == self.evt_type:
if event.button() == self.button:
if self.modifiers != QC.Qt.NoModifier:
if (event.modifiers() & self.modifiers) == self.modifiers:
return True
elif event.modifiers() == QC.Qt.NoModifier:
return True
return False
def __repr__(self) -> str:
"""Return textual representation"""
return "<MouseMatch: {}/ {:08x}:{}>".format(
evt_type_to_str(self.evt_type), self.modifiers, buttons_to_str(self.button)
)
class MouseMoveMatch(MouseEventMatch):
def __call__(self, event: QMouseEvent) -> bool:
"""Returns True if the event matches the event match
Args:
event: event to match
"""
if event.type() == self.evt_type:
if (
self.button != QC.Qt.NoButton
and (event.buttons() & self.button == self.button)
) or event.buttons() == QC.Qt.NoButton:
if self.modifiers != QC.Qt.NoModifier:
if (event.modifiers() & self.modifiers) == self.modifiers:
return True
elif event.modifiers() == QC.Qt.NoModifier:
return True
return False
[docs]
class GestureEventMatch(EventMatch):
"""Base class for matching gesture events"""
def __init__(self, gesture_type: int, gesture_state: int) -> None:
super().__init__()
self.gesture_type = gesture_type
self.gesture_state = gesture_state
@staticmethod
def __get_type_str(gesture_type: int) -> str:
"""Return text representation for gesture type"""
for attr in (
"TapGesture",
"TapAndHoldGesture",
"PanGesture",
"PinchGesture",
"SwipeGesture",
"CustomGesture",
):
if gesture_type == getattr(QC.Qt, attr):
return attr
@staticmethod
def __get_state_str(gesture_state: int) -> str:
"""Return text representation for gesture state"""
for attr in (
"GestureStarted",
"GestureUpdated",
"GestureFinished",
"GestureCanceled",
):
if gesture_state == getattr(QC.Qt, attr):
return attr
[docs]
def get_event_types(self) -> frozenset[int]:
"""Return the set of event types handled by this event match"""
return frozenset((QC.QEvent.Gesture,))
def __call__(self, event: QC.QEvent) -> bool:
"""Returns True if the event matches the event match
Args:
event: event to match
"""
if event.type() == QC.QEvent.Gesture:
gesture = event.gesture(self.gesture_type)
return gesture and gesture.state() == self.gesture_state
return False
def __repr__(self) -> str:
"""Return textual representation"""
type_str = self.__get_type_str(self.gesture_type)
state_str = self.__get_state_str(self.gesture_state)
return "<GestureMatch: %s:%s>" % (type_str, state_str)
class WheelEventMatch(EventMatch):
"""A callable returning True if it matches a wheel event
Args:
modifiers: keyboard modifiers
"""
def __init__(self, modifiers: int = QC.Qt.KeyboardModifier.NoModifier) -> None:
super().__init__()
self.modifiers = modifiers
def get_event_types(self) -> frozenset[int]:
"""Return the set of event types handled by this event match"""
return frozenset((QC.QEvent.Type.Wheel,))
def __call__(self, event: QG.QWheelEvent) -> bool:
"""Returns True if the event matches the event match
Args:
event: event to match
"""
return isinstance(event, QG.QWheelEvent) and event.modifiers() == self.modifiers
# MARK: Finite state machine -----------------------------------------------------------
[docs]
class StatefulEventFilter(QC.QObject):
"""State machine for handling events of a plot's canvas
Args:
parent: plot on which to install the event filter
"""
def __init__(self, parent: BasePlot) -> None:
super().__init__()
# Machine states: (0: cursor, 1: panning, 2: zooming)
self.states: dict[int, dict[EventMatch, list[list[callable], int]]] = {0: {}}
self.cursors: dict[int, QC.Qt.CursorShape] = {}
self.state = 0
self.max_state = 0
self.events: dict[tuple[str, int, int], EventMatch] = {}
self.plot: BasePlot = parent
self.all_event_types = frozenset()
[docs]
def eventFilter(self, _obj: QC.QObject, event: QC.QEvent) -> bool:
"""The `eventfilter` callback for Qt"""
if not hasattr(self, "all_event_types"):
print(repr(self), self)
if event.type() not in self.all_event_types:
return False
state = self.states[self.state]
for match, (call_list, next_state) in list(state.items()):
if match(event):
self.set_state(next_state, event)
for call in call_list:
call(self, event) # might change state
return False
[docs]
def set_state(self, state: int, event: QC.QEvent) -> None:
"""Change the current state.
Can be called by handlers to cancel a state change
Args:
state: new state
event: event that triggered the state change
"""
assert state in self.states
if state == self.state:
return
self.state = state
cursor = self.get_cursor(event)
if cursor is not None:
self.plot.canvas().setCursor(cursor)
[docs]
def new_state(self) -> int:
"""Create (reserve) a new state number
Returns:
New state number
"""
self.max_state += 1
self.states[self.max_state] = {}
return self.max_state
[docs]
def add_event(
self, state: int, match: EventMatch, call: callable, next_state: int = None
) -> int:
"""Add a transition to the state machine.
If next_state is provided, it must correspond to an existing state,
otherwise a new destination state is created
Args:
state: current state
match: event matcher
call: callable to execute when the event matches
next_state: next state to go to when the event matches
Returns:
Next state number
"""
assert isinstance(state, int)
assert isinstance(match, EventMatch)
self.all_event_types = self.all_event_types.union(match.get_event_types())
entry = self.states[state].setdefault(match, [[], None])
entry[0].append(call)
if entry[1] is None:
if next_state is None:
next_state = self.new_state()
else:
pass
else:
if next_state is not None:
assert next_state == entry[1]
else:
next_state = entry[1]
entry[1] = next_state
return next_state
[docs]
def nothing(self, filter: StatefulEventFilter, event: QC.QEvent) -> None:
"""A nothing filter, provided to help removing duplicate handlers
Args:
filter: event filter that contains the BasePlot instance
event: event that triggered the action
"""
pass
# Cursor management ----------------------------------------------------------------
[docs]
def set_cursor(self, cursor: QC.Qt.CursorShape, *states: int) -> None:
"""Associate a cursor with one or more states
Args:
cursor: cursor shape
states: state numbers
"""
assert isinstance(cursor, QC.Qt.CursorShape)
for s in states:
self.cursors[s] = cursor
[docs]
def get_cursor(self, _event: QC.QEvent) -> QC.Qt.CursorShape | None:
"""Get the cursor associated with a given state/event
Returns:
Cursor shape or None if no cursor is associated with the state
"""
# pass event to potentially choose the cursor based on modifier keys
cursor = self.cursors.get(self.state, None)
if cursor is None:
# no cursor specified : should keep previous one
return None
return cursor
# Mouse management -----------------------------------------------------------------
[docs]
def mouse_press(
self, btn: int, modifiers: int = QC.Qt.NoModifier
) -> MouseEventMatch:
"""Create a filter for the MousePress event
Args:
btn: button to match
modifiers: keyboard modifiers to match
Returns:
An instance of MouseEventMatch that matches the MousePress event
"""
return self.events.setdefault(
("mousepress", btn, modifiers),
MouseEventMatch(QC.QEvent.MouseButtonPress, btn, modifiers),
)
[docs]
def mouse_move(
self, btn: int, modifiers: int = QC.Qt.NoModifier
) -> MouseEventMatch:
"""Create a filter for the MouseMove event
Args:
btn: button to match
modifiers: keyboard modifiers to match
Returns:
An instance of MouseEventMatch that matches the MouseMove event
"""
return self.events.setdefault(
("mousemove", btn, modifiers),
MouseMoveMatch(QC.QEvent.MouseMove, btn, modifiers),
)
[docs]
def mouse_release(
self, btn: int, modifiers: int = QC.Qt.NoModifier
) -> MouseEventMatch:
"""Create a filter for the MouseRelease event
Args:
btn: The button to match
modifiers: The keyboard modifiers to match
Returns:
An instance of MouseEventMatch that matches the MouseRelease event
"""
return self.events.setdefault(
("mouserelease", btn, modifiers),
MouseEventMatch(QC.QEvent.MouseButtonRelease, btn, modifiers),
)
# Gesture management ---------------------------------------------------------------
[docs]
def gesture(self, kind: int, state: int) -> GestureEventMatch:
"""Create a filter for the gesture event
Args:
kind: The type of gesture
state: The state of the gesture
Returns:
An instance of GestureEventMatch that matches the gesture event
"""
return self.events.setdefault(
("gesture", kind, state), GestureEventMatch(kind, state)
)
# Wheel management -----------------------------------------------------------------
[docs]
def wheel(
self, modifiers: int = QC.Qt.KeyboardModifier.NoModifier
) -> WheelEventMatch:
"""Create a filter for wheel events
Args:
modifiers: The keyboard modifiers to use with the wheel event
Returns:
An instance of WheelEventMatch that matches the wheel event"""
return self.events.setdefault(("wheel", modifiers), WheelEventMatch(modifiers))
# MARK: Event handlers -----------------------------------------------------------------
[docs]
class DragHandler(QC.QObject):
"""Base class for click-drag-release event handlers.
Args:
filter: The StatefulEventFilter instance.
btn: The mouse button to match.
mods: The keyboard modifiers to match. (default: QC.Qt.NoModifier)
start_state: The starting state. (default: 0)
"""
cursor = None
def __init__(
self,
filter: StatefulEventFilter,
btn: int,
mods: int = QC.Qt.NoModifier,
start_state: int = 0,
) -> None:
super().__init__()
self.state0 = filter.add_event(
start_state, filter.mouse_press(btn, mods), self.start_tracking
)
self.state1 = filter.add_event(
self.state0, filter.mouse_move(btn, mods), self.start_moving
)
filter.add_event(
self.state1, filter.mouse_move(btn, mods), self.move, self.state1
)
filter.add_event(
self.state0,
filter.mouse_release(btn, mods),
self.stop_notmoving,
start_state,
)
filter.add_event(
self.state1, filter.mouse_release(btn, mods), self.stop_moving, start_state
)
if self.cursor is not None:
filter.set_cursor(self.cursor, self.state0, self.state1)
self.start = None # first mouse position
self.last = None # mouse position seen during last event
self.parent_tracking = None
[docs]
def get_move_state(
self, filter: StatefulEventFilter, pos: QC.QPointF
) -> tuple[float, float]:
"""Get the movement state based on the current filter and position
Args:
filter: The StatefulEventFilter instance
pos: The current position
Returns:
A tuple containing the movement state in the x and y directions
"""
rct = filter.plot.contentsRect()
dx = (pos.x(), self.last.x(), self.start.x(), rct.width())
dy = (pos.y(), self.last.y(), self.start.y(), rct.height())
self.last = QC.QPointF(pos)
return dx, dy
[docs]
def start_tracking(self, _filter: StatefulEventFilter, event: QMouseEvent) -> None:
"""Start tracking the mouse movement
Args:
_filter: The StatefulEventFilter instance
event: The mouse event
"""
self.start = self.last = QC.QPointF(event.pos())
[docs]
def start_moving(self, filter: StatefulEventFilter, event: QMouseEvent) -> None:
"""Start moving the mouse
Args:
filter: The StatefulEventFilter instance
event: The mouse event
"""
return self.move(filter, event)
[docs]
def stop_tracking(self, _filter: StatefulEventFilter, _event: QMouseEvent) -> None:
"""Stop tracking the mouse movement
Args:
_filter: The StatefulEventFilter instance
_event: The mouse event
"""
pass
# filter.plot.canvas().setMouseTracking(self.parent_tracking)
[docs]
def stop_notmoving(self, filter: StatefulEventFilter, event: QMouseEvent) -> None:
"""Stop tracking the mouse movement when the mouse is not moving.
Args:
filter: The StatefulEventFilter instance.
event: The mouse event.
"""
self.stop_tracking(filter, event)
[docs]
def stop_moving(self, filter: StatefulEventFilter, event: QMouseEvent) -> None:
"""Stop moving the mouse
Args:
filter: The StatefulEventFilter instance
event: The mouse event
"""
self.stop_tracking(filter, event)
[docs]
def move(self, filter: StatefulEventFilter, event: QMouseEvent) -> None:
"""Move the mouse
Args:
filter: The StatefulEventFilter instance
event: The mouse event
"""
raise NotImplementedError()
[docs]
class ClickHandler(QC.QObject):
"""Base class for click-release event handlers.
Args:
filter: The StatefulEventFilter instance.
btn: The mouse button to match.
mods: The keyboard modifiers to match. (default: QC.Qt.NoModifier)
start_state: The starting state. (default: 0)
Signals:
SIG_CLICK_EVENT: Signal emitted by ClickHandler on mouse click
"""
#: Signal emitted by ClickHandler on mouse click
SIG_CLICK_EVENT = QC.Signal(object, object)
def __init__(
self,
filter: StatefulEventFilter,
btn: int,
mods: int = QC.Qt.NoModifier,
start_state: int = 0,
) -> None:
super().__init__()
self.state0 = filter.add_event(
start_state, filter.mouse_press(btn, mods), filter.nothing
)
filter.add_event(
self.state0, filter.mouse_release(btn, mods), self.click, start_state
)
[docs]
def click(self, filter: StatefulEventFilter, event: QC.QEvent) -> None:
"""Handle the click event.
Args:
filter: The StatefulEventFilter instance.
event: The mouse event.
"""
self.SIG_CLICK_EVENT.emit(filter, event)
[docs]
class PanHandler(DragHandler):
cursor = QC.Qt.ClosedHandCursor
[docs]
def move(self, filter: StatefulEventFilter, event: QMouseEvent) -> None:
"""Move the mouse.
Args:
filter: The StatefulEventFilter instance.
event: The mouse event.
"""
x_state, y_state = self.get_move_state(filter, QC.QPointF(event.pos()))
filter.plot.do_pan_view(x_state, y_state)
[docs]
class ZoomHandler(DragHandler):
cursor = QC.Qt.SizeAllCursor
[docs]
def move(self, filter: StatefulEventFilter, event: QMouseEvent) -> None:
"""Move the mouse.
Args:
filter: The StatefulEventFilter instance.
event: The mouse event.
"""
x_state, y_state = self.get_move_state(filter, QC.QPointF(event.pos()))
filter.plot.do_zoom_view(x_state, y_state)
[docs]
class GestureHandler(QC.QObject):
"""Base class used to handle gestures.
Args:
filter: event filter into which to add the handler instance.
start_state: start state to use in the event filter state machine.
Defaults to 0.
"""
kind: QC.Qt.GestureType | None = None
def __init__(self, filter: StatefulEventFilter, start_state: int = 0) -> None:
super().__init__()
filter.plot.canvas().grabGesture(self.kind)
self.state0 = filter.add_event(
start_state,
filter.gesture(self.kind, QC.Qt.GestureState.GestureStarted),
self.start_tracking,
)
self.state1 = filter.add_event(
self.state0,
filter.gesture(self.kind, QC.Qt.GestureState.GestureUpdated),
self.start_moving,
)
filter.add_event(
self.state1,
filter.gesture(self.kind, QC.Qt.GestureState.GestureUpdated),
self.move,
self.state1,
)
filter.add_event(
self.state0,
filter.gesture(self.kind, QC.Qt.GestureState.GestureFinished),
self.stop_tracking,
start_state,
)
filter.add_event(
self.state1,
filter.gesture(self.kind, QC.Qt.GestureState.GestureFinished),
self.stop_tracking,
start_state,
)
filter.add_event(
self.state0,
filter.gesture(self.kind, QC.Qt.GestureState.GestureCanceled),
self.stop_tracking,
start_state,
)
filter.add_event(
self.state1,
filter.gesture(self.kind, QC.Qt.GestureState.GestureCanceled),
self.stop_tracking,
start_state,
)
self.start = QC.QPoint() # first gesture position
self.last = QC.QPoint() # gesture position seen during last event
self.parent_tracking = None
[docs]
def get_glob_position(self, event: QW.QGestureEvent) -> QC.QPointF:
"""Returns the hotspot global position of the gesture event.
Args:
event: event from which to get the position
Returns:
event hotspot poisition in global coordinates.
"""
return self.get_gesture(event).hotSpot()
[docs]
def get_gesture(self, event: QW.QGestureEvent) -> QW.QGesture:
"""Returns the gesture from the event.
Args:
event: event from which to get the gesture.
Returns:
gesture from the event.
"""
return event.gesture(self.kind)
[docs]
def start_tracking(
self, filter: StatefulEventFilter, event: QW.QGestureEvent
) -> None:
"""Handles the start of the gesture tracking.
Args:
filter: event filter that contains the BasePlot instance
event: event that triggered the start of the tracking, used to get
a position.
"""
origin = self.get_glob_position(event)
self.start = self.last = filter.plot.canvas().mapFromGlobal(origin.toPoint())
[docs]
def start_moving(
self, filter: StatefulEventFilter, event: QW.QGestureEvent
) -> None:
"""Handles the start of the gesture moving.
Args:
filter: event filter that contains the BasePlot instance
event: event that triggered the start of the moving
"""
return self.move(filter, event)
[docs]
def stop_tracking(
self, _filter: StatefulEventFilter, _event: QW.QGestureEvent
) -> None:
"""Handles the stop of the gesture tracking.
Args:
_filter: event filter that contains the BasePlot instance
_event: event that triggered the stop of the tracking
"""
pass
[docs]
def move(self, filter: StatefulEventFilter, event: QW.QGestureEvent) -> None:
"""Handles the movement of the gesture.
Args:
filter: event filter that contains the BasePlot instance
event: event that triggered the movement
"""
raise NotImplementedError
[docs]
class PinchPanGestureHandler(GestureHandler):
"""Class used to handle pinch and pan gestures.
Args:
filter: event filter into which to add the handler instance.
start_state: start state to use in the event filter state machine.
Defaults to 0.
"""
kind = QC.Qt.GestureType.PinchGesture
def __init__(
self,
filter: StatefulEventFilter,
start_state=0,
) -> None:
super().__init__(filter, start_state)
self.last_center_diff = 0
self.marker: Marker | None = None
[docs]
def get_pan_param(
self, plot: BasePlot, pos: QPoint
) -> tuple[tuple[float, float, float, float], tuple[float, float, float, float]]:
"""Returns the parameters to use for panning the plot.
Args:
plot: instance of BasePlot to use as a reference.
pos: position on the plot canvas of the current hotspot.
Returns:
Returns two tuples of four floats each, representing the parameters used
by BasePlot.do_pan_view.
"""
rct = plot.contentsRect()
dx = (pos.x(), self.last.x(), self.start.x(), rct.width())
dy = (pos.y(), self.last.y(), self.start.y(), rct.height())
self.last = pos
return dx, dy
[docs]
def get_zoom_param(
self, plot: BasePlot, pos: QPoint, factor: float
) -> tuple[tuple[float, float, float, float], tuple[float, float, float, float]]:
"""Returns the parameters to use for zooming on the plot.
Args:
plot: instance of BasePlot to use as a reference.
pos: position on the plot canvas of the current hotspot.
factor: factor by which to zoom (zero-centered).
Returns:
Returns two tuples of four floats each, representing the parameters used
by BasePlot.do_zoom_view.
"""
rect = plot.contentsRect()
w, h = rect.width(), rect.height()
x, y = pos.x(), pos.y()
dx = (
x + (w * factor),
x,
x,
w,
)
dy = (
y + (h * factor),
y,
y,
h,
)
return dx, dy
[docs]
def start_tracking(
self, filter: StatefulEventFilter, event: QW.QGestureEvent
) -> None:
"""Overrides the GestureHandler.start_tracking method to add a marker at the
hostpot of the pinch gesture.
Args:
filter: event filter that contains the BasePlot instance
event: Gesture event that triggered the start of the tracking, used to get
a position.
"""
plot = filter.plot
if self.marker is None:
self.marker = Marker()
self.marker.attach(plot)
self.marker.setZ(plot.get_max_z() + 1)
self.marker.setVisible(True)
pos = plot.canvas().mapFromGlobal(self.get_glob_position(event).toPoint())
self.marker.move_local_point_to(0, pos)
return super().start_tracking(filter, event)
[docs]
def move(self, filter: StatefulEventFilter, event: QW.QGestureEvent) -> None:
"""Overrides the GestureHandler.move method to handle the pinch and pan gesture.
Args:
filter: event filter that contains the BasePlot instance
event: event that triggered the move, used to get the hotspot position.
"""
gesture: QW.QPinchGesture = self.get_gesture(event)
plot = filter.plot
center_point = self.get_glob_position(event)
center_point = filter.plot.canvas().mapFromGlobal(center_point.toPoint())
scale_factor = np.clip(gesture.scaleFactor(), 0.95, 1.05) - 1
pan_dx, pan_dy = self.get_pan_param(plot, center_point)
zoom_dx, zoom_dy = self.get_zoom_param(plot, center_point, scale_factor)
plot.do_pan_view(pan_dx, pan_dy, replot=True)
plot.do_zoom_view(zoom_dx, zoom_dy, lock_aspect_ratio=True)
[docs]
def stop_tracking(
self, _filter: StatefulEventFilter, _event: QW.QGestureEvent
) -> None:
"""Overrides the GestureHandler.stop_tracking method to remove the marker when
the gesture tracking stops.
Args:
_filter: event filter that contains the BasePlot instance
_event: event that triggered the stop, not used.
"""
if self.marker is not None:
self.marker.detach()
self.marker = None
[docs]
class MenuHandler(ClickHandler):
"""Class to handle context menu events."""
[docs]
def click(self, filter: StatefulEventFilter, event: QC.QEvent) -> None:
"""
Handles the click event.
Args:
filter: The StatefulEventFilter instance.
event: The QC.QEvent instance.
"""
menu = filter.plot.get_context_menu()
if menu:
menu.popup(event.globalPos())
class WheelHandler(QC.QObject):
"""Base class for handling mouse wheel events.
Args:
filter: event filter into which to add the handler instance.
mods: Keyboard modifier that can be used at the same time as the wheel to
trigger the event. Defaults to QC.Qt.KeyboardModifier.NoModifier.
start_state: start state to use in the event filter state machine.
Defaults to 0."""
def __init__(
self,
filter: StatefulEventFilter,
mods=QC.Qt.KeyboardModifier.NoModifier,
start_state=0,
) -> None:
super().__init__()
self.state0 = filter.add_event(
start_state, filter.wheel(mods), self.wheel, start_state
)
def wheel(self, filter: StatefulEventFilter, event: QG.QWheelEvent) -> None:
"""Handles the wheel event.
Args:
filter: event filter that contains the BasePlot instance
event: the wheel event that triggered the action."""
raise NotImplementedError()
class WheelZoomHandler(WheelHandler):
"""Class to handle zooming with the mouse wheel."""
def get_zoom_param(
self, plot: BasePlot, pos: QPoint, factor: float
) -> tuple[tuple[float, float, float, float], tuple[float, float, float, float]]:
"""Returns the parameters to use for zooming on the plot.
Args:
plot: instance of BasePlot to use as a reference.
pos: position on the plot canvas of the current hotspot.
factor: factor by which to zoom (zero-centered).
Returns:
Returns two tuples of four floats each, representing the parameters used
by BasePlot.do_zoom_view.
"""
x, y = pos.x(), pos.y()
rect = plot.contentsRect()
w, h = rect.width(), rect.height()
dx = (
x + (w * factor),
x,
x,
w,
)
dy = (
y + (h * factor),
y,
y,
h,
)
return dx, dy
def wheel(self, filter: StatefulEventFilter, event: QG.QWheelEvent) -> None:
"""Overrides the WheelHandler.wheel method to handle the zooming with the mouse
wheel.
Args:
filter: event filter that contains the BasePlot instance
event: the wheel event that triggered the zooming. Use to get zoom factor
and position
"""
plot = filter.plot
center_point = event.globalPos()
center_point = filter.plot.canvas().mapFromGlobal(center_point) # type: ignore
zoom_factor = (event.angleDelta().y() / 120) * 0.08
dx, dy = self.get_zoom_param(plot, center_point, zoom_factor)
plot.do_zoom_view(dx, dy, lock_aspect_ratio=True)
[docs]
class QtDragHandler(DragHandler):
"""Class to handle drag events using Qt signals."""
#: Signal emitted by QtDragHandler when starting tracking
SIG_START_TRACKING = QC.Signal(object, object)
#: Signal emitted by QtDragHandler when stopping tracking and not moving
SIG_STOP_NOT_MOVING = QC.Signal(object, object)
#: Signal emitted by QtDragHandler when stopping tracking and moving
SIG_STOP_MOVING = QC.Signal(object, object)
#: Signal emitted by QtDragHandler when moving
SIG_MOVE = QC.Signal(object, object)
[docs]
def start_tracking(self, filter: StatefulEventFilter, event: QMouseEvent) -> None:
"""Starts tracking the drag event.
Args:
filter: The StatefulEventFilter instance.
event: The QC.QEvent instance.
"""
DragHandler.start_tracking(self, filter, event)
self.SIG_START_TRACKING.emit(filter, event)
[docs]
def stop_notmoving(self, filter: StatefulEventFilter, event: QMouseEvent) -> None:
"""Stops tracking when the drag event is not moving.
Args:
filter: The StatefulEventFilter instance.
event: The QC.QEvent instance.
"""
self.SIG_STOP_NOT_MOVING.emit(filter, event)
[docs]
def stop_moving(self, filter: StatefulEventFilter, event: QMouseEvent) -> None:
"""Stops the movement of the drag event.
Args:
filter: The StatefulEventFilter instance.
event: The QC.QEvent instance
"""
self.SIG_STOP_MOVING.emit(filter, event)
[docs]
def move(self, filter: StatefulEventFilter, event: QMouseEvent) -> None:
"""Handles the move event.
Args:
filter: The StatefulEventFilter instance.
event: The QC.QEvent instance.
"""
self.SIG_MOVE.emit(filter, event)
[docs]
class AutoZoomHandler(ClickHandler):
"""Class to handle auto-zoom events."""
[docs]
def click(self, filter: StatefulEventFilter, _event: QC.QEvent) -> None:
"""Handles the click event.
Args:
filter: The StatefulEventFilter instance.
_event: The QC.QEvent instance.
"""
filter.plot.do_autoscale()
[docs]
class MoveHandler:
"""Class to handle moving events.
Args:
filter: The StatefulEventFilter instance.
btn: The mouse button to match.
mods: The keyboard modifiers to match. (default: QC.Qt.NoModifier)
start_state: The starting state. (default: 0)
"""
def __init__(
self,
filter: StatefulEventFilter,
btn: QC.Qt.MouseButton = QC.Qt.NoButton,
mods: QC.Qt.KeyboardModifiers = QC.Qt.NoModifier,
start_state: int = 0,
):
filter.add_event(
start_state, filter.mouse_move(btn, mods), self.move, start_state
)
[docs]
def move(self, filter: StatefulEventFilter, event: QW.QGestureEvent) -> None:
"""
Handles the move event.
Args:
filter: The StatefulEventFilter instance.
event: The QGestureEvent instance.
"""
filter.plot.do_move_marker(event)
class UndoMoveObject:
"""Class to handle undo/redo events for moving objects.
Args:
obj: The object to move.
pos1: The initial position.
pos2: The final position.
"""
def __init__(
self, obj: Any, pos1: tuple[float, float], pos2: tuple[float, float]
) -> None:
self.obj = obj
self.coords1 = canvas_to_axes(obj, pos1)
self.coords2 = canvas_to_axes(obj, pos2)
def is_valid(self) -> bool:
"""Check if the object is valid.
Returns:
True if the object is valid, False otherwise.
"""
return self.obj.plot() is not None
def compute_positions(self) -> tuple[QC.QPointF, QC.QPointF]:
"""Compute the positions of the object.
Returns:
A tuple containing the positions of the object.
"""
pos1 = QC.QPointF(*axes_to_canvas(self.obj, *self.coords1))
pos2 = QC.QPointF(*axes_to_canvas(self.obj, *self.coords2))
return pos1, pos2
def undo(self) -> None:
"""Undo the action."""
pos1, pos2 = self.compute_positions()
self.obj.plot().unselect_all()
self.obj.move_local_shape(pos1, pos2)
def redo(self) -> None:
"""Redo the action."""
pos1, pos2 = self.compute_positions()
self.obj.plot().unselect_all()
self.obj.move_local_shape(pos2, pos1)
class UndoMovePoint(UndoMoveObject):
"""Class to handle undo/redo events for moving points.
Args:
obj: The object to move.
pos1: The initial position.
pos2: The final position.
handle: The handle to move.
ctrl: The control point.
"""
def __init__(
self,
obj: Any,
pos1: tuple[float, float],
pos2: tuple[float, float],
handle: Any,
ctrl: Any,
) -> None:
super().__init__(obj, pos1, pos2)
self.handle = handle
self.ctrl = ctrl
def undo(self) -> None:
"""Undo the action."""
pos1, pos2 = self.compute_positions()
self.obj.move_local_point_to(self.handle, pos1, self.ctrl)
def redo(self) -> None:
"""Redo the action."""
pos1, pos2 = self.compute_positions()
self.obj.move_local_point_to(self.handle, pos2, self.ctrl)
class UndoRotatePoint(UndoMoveObject):
"""Class to handle undo/redo events for rotating points.
Args:
obj: The object to move.
pos1: The initial position.
pos2: The final position.
"""
def __init__(
self, obj: Any, pos1: tuple[float, float], pos2: tuple[float, float]
) -> None:
super().__init__(obj, pos1, pos2)
def undo(self) -> None:
"""Undo the action."""
pos1, pos2 = self.compute_positions()
self.obj.plot().unselect_all()
self.obj.rotate_local_shape(pos1, pos2)
def redo(self) -> None:
"""Redo the action."""
pos1, pos2 = self.compute_positions()
self.obj.plot().unselect_all()
self.obj.rotate_local_shape(pos2, pos1)
[docs]
class ObjectHandler:
"""Base class for handling objects.
Args:
filter: The StatefulEventFilter instance.
btn: The mouse button to match.
mods: The keyboard modifiers to match. (default: QC.Qt.NoModifier)
start_state: The starting state. (default: 0)
multiselection: Whether to allow multiple selections. (default: False)
"""
def __init__(
self,
filter: StatefulEventFilter,
btn: int,
mods: QC.Qt.KeyboardModifiers = QC.Qt.NoModifier,
start_state: int = 0,
multiselection: bool = False,
) -> None:
self.multiselection = multiselection
self.start_state = start_state
self.state0 = filter.add_event(
start_state, filter.mouse_press(btn, mods), self.start_tracking
)
filter.add_event(
self.state0, filter.mouse_move(btn, mods), self.move, self.state0
)
filter.add_event(
self.state0,
filter.mouse_release(btn, mods),
self.stop_tracking,
start_state,
)
filter.add_event(
start_state, StandardKeyMatch(QG.QKeySequence.Undo), self.undo, start_state
)
filter.add_event(
start_state, StandardKeyMatch(QG.QKeySequence.Redo), self.redo, start_state
)
self.handle = None # first mouse position
self.inside = False
self.rotate_inside = False # cas de la rotation avec un point à l'intérieur
self._active = None # mouse position seen during last event
self.last_pos = None
self.unselection_pending = None
self.undo_stack = [None]
self.undo_index = 0
self.first_pos = None
self.undo_action = None
@property
def active(self) -> Any:
"""Return active object"""
if self._active is not None:
return self._active()
@active.setter
def active(self, value: Any) -> None:
"""Set active object"""
if value is None:
self._active = None
else:
self._active = weakref.ref(value)
[docs]
def add_undo_move_action(self, undo_action: Any) -> None:
"""Add undo action to undo stack"""
self.undo_stack = self.undo_stack[: self.undo_index + 1]
self.undo_stack.append(undo_action)
self.undo_index = len(self.undo_stack) - 1
[docs]
def undo(self, filter: StatefulEventFilter, event: QC.QEvent) -> None:
"""Undo the last action.
Args:
filter: The StatefulEventFilter instance.
event: The event triggering the undo action.
"""
action = self.undo_stack[self.undo_index]
if action is not None:
if action.is_valid():
action.undo()
filter.plot.replot()
else:
self.undo_stack.remove(action)
self.undo_index -= 1
[docs]
def redo(self, filter: StatefulEventFilter, event: QC.QEvent) -> None:
"""Redo the last action.
Args:
filter: The StatefulEventFilter instance.
event: The event triggering the redo action.
"""
if self.undo_index < len(self.undo_stack) - 1:
action = self.undo_stack[self.undo_index + 1]
if action.is_valid():
action.redo()
filter.plot.replot()
self.undo_index += 1
else:
self.undo_stack.remove(action)
[docs]
def start_tracking(self, filter: StatefulEventFilter, event: QC.QEvent) -> None:
"""Starts tracking the mouse movement for selection and interaction.
Args:
filter: The filter object.
event: The mouse event.
"""
plot = filter.plot
self.inside = False
self.rotate_inside = False
self.active = None
self.handle = None
self.first_pos = pos = event.pos()
self.last_pos = QC.QPointF(pos)
selected = plot.get_active_item()
distance = CONF.get("plot", "selection/distance", 6)
(
nearest,
nearest_dist,
nearest_handle,
nearest_inside,
) = plot.get_nearest_object(pos, distance)
if nearest is not None:
# Is the nearest object the real deal?
if not nearest.can_select() or nearest_dist >= distance:
# Looking for the nearest object in z containing cursor position
(
nearest,
nearest_dist,
nearest_handle,
nearest_inside,
) = plot.get_nearest_object_in_z(pos)
# This will unselect active item only if it's not moved afterwards:
self.unselection_pending = selected is nearest
if selected and not self.multiselection:
# An item is selected
self.active = selected
(dist, self.handle, self.inside, other_object) = self.active.hit_test(pos)
if other_object is not None:
# e.g. LegendBoxItem: 'other_object' is the selected curve
plot.set_active_item(other_object)
return
if dist >= distance and not self.inside:
# The following allows to move together selected items by
# clicking inside any of them (instead of active item only)
other_selitems = [
_it
for _it in plot.get_selected_items()
if _it is not self.active and _it.can_move()
]
for selitem in other_selitems:
dist, handle, inside, _other = selitem.hit_test(pos)
if dist < distance or inside:
self.inside = inside
break
else:
# Check if user clicked on a different selectable object
if nearest is not None and nearest.can_select():
# Direct selection of a different object
plot.unselect_all()
plot.select_item(nearest)
plot.set_active_item(nearest)
self.active = nearest
self.handle = nearest_handle
self.inside = nearest_inside
# Update distance for the new object
dist = nearest_dist
# Clear unselection pending since we've switched to a new object
self.unselection_pending = False
# Continue to move_or_resize_object for proper validation
else:
# Clicked on empty space - unselect everything
self.__unselect_objects(filter)
filter.set_state(self.start_state, event)
return
else:
# No item is selected
self.active = nearest
self.handle = nearest_handle
self.inside = nearest_inside
dist = nearest_dist
if nearest is not None:
plot.set_active_item(nearest)
if not nearest.selected:
if not self.multiselection:
plot.unselect_all()
plot.select_item(nearest)
# Eventually move or resize selected object:
self.__move_or_resize_object(dist, distance, event, filter)
plot.replot()
def __unselect_objects(self, filter: StatefulEventFilter) -> None:
"""Unselect selected object*s*
Args:
filter: The StatefulEventFilter instance.
"""
plot = filter.plot
plot.unselect_all()
plot.replot()
def __move_or_resize_object(
self,
dist: float,
distance: float,
event: QC.QEvent,
filter: StatefulEventFilter,
) -> None:
"""Move or resize the object
Args:
dist: The distance.
distance: The distance.
event: The mouse event.
filter: The StatefulEventFilter instance.
"""
if (
self.active is not None
and self.active.can_move() is False
and self.inside
and self.active.can_rotate()
):
# Rotate inside
self.inside = False
self.rotate_inside = True
return
if (
dist < distance
and self.handle is not None
and (self.active.can_resize() or self.active.can_rotate())
):
# Resize / move handle
self.inside = False
return
if self.inside and self.active.can_move():
# Move object
return
# can't move, can't resize
filter.set_state(self.start_state, event)
if self.unselection_pending:
self.__unselect_objects(filter)
[docs]
def move(self, filter: StatefulEventFilter, event: QC.QEvent) -> None:
"""Move the object.
Args:
filter: The StatefulEventFilter instance.
event: The mouse event.
"""
if self.active is None:
return
self.unselection_pending = False
if self.rotate_inside:
self.active.rotate_local_shape(self.last_pos, event.pos())
self.undo_action = UndoRotatePoint(self.active, event.pos(), self.first_pos)
elif self.inside:
self.active.move_local_shape(self.last_pos, event.pos())
self.undo_action = UndoMoveObject(self.active, event.pos(), self.first_pos)
else:
ctrl = event.modifiers() & QC.Qt.ControlModifier == QC.Qt.ControlModifier
self.active.move_local_point_to(self.handle, event.pos(), ctrl)
self.undo_action = UndoMovePoint(
self.active, self.first_pos, event.pos(), self.handle, ctrl
)
self.last_pos = QC.QPointF(event.pos())
filter.plot.replot()
[docs]
def stop_tracking(self, filter: StatefulEventFilter, event: QC.QEvent) -> None:
"""Stop tracking the object.
Args:
filter: The StatefulEventFilter instance.
event: The mouse event.
"""
self.add_undo_move_action(self.undo_action)
if self.unselection_pending:
self.__unselect_objects(filter)
self.handle = None
self.active = None
self.inside = False
self.rotate_inside = False
[docs]
class RectangularSelectionHandler(DragHandler):
"""Base class for handling rectangular selections.
Args:
filter: The StatefulEventFilter instance.
btn: The mouse button to match.
mods: The keyboard modifiers to match. (default: QC.Qt.NoModifier)
start_state: The starting state. (default: 0)
"""
#: Signal emitted by RectangularSelectionHandler when ending selection
SIG_END_RECT = QC.Signal(object, object, object)
def __init__(
self,
filter: StatefulEventFilter,
btn: int,
mods: QC.Qt.KeyboardModifiers = QC.Qt.NoModifier,
start_state: int = 0,
) -> None:
super().__init__(filter, btn, mods, start_state)
self.avoid_null_shape = False
self.setup_shape_cb = None
self.shape = None
self.shape_h0 = None
self.shape_h1 = None
[docs]
def set_shape(
self,
shape: Any,
h0: Any,
h1: Any,
setup_shape_cb: Callable | None = None,
avoid_null_shape: bool = False,
) -> None:
"""Set the shape.
Args:
shape: The shape.
h0: The first handle.
h1: The second handle.
setup_shape_cb: The setup shape callback.
avoid_null_shape: Whether to avoid null shape.
"""
self.shape = shape
self.shape_h0 = h0
self.shape_h1 = h1
self.setup_shape_cb = setup_shape_cb
self.avoid_null_shape = avoid_null_shape
[docs]
def start_tracking(self, filter: StatefulEventFilter, event: QMouseEvent) -> None:
"""Start tracking the mouse movement for selection and interaction.
Args:
filter: The StatefulEventFilter instance.
event: The mouse event.
"""
self.start = self.last = QC.QPointF(event.pos())
[docs]
def start_moving(self, filter: StatefulEventFilter, event: QMouseEvent) -> None:
"""Start moving the object.
Args:
filter: The StatefulEventFilter instance.
event: The mouse event.
"""
self.shape.attach(filter.plot)
self.shape.setZ(filter.plot.get_max_z() + 1)
if self.avoid_null_shape:
self.start -= QC.QPointF(1, 1)
self.shape.move_local_point_to(self.shape_h0, self.start)
self.shape.move_local_point_to(self.shape_h1, QC.QPointF(event.pos()))
self.start_moving_action(filter, event)
if self.setup_shape_cb is not None:
self.setup_shape_cb(self.shape)
self.shape.show()
filter.plot.replot()
[docs]
def start_moving_action(
self, filter: StatefulEventFilter, event: QMouseEvent
) -> None:
"""Start moving the object.
Args:
filter: The StatefulEventFilter instance.
event: The mouse event.
"""
pass
[docs]
def move(self, filter: StatefulEventFilter, event: QMouseEvent) -> None:
"""Move the object.
Args:
filter: The StatefulEventFilter instance.
event: The mouse event.
"""
self.shape.move_local_point_to(self.shape_h1, QC.QPointF(event.pos()))
self.move_action(filter, event)
filter.plot.replot()
[docs]
def move_action(self, filter: StatefulEventFilter, event: QMouseEvent) -> None:
"""Move the object.
Args:
filter: The StatefulEventFilter instance.
event: The mouse event.
"""
pass
[docs]
def stop_moving(self, filter: StatefulEventFilter, event: QMouseEvent) -> None:
"""Stop moving the object.
Args:
filter: The StatefulEventFilter instance.
event: The mouse event.
"""
self.shape.detach()
self.stop_moving_action(filter, event)
filter.plot.replot()
[docs]
def stop_moving_action(
self, filter: StatefulEventFilter, event: QMouseEvent
) -> None:
"""Stop moving the object.
Args:
filter: The StatefulEventFilter instance.
event: The mouse event.
"""
self.SIG_END_RECT.emit(filter, self.start, QC.QPointF(event.pos()))
[docs]
class PointSelectionHandler(RectangularSelectionHandler):
"""Class to handle point selections."""
[docs]
def stop_notmoving(self, filter: StatefulEventFilter, event: QMouseEvent) -> None:
"""Stop the point selection when the point is not moving.
Args:
filter: The StatefulEventFilter instance.
event: The mouse event.
"""
self.stop_moving(filter, event)
[docs]
class ZoomRectHandler(RectangularSelectionHandler):
"""Class to handle zoom rectangles."""
[docs]
def stop_moving_action(
self, filter: StatefulEventFilter, event: QMouseEvent
) -> None:
"""Stop moving the object.
Args:
filter: The StatefulEventFilter instance.
event: The mouse event.
"""
filter.plot.do_zoom_rect_view(self.start, QC.QPointF(event.pos()))
class MultilineSelectionHandler(DragHandler):
"""
A handler for handling multi-line selections.
This handler extends the DragHandler to handle multi-line selections.
Args:
filter: The StatefulEventFilter instance.
btn: The mouse button to match.
mods: The keyboard modifiers to match. (default: QC.Qt.NoModifier)
start_state: The starting state. (default: 0)
closed: Whether the polygon should be closed. (default: False)
"""
SIG_END_POLYLINE = QC.Signal(object, np.ndarray)
def __init__(
self,
filter: StatefulEventFilter,
btn: int,
mods: QC.Qt.KeyboardModifiers = QC.Qt.NoModifier,
start_state: int = 0,
closed: bool = False,
) -> None:
super().__init__(filter, btn, mods, start_state)
self.closed = closed
filter.add_event(
start_state,
KeyEventMatch((QC.Qt.Key_Enter, QC.Qt.Key_Return, QC.Qt.Key_Space)),
self.accept_polygonshape,
start_state,
)
filter.add_event(
start_state,
KeyEventMatch((QC.Qt.Key_Backspace, QC.Qt.Key_Escape)),
self.cancel_point,
start_state,
)
self.init_pos: QC.QPointF | None = None
self.shape: PolygonShape | AnnotatedPolygon | None = None
self.current_handle: int | None = None
self.setup_shape_cb: Callable | None = None
def set_shape(
self,
shape: PolygonShape | AnnotatedPolygon,
setup_shape_cb: Callable | None = None,
) -> None:
"""Set the shape.
Args:
shape: The shape.
setup_shape_cb: The setup shape callback.
"""
self.shape = shape
self.setup_shape_cb = setup_shape_cb
def start_tracking(self, filter: StatefulEventFilter, event: QMouseEvent) -> None:
"""Starts tracking the drag event.
Args:
filter: The StatefulEventFilter instance.
event: The QC.QEvent instance.
"""
if self.init_pos is None:
self.init_pos = QC.QPointF(event.pos())
def start_moving(self, filter: StatefulEventFilter, event: QMouseEvent) -> None:
"""Start moving the object.
Args:
filter: The StatefulEventFilter instance.
event: The mouse event.
"""
if self.init_pos is None:
return
if self.shape.plot() is None:
self.shape.set_points(None)
filter.plot.add_item_with_z_offset(self.shape, SHAPE_Z_OFFSET)
self.shape.setZ(filter.plot.get_max_z() + 1)
if self.setup_shape_cb is not None:
self.setup_shape_cb(self.shape)
self.shape.add_local_point(self.init_pos)
self.current_handle = self.shape.add_local_point(QC.QPointF(event.pos()))
self.shape.set_closed(self.closed and len(self.shape.get_points()) > 2)
self.shape.show()
filter.plot.replot()
def move(self, filter: StatefulEventFilter, event: QMouseEvent) -> None:
"""
Handle mouse move event to update the position of the last point.
Args:
filter: The plot filter.
event: The mouse event.
"""
self.shape.move_local_point_to(self.current_handle, QC.QPointF(event.pos()))
filter.plot.replot()
def stop_notmoving(self, filter: StatefulEventFilter, event: QMouseEvent) -> None:
"""Stops tracking when the drag event is not moving.
Args:
filter: The StatefulEventFilter instance.
event: The QC.QEvent instance.
"""
points = self.shape.get_points()
if len(points) > 1:
self.accept_polygonshape(filter, event)
def stop_moving(self, filter: StatefulEventFilter, event: QMouseEvent) -> None:
"""Stops the movement of the drag event.
Args:
filter: The StatefulEventFilter instance.
event: The QC.QEvent instance
"""
pos = QC.QPointF(event.pos())
if self.init_pos == pos:
self.shape.del_point(-1)
else:
self.shape.move_local_point_to(self.current_handle, pos)
filter.plot.replot()
def accept_polygonshape(
self, filter: StatefulEventFilter, event: QMouseEvent
) -> None:
"""Accept the polygon shape.
Args:
filter: The StatefulEventFilter instance.
event: The mouse event.
"""
if self.shape.plot() is None:
return
filter.plot.del_item(self.shape)
self.init_pos = None
self.current_handle = None
self.SIG_END_POLYLINE.emit(filter, self.shape.get_points())
def cancel_point(self, filter: StatefulEventFilter, event: QMouseEvent) -> None:
"""
Cancel the last point or remove the shape if it has less than 3 points.
Args:
filter: The plot filter.
event: The triggering event.
"""
points = self.shape.get_points()
if points is None or self.shape.plot() is None:
return
if len(points) <= 2:
filter.plot.del_item(self.shape)
self.init_pos = None
self.shape.closed = False
else:
if self.current_handle:
newh = self.shape.del_point(self.current_handle)
else:
newh = self.shape.del_point(-1)
self.current_handle = newh
self.shape.set_closed(self.closed and len(self.shape.get_points()) > 2)
filter.plot.replot()
def setup_standard_tool_filter(filter: StatefulEventFilter, start_state: int) -> int:
"""Creation of standard filters (pan/zoom) on middle/right buttons
Args:
filter: The StatefulEventFilter instance.
start_state: The starting state.
Returns:
The starting state.
"""
# Middle button
# Do not use QC.Qt.MouseButton.MidButton (Qt6!)
PanHandler(filter, QC.Qt.MidButton, start_state=start_state)
AutoZoomHandler(filter, QC.Qt.MidButton, start_state=start_state)
# Right button
ZoomHandler(filter, QC.Qt.MouseButton.RightButton, start_state=start_state)
MenuHandler(filter, QC.Qt.MouseButton.RightButton, start_state=start_state)
# Gestures
PinchPanGestureHandler(filter, start_state=start_state)
# Other events
MoveHandler(filter, start_state=start_state)
MoveHandler(
filter, start_state=start_state, mods=QC.Qt.KeyboardModifier.ShiftModifier
)
MoveHandler(
filter, start_state=start_state, mods=QC.Qt.KeyboardModifier.AltModifier
)
WheelZoomHandler(
filter, start_state=start_state, mods=QC.Qt.KeyboardModifier.ControlModifier
)
return start_state