diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 034a9b4db7a0..d10de28c106a 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -11,6 +11,7 @@ from contextlib import ExitStack import copy +import enum import itertools from numbers import Integral, Number @@ -150,6 +151,10 @@ def ignore(self, event): # docstring inherited return super().ignore(event) or self.canvas is None + def _set_cursor(self, cursor): + """Update the canvas cursor.""" + self.ax.get_figure(root=True).canvas.set_cursor(cursor) + class Button(AxesWidget): """ @@ -2643,7 +2648,7 @@ def _handles_artists(self): else: return () - def _set_cursor(self, enabled): + def _set_span_cursor(self, *, enabled): """Update the canvas cursor based on direction of the selector.""" if enabled: cursor = (backend_tools.Cursors.RESIZE_HORIZONTAL @@ -2652,7 +2657,7 @@ def _set_cursor(self, enabled): else: cursor = backend_tools.Cursors.POINTER - self.ax.get_figure(root=True).canvas.set_cursor(cursor) + self._set_cursor(cursor) def connect_default_events(self): # docstring inherited @@ -2662,7 +2667,7 @@ def connect_default_events(self): def _press(self, event): """Button press event handler.""" - self._set_cursor(True) + self._set_span_cursor(enabled=True) if self._interactive and self._selection_artist.get_visible(): self._set_active_handle(event) else: @@ -2712,7 +2717,7 @@ def direction(self, direction): def _release(self, event): """Button release event handler.""" - self._set_cursor(False) + self._set_span_cursor(enabled=False) if not self._interactive: self._selection_artist.set_visible(False) @@ -2754,7 +2759,7 @@ def _hover(self, event): return _, e_dist = self._edge_handles.closest(event.x, event.y) - self._set_cursor(e_dist <= self.grab_range) + self._set_span_cursor(enabled=e_dist <= self.grab_range) def _onmove(self, event): """Motion notify event handler.""" @@ -3145,6 +3150,13 @@ def onselect(eclick: MouseEvent, erelease: MouseEvent) """ +class _RectangleSelectorAction(enum.Enum): + ROTATE = enum.auto() + MOVE = enum.auto() + RESIZE = enum.auto() + CREATE = enum.auto() + + @_docstring.Substitution(_RECTANGLESELECTOR_PARAMETERS_DOCSTRING.replace( '__ARTIST_NAME__', 'rectangle')) class RectangleSelector(_SelectorWidget): @@ -3278,10 +3290,23 @@ def _press(self, event): self._rotation_on_press = self._rotation self._set_aspect_ratio_correction() + match self._get_action(): + case _RectangleSelectorAction.ROTATE: + # TODO: set to a rotate cursor if possible? + pass + case _RectangleSelectorAction.MOVE: + self._set_cursor(backend_tools.cursors.MOVE) + case _RectangleSelectorAction.RESIZE: + # TODO: set to a resize cursor if possible? + pass + case _RectangleSelectorAction.CREATE: + self._set_cursor(backend_tools.cursors.SELECT_REGION) + return False def _release(self, event): """Button release event handler.""" + self._set_cursor(backend_tools.Cursors.POINTER) if not self._interactive: self._selection_artist.set_visible(False) @@ -3325,9 +3350,20 @@ def _release(self, event): self.update() self._active_handle = None self._extents_on_press = None - return False + def _get_action(self): + state = self._state + if 'rotate' in state and self._active_handle in self._corner_order: + return _RectangleSelectorAction.ROTATE + elif self._active_handle == 'C': + return _RectangleSelectorAction.MOVE + elif self._active_handle: + return _RectangleSelectorAction.RESIZE + + return _RectangleSelectorAction.CREATE + + def _onmove(self, event): """ Motion notify event handler. @@ -3342,12 +3378,10 @@ def _onmove(self, event): # The calculations are done for rotation at zero: we apply inverse # transformation to events except when we rotate and move state = self._state - rotate = 'rotate' in state and self._active_handle in self._corner_order - move = self._active_handle == 'C' - resize = self._active_handle and not move + action = self._get_action() xdata, ydata = self._get_data_coords(event) - if resize: + if action == _RectangleSelectorAction.RESIZE: inv_tr = self._get_rotation_transform().inverted() xdata, ydata = inv_tr.transform([xdata, ydata]) eventpress.xdata, eventpress.ydata = inv_tr.transform( @@ -3367,7 +3401,7 @@ def _onmove(self, event): x0, x1, y0, y1 = self._extents_on_press # rotate an existing shape - if rotate: + if action == _RectangleSelectorAction.ROTATE: # calculate angle abc a = (eventpress.xdata, eventpress.ydata) b = self.center @@ -3376,7 +3410,7 @@ def _onmove(self, event): np.arctan2(a[1]-b[1], a[0]-b[0])) self.rotation = np.rad2deg(self._rotation_on_press + angle) - elif resize: + elif action == _RectangleSelectorAction.RESIZE: size_on_press = [x1 - x0, y1 - y0] center = (x0 + size_on_press[0] / 2, y0 + size_on_press[1] / 2) @@ -3427,7 +3461,7 @@ def _onmove(self, event): sign = np.sign(xdata - x0) x1 = x0 + sign * abs(y1 - y0) * self._aspect_ratio_correction - elif move: + elif action == _RectangleSelectorAction.MOVE: x0, x1, y0, y1 = self._extents_on_press dx = xdata - eventpress.xdata dy = ydata - eventpress.ydata diff --git a/lib/matplotlib/widgets.pyi b/lib/matplotlib/widgets.pyi index e143d0b2c96e..f74b9c7f32bf 100644 --- a/lib/matplotlib/widgets.pyi +++ b/lib/matplotlib/widgets.pyi @@ -6,6 +6,7 @@ from .figure import Figure from .lines import Line2D from .patches import Polygon, Rectangle from .text import Text +from .backend_tools import Cursors import PIL.Image @@ -38,6 +39,7 @@ class AxesWidget(Widget): def canvas(self) -> FigureCanvasBase | None: ... def connect_event(self, event: Event, callback: Callable) -> None: ... def disconnect_events(self) -> None: ... + def _set_cursor(self, cursor: Cursors) -> None: ... class Button(AxesWidget): label: Text @@ -335,6 +337,7 @@ class SpanSelector(_SelectorWidget): _props: dict[str, Any] | None = ..., _init: bool = ..., ) -> None: ... + def _set_span_cursor(self, *, enabled: bool) -> None: ... def connect_default_events(self) -> None: ... @property def direction(self) -> Literal["horizontal", "vertical"]: ... @@ -398,6 +401,7 @@ class RectangleSelector(_SelectorWidget): minspany: float spancoords: Literal["data", "pixels"] grab_range: float + _active_handle: None | Literal["C", "N", "NE", "E", "SE", "S", "SW", "W", "NW"] def __init__( self, ax: Axes,