Skip to content

Commit 8e2eb5d

Browse files
authored
Merge pull request matplotlib#30499 from dstansby/rect-select-cursor
Improve cursor icons with RectangleSelector
2 parents c3f423d + a3fafa6 commit 8e2eb5d

File tree

2 files changed

+51
-13
lines changed

2 files changed

+51
-13
lines changed

lib/matplotlib/widgets.py

Lines changed: 47 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
from contextlib import ExitStack
1313
import copy
14+
import enum
1415
import itertools
1516
from numbers import Integral, Number
1617

@@ -150,6 +151,10 @@ def ignore(self, event):
150151
# docstring inherited
151152
return super().ignore(event) or self.canvas is None
152153

154+
def _set_cursor(self, cursor):
155+
"""Update the canvas cursor."""
156+
self.ax.get_figure(root=True).canvas.set_cursor(cursor)
157+
153158

154159
class Button(AxesWidget):
155160
"""
@@ -2645,7 +2650,7 @@ def _handles_artists(self):
26452650
else:
26462651
return ()
26472652

2648-
def _set_cursor(self, enabled):
2653+
def _set_span_cursor(self, *, enabled):
26492654
"""Update the canvas cursor based on direction of the selector."""
26502655
if enabled:
26512656
cursor = (backend_tools.Cursors.RESIZE_HORIZONTAL
@@ -2654,7 +2659,7 @@ def _set_cursor(self, enabled):
26542659
else:
26552660
cursor = backend_tools.Cursors.POINTER
26562661

2657-
self.ax.get_figure(root=True).canvas.set_cursor(cursor)
2662+
self._set_cursor(cursor)
26582663

26592664
def connect_default_events(self):
26602665
# docstring inherited
@@ -2664,7 +2669,7 @@ def connect_default_events(self):
26642669

26652670
def _press(self, event):
26662671
"""Button press event handler."""
2667-
self._set_cursor(True)
2672+
self._set_span_cursor(enabled=True)
26682673
if self._interactive and self._selection_artist.get_visible():
26692674
self._set_active_handle(event)
26702675
else:
@@ -2714,7 +2719,7 @@ def direction(self, direction):
27142719

27152720
def _release(self, event):
27162721
"""Button release event handler."""
2717-
self._set_cursor(False)
2722+
self._set_span_cursor(enabled=False)
27182723

27192724
if not self._interactive:
27202725
self._selection_artist.set_visible(False)
@@ -2756,7 +2761,7 @@ def _hover(self, event):
27562761
return
27572762

27582763
_, e_dist = self._edge_handles.closest(event.x, event.y)
2759-
self._set_cursor(e_dist <= self.grab_range)
2764+
self._set_span_cursor(enabled=e_dist <= self.grab_range)
27602765

27612766
def _onmove(self, event):
27622767
"""Motion notify event handler."""
@@ -3147,6 +3152,13 @@ def onselect(eclick: MouseEvent, erelease: MouseEvent)
31473152
"""
31483153

31493154

3155+
class _RectangleSelectorAction(enum.Enum):
3156+
ROTATE = enum.auto()
3157+
MOVE = enum.auto()
3158+
RESIZE = enum.auto()
3159+
CREATE = enum.auto()
3160+
3161+
31503162
@_docstring.Substitution(_RECTANGLESELECTOR_PARAMETERS_DOCSTRING.replace(
31513163
'__ARTIST_NAME__', 'rectangle'))
31523164
class RectangleSelector(_SelectorWidget):
@@ -3280,10 +3292,23 @@ def _press(self, event):
32803292
self._rotation_on_press = self._rotation
32813293
self._set_aspect_ratio_correction()
32823294

3295+
match self._get_action():
3296+
case _RectangleSelectorAction.ROTATE:
3297+
# TODO: set to a rotate cursor if possible?
3298+
pass
3299+
case _RectangleSelectorAction.MOVE:
3300+
self._set_cursor(backend_tools.cursors.MOVE)
3301+
case _RectangleSelectorAction.RESIZE:
3302+
# TODO: set to a resize cursor if possible?
3303+
pass
3304+
case _RectangleSelectorAction.CREATE:
3305+
self._set_cursor(backend_tools.cursors.SELECT_REGION)
3306+
32833307
return False
32843308

32853309
def _release(self, event):
32863310
"""Button release event handler."""
3311+
self._set_cursor(backend_tools.Cursors.POINTER)
32873312
if not self._interactive:
32883313
self._selection_artist.set_visible(False)
32893314

@@ -3327,9 +3352,20 @@ def _release(self, event):
33273352
self.update()
33283353
self._active_handle = None
33293354
self._extents_on_press = None
3330-
33313355
return False
33323356

3357+
def _get_action(self):
3358+
state = self._state
3359+
if 'rotate' in state and self._active_handle in self._corner_order:
3360+
return _RectangleSelectorAction.ROTATE
3361+
elif self._active_handle == 'C':
3362+
return _RectangleSelectorAction.MOVE
3363+
elif self._active_handle:
3364+
return _RectangleSelectorAction.RESIZE
3365+
3366+
return _RectangleSelectorAction.CREATE
3367+
3368+
33333369
def _onmove(self, event):
33343370
"""
33353371
Motion notify event handler.
@@ -3344,12 +3380,10 @@ def _onmove(self, event):
33443380
# The calculations are done for rotation at zero: we apply inverse
33453381
# transformation to events except when we rotate and move
33463382
state = self._state
3347-
rotate = 'rotate' in state and self._active_handle in self._corner_order
3348-
move = self._active_handle == 'C'
3349-
resize = self._active_handle and not move
3383+
action = self._get_action()
33503384

33513385
xdata, ydata = self._get_data_coords(event)
3352-
if resize:
3386+
if action == _RectangleSelectorAction.RESIZE:
33533387
inv_tr = self._get_rotation_transform().inverted()
33543388
xdata, ydata = inv_tr.transform([xdata, ydata])
33553389
eventpress.xdata, eventpress.ydata = inv_tr.transform(
@@ -3369,7 +3403,7 @@ def _onmove(self, event):
33693403

33703404
x0, x1, y0, y1 = self._extents_on_press
33713405
# rotate an existing shape
3372-
if rotate:
3406+
if action == _RectangleSelectorAction.ROTATE:
33733407
# calculate angle abc
33743408
a = (eventpress.xdata, eventpress.ydata)
33753409
b = self.center
@@ -3378,7 +3412,7 @@ def _onmove(self, event):
33783412
np.arctan2(a[1]-b[1], a[0]-b[0]))
33793413
self.rotation = np.rad2deg(self._rotation_on_press + angle)
33803414

3381-
elif resize:
3415+
elif action == _RectangleSelectorAction.RESIZE:
33823416
size_on_press = [x1 - x0, y1 - y0]
33833417
center = (x0 + size_on_press[0] / 2, y0 + size_on_press[1] / 2)
33843418

@@ -3429,7 +3463,7 @@ def _onmove(self, event):
34293463
sign = np.sign(xdata - x0)
34303464
x1 = x0 + sign * abs(y1 - y0) * self._aspect_ratio_correction
34313465

3432-
elif move:
3466+
elif action == _RectangleSelectorAction.MOVE:
34333467
x0, x1, y0, y1 = self._extents_on_press
34343468
dx = xdata - eventpress.xdata
34353469
dy = ydata - eventpress.ydata

lib/matplotlib/widgets.pyi

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ from .figure import Figure
66
from .lines import Line2D
77
from .patches import Polygon, Rectangle
88
from .text import Text
9+
from .backend_tools import Cursors
910

1011
import PIL.Image
1112

@@ -38,6 +39,7 @@ class AxesWidget(Widget):
3839
def canvas(self) -> FigureCanvasBase | None: ...
3940
def connect_event(self, event: Event, callback: Callable) -> None: ...
4041
def disconnect_events(self) -> None: ...
42+
def _set_cursor(self, cursor: Cursors) -> None: ...
4143

4244
class Button(AxesWidget):
4345
label: Text
@@ -335,6 +337,7 @@ class SpanSelector(_SelectorWidget):
335337
_props: dict[str, Any] | None = ...,
336338
_init: bool = ...,
337339
) -> None: ...
340+
def _set_span_cursor(self, *, enabled: bool) -> None: ...
338341
def connect_default_events(self) -> None: ...
339342
@property
340343
def direction(self) -> Literal["horizontal", "vertical"]: ...
@@ -398,6 +401,7 @@ class RectangleSelector(_SelectorWidget):
398401
minspany: float
399402
spancoords: Literal["data", "pixels"]
400403
grab_range: float
404+
_active_handle: None | Literal["C", "N", "NE", "E", "SE", "S", "SW", "W", "NW"]
401405
def __init__(
402406
self,
403407
ax: Axes,

0 commit comments

Comments
 (0)