Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 4e21912

Browse files
anntzertacaswell
authored andcommitted
Make it easier to improve UI event metadata.
Currently, UI events (MouseEvent, KeyEvent, etc.) are generated by letting the GUI-specific backends massage the native event objects into a list of args/kwargs and then call `FigureCanvasBase.motion_notify_event`/`.key_press_event`/etc. This makes it a bit tricky to improve the metadata on the events, because one needs to change the signature on both the `FigureCanvasBase` method and the event class. Moreover, the `motion_notify_event`/etc. methods are directly bound as event handlers in the gtk3 and tk backends, and thus have incompatible signatures there. Instead, the native GUI handlers can directly construct the relevant event objects and trigger the events themselves; a new `Event._process` helper method makes this even shorter (and allows to keep factoring some common functionality e.g. for tracking the last pressed button or key). As an example, this PR also updates figure_leave_event to always correctly set the event location based on the *current* cursor position, instead of the last triggered location event (which may be outdated); this can now easily be done on a backend-by-backend basis, instead of coordinating the change with FigureCanvasBase.figure_leave_event. This also exposed another (minor) issue, in that resize events often trigger *two* calls to draw_idle -- one in the GUI-specific handler, and one in FigureCanvasBase.draw_idle (now moved to ResizeEvent._process, but should perhaps instead be a callback autoconnected to "resize_event") -- could probably be fixed later.
1 parent 4455dc9 commit 4e21912

File tree

15 files changed

+407
-315
lines changed

15 files changed

+407
-315
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
Event handlers
2+
~~~~~~~~~~~~~~
3+
The ``draw_event``, ``resize_event``, ``close_event``, ``key_press_event``,
4+
``key_release_event``, ``pick_event``, ``scroll_event``,
5+
``button_press_event``, ``button_release_event``, ``motion_notify_event``,
6+
``enter_notify_event`` and ``leave_notify_event`` methods of `.FigureCanvasBase`
7+
are deprecated. They had inconsistent signatures across backends, and made it
8+
difficult to improve event metadata.
9+
10+
In order to trigger an event on a canvas, directly construct an `.Event` object
11+
of the correct class and call ``canvas.callbacks.process(event.name, event)``.

lib/matplotlib/artist.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -498,6 +498,7 @@ def pick(self, mouseevent):
498498
--------
499499
set_picker, get_picker, pickable
500500
"""
501+
from .backend_bases import PickEvent # Circular import.
501502
# Pick self
502503
if self.pickable():
503504
picker = self.get_picker()
@@ -506,7 +507,8 @@ def pick(self, mouseevent):
506507
else:
507508
inside, prop = self.contains(mouseevent)
508509
if inside:
509-
self.figure.canvas.pick_event(mouseevent, self, **prop)
510+
PickEvent("pick_event", self.figure.canvas,
511+
mouseevent, self, **prop)._process()
510512

511513
# Pick children
512514
for a in self.get_children():

lib/matplotlib/backend_bases.py

Lines changed: 87 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1220,11 +1220,16 @@ class Event:
12201220
guiEvent
12211221
The GUI event that triggered the Matplotlib event.
12221222
"""
1223+
12231224
def __init__(self, name, canvas, guiEvent=None):
12241225
self.name = name
12251226
self.canvas = canvas
12261227
self.guiEvent = guiEvent
12271228

1229+
def _process(self):
1230+
"""Generate an event with name ``self.name`` on ``self.canvas``."""
1231+
self.canvas.callbacks.process(self.name, self)
1232+
12281233

12291234
class DrawEvent(Event):
12301235
"""
@@ -1267,14 +1272,28 @@ class ResizeEvent(Event):
12671272
height : int
12681273
Height of the canvas in pixels.
12691274
"""
1275+
12701276
def __init__(self, name, canvas):
12711277
super().__init__(name, canvas)
12721278
self.width, self.height = canvas.get_width_height()
12731279

1280+
def _process(self):
1281+
super()._process()
1282+
self.canvas.draw_idle()
1283+
12741284

12751285
class CloseEvent(Event):
12761286
"""An event triggered by a figure being closed."""
12771287

1288+
def _process(self):
1289+
try:
1290+
super()._process()
1291+
except (AttributeError, TypeError):
1292+
pass
1293+
# Suppress AttributeError/TypeError that occur when the python
1294+
# session is being killed. It may be that a better solution would
1295+
# be a mechanism to disconnect all callbacks upon shutdown.
1296+
12781297

12791298
class LocationEvent(Event):
12801299
"""
@@ -1294,7 +1313,7 @@ class LocationEvent(Event):
12941313
is not over an Axes.
12951314
"""
12961315

1297-
lastevent = None # the last event that was triggered before this one
1316+
lastevent = None # The last event processed so far.
12981317

12991318
def __init__(self, name, canvas, x, y, guiEvent=None):
13001319
super().__init__(name, canvas, guiEvent=guiEvent)
@@ -1308,7 +1327,6 @@ def __init__(self, name, canvas, x, y, guiEvent=None):
13081327

13091328
if x is None or y is None:
13101329
# cannot check if event was in Axes if no (x, y) info
1311-
self._update_enter_leave()
13121330
return
13131331

13141332
if self.canvas.mouse_grabber is None:
@@ -1326,33 +1344,21 @@ def __init__(self, name, canvas, x, y, guiEvent=None):
13261344
self.xdata = xdata
13271345
self.ydata = ydata
13281346

1329-
self._update_enter_leave()
1330-
1331-
def _update_enter_leave(self):
1332-
"""Process the figure/axes enter leave events."""
1333-
if LocationEvent.lastevent is not None:
1334-
last = LocationEvent.lastevent
1335-
if last.inaxes != self.inaxes:
1336-
# process Axes enter/leave events
1347+
def _process(self):
1348+
last = LocationEvent.lastevent
1349+
last_axes = last.inaxes if last is not None else None
1350+
if last_axes != self.inaxes:
1351+
if last_axes is not None:
13371352
try:
1338-
if last.inaxes is not None:
1339-
last.canvas.callbacks.process('axes_leave_event', last)
1353+
last.canvas.callbacks.process("axes_leave_event", last)
13401354
except Exception:
1355+
# The last canvas may already have been torn down.
13411356
pass
1342-
# See ticket 2901582.
1343-
# I think this is a valid exception to the rule
1344-
# against catching all exceptions; if anything goes
1345-
# wrong, we simply want to move on and process the
1346-
# current event.
1347-
if self.inaxes is not None:
1348-
self.canvas.callbacks.process('axes_enter_event', self)
1349-
1350-
else:
1351-
# process a figure enter event
13521357
if self.inaxes is not None:
1353-
self.canvas.callbacks.process('axes_enter_event', self)
1354-
1355-
LocationEvent.lastevent = self
1358+
self.canvas.callbacks.process("axes_enter_event", self)
1359+
LocationEvent.lastevent = (
1360+
None if self.name == "figure_leave_event" else self)
1361+
super()._process()
13561362

13571363

13581364
class MouseButton(IntEnum):
@@ -1375,11 +1381,15 @@ class MouseEvent(LocationEvent):
13751381
----------
13761382
button : None or `MouseButton` or {'up', 'down'}
13771383
The button pressed. 'up' and 'down' are used for scroll events.
1384+
13781385
Note that LEFT and RIGHT actually refer to the "primary" and
13791386
"secondary" buttons, i.e. if the user inverts their left and right
13801387
buttons ("left-handed setting") then the LEFT button will be the one
13811388
physically on the right.
13821389
1390+
If this is unset, *name* is "scroll_event", and *step* is nonzero, then
1391+
this will be set to "up" or "down" depending on the sign of *step*.
1392+
13831393
key : None or str
13841394
The key pressed when the mouse event triggered, e.g. 'shift'.
13851395
See `KeyEvent`.
@@ -1413,6 +1423,11 @@ def __init__(self, name, canvas, x, y, button=None, key=None,
14131423
step=0, dblclick=False, guiEvent=None):
14141424
if button in MouseButton.__members__.values():
14151425
button = MouseButton(button)
1426+
if name == "scroll_event" and button is None:
1427+
if step > 0:
1428+
button = "up"
1429+
elif step < 0:
1430+
button = "down"
14161431
self.button = button
14171432
self.key = key
14181433
self.step = step
@@ -1422,6 +1437,17 @@ def __init__(self, name, canvas, x, y, button=None, key=None,
14221437
# 'axes_enter_event', which requires a fully initialized event.
14231438
super().__init__(name, canvas, x, y, guiEvent=guiEvent)
14241439

1440+
def _process(self):
1441+
if self.name == "button_press_event":
1442+
self.canvas._button = self.button
1443+
elif self.name == "button_release_event":
1444+
self.canvas._button = None
1445+
elif self.name == "motion_notify_event" and self.button is None:
1446+
self.button = self.canvas._button
1447+
if self.key is None:
1448+
self.key = self.canvas._key
1449+
super()._process()
1450+
14251451
def __str__(self):
14261452
return (f"{self.name}: "
14271453
f"xy=({self.x}, {self.y}) xydata=({self.xdata}, {self.ydata}) "
@@ -1467,8 +1493,11 @@ def on_pick(event):
14671493
14681494
cid = fig.canvas.mpl_connect('pick_event', on_pick)
14691495
"""
1496+
14701497
def __init__(self, name, canvas, mouseevent, artist,
14711498
guiEvent=None, **kwargs):
1499+
if guiEvent is None:
1500+
guiEvent = mouseevent.guiEvent
14721501
super().__init__(name, canvas, guiEvent)
14731502
self.mouseevent = mouseevent
14741503
self.artist = artist
@@ -1506,11 +1535,19 @@ def on_key(event):
15061535
15071536
cid = fig.canvas.mpl_connect('key_press_event', on_key)
15081537
"""
1538+
15091539
def __init__(self, name, canvas, key, x=0, y=0, guiEvent=None):
15101540
self.key = key
15111541
# super-init deferred to the end: callback errors if called before
15121542
super().__init__(name, canvas, x, y, guiEvent=guiEvent)
15131543

1544+
def _process(self):
1545+
if self.name == "key_press_event":
1546+
self.canvas._key = self.key
1547+
elif self.name == "key_release_event":
1548+
self.canvas._key = None
1549+
super()._process()
1550+
15141551

15151552
def _get_renderer(figure, print_method=None):
15161553
"""
@@ -1720,12 +1757,16 @@ def resize(self, w, h):
17201757
_api.warn_deprecated("3.6", name="resize", obj_type="method",
17211758
alternative="FigureManagerBase.resize")
17221759

1760+
@_api.deprecated("3.6", alternative=(
1761+
"callbacks.process('draw_event', DrawEvent(...))"))
17231762
def draw_event(self, renderer):
17241763
"""Pass a `DrawEvent` to all functions connected to ``draw_event``."""
17251764
s = 'draw_event'
17261765
event = DrawEvent(s, self, renderer)
17271766
self.callbacks.process(s, event)
17281767

1768+
@_api.deprecated("3.6", alternative=(
1769+
"callbacks.process('resize_event', ResizeEvent(...))"))
17291770
def resize_event(self):
17301771
"""
17311772
Pass a `ResizeEvent` to all functions connected to ``resize_event``.
@@ -1735,6 +1776,8 @@ def resize_event(self):
17351776
self.callbacks.process(s, event)
17361777
self.draw_idle()
17371778

1779+
@_api.deprecated("3.6", alternative=(
1780+
"callbacks.process('close_event', CloseEvent(...))"))
17381781
def close_event(self, guiEvent=None):
17391782
"""
17401783
Pass a `CloseEvent` to all functions connected to ``close_event``.
@@ -1751,6 +1794,8 @@ def close_event(self, guiEvent=None):
17511794
# AttributeError occurs on OSX with qt4agg upon exiting
17521795
# with an open window; 'callbacks' attribute no longer exists.
17531796

1797+
@_api.deprecated("3.6", alternative=(
1798+
"callbacks.process('key_press_event', KeyEvent(...))"))
17541799
def key_press_event(self, key, guiEvent=None):
17551800
"""
17561801
Pass a `KeyEvent` to all functions connected to ``key_press_event``.
@@ -1761,6 +1806,8 @@ def key_press_event(self, key, guiEvent=None):
17611806
s, self, key, self._lastx, self._lasty, guiEvent=guiEvent)
17621807
self.callbacks.process(s, event)
17631808

1809+
@_api.deprecated("3.6", alternative=(
1810+
"callbacks.process('key_release_event', KeyEvent(...))"))
17641811
def key_release_event(self, key, guiEvent=None):
17651812
"""
17661813
Pass a `KeyEvent` to all functions connected to ``key_release_event``.
@@ -1771,6 +1818,8 @@ def key_release_event(self, key, guiEvent=None):
17711818
self.callbacks.process(s, event)
17721819
self._key = None
17731820

1821+
@_api.deprecated("3.6", alternative=(
1822+
"callbacks.process('pick_event', PickEvent(...))"))
17741823
def pick_event(self, mouseevent, artist, **kwargs):
17751824
"""
17761825
Callback processing for pick events.
@@ -1787,6 +1836,8 @@ def pick_event(self, mouseevent, artist, **kwargs):
17871836
**kwargs)
17881837
self.callbacks.process(s, event)
17891838

1839+
@_api.deprecated("3.6", alternative=(
1840+
"callbacks.process('scroll_event', MouseEvent(...))"))
17901841
def scroll_event(self, x, y, step, guiEvent=None):
17911842
"""
17921843
Callback processing for scroll events.
@@ -1807,6 +1858,8 @@ def scroll_event(self, x, y, step, guiEvent=None):
18071858
step=step, guiEvent=guiEvent)
18081859
self.callbacks.process(s, mouseevent)
18091860

1861+
@_api.deprecated("3.6", alternative=(
1862+
"callbacks.process('button_press_event', MouseEvent(...))"))
18101863
def button_press_event(self, x, y, button, dblclick=False, guiEvent=None):
18111864
"""
18121865
Callback processing for mouse button press events.
@@ -1824,6 +1877,8 @@ def button_press_event(self, x, y, button, dblclick=False, guiEvent=None):
18241877
dblclick=dblclick, guiEvent=guiEvent)
18251878
self.callbacks.process(s, mouseevent)
18261879

1880+
@_api.deprecated("3.6", alternative=(
1881+
"callbacks.process('button_release_event', MouseEvent(...))"))
18271882
def button_release_event(self, x, y, button, guiEvent=None):
18281883
"""
18291884
Callback processing for mouse button release events.
@@ -1848,6 +1903,9 @@ def button_release_event(self, x, y, button, guiEvent=None):
18481903
self.callbacks.process(s, event)
18491904
self._button = None
18501905

1906+
# Also remove _lastx, _lasty when this goes away.
1907+
@_api.deprecated("3.6", alternative=(
1908+
"callbacks.process('motion_notify_event', MouseEvent(...))"))
18511909
def motion_notify_event(self, x, y, guiEvent=None):
18521910
"""
18531911
Callback processing for mouse movement events.
@@ -1873,6 +1931,8 @@ def motion_notify_event(self, x, y, guiEvent=None):
18731931
guiEvent=guiEvent)
18741932
self.callbacks.process(s, event)
18751933

1934+
@_api.deprecated("3.6", alternative=(
1935+
"callbacks.process('leave_notify_event', LocationEvent(...))"))
18761936
def leave_notify_event(self, guiEvent=None):
18771937
"""
18781938
Callback processing for the mouse cursor leaving the canvas.
@@ -1889,6 +1949,8 @@ def leave_notify_event(self, guiEvent=None):
18891949
LocationEvent.lastevent = None
18901950
self._lastx, self._lasty = None, None
18911951

1952+
@_api.deprecated("3.6", alternative=(
1953+
"callbacks.process('enter_notify_event', LocationEvent(...))"))
18921954
def enter_notify_event(self, guiEvent=None, xy=None):
18931955
"""
18941956
Callback processing for the mouse cursor entering the canvas.

0 commit comments

Comments
 (0)