From 6bd6238f70631e91bdfb43cadfa50ff5240a074a Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Fri, 27 Jan 2023 22:46:42 +0100 Subject: [PATCH] Deprecate LocationEvent.lastevent. Keeping around lastevent can delay garbage collection of a torn-down axes, and can also keep around an actual GUI event object (guiEvent) associated with a long-torn down widget, which can be problematic for some GUI toolkits. Instead, only keep track of the last inaxes attribute. Unfortunately there are no tests for axes_enter_event/axes_leave_event, but this can be manually tested by running the event_handling/figure_axes_enter_leave.py example. --- .../next_api_changes/behavior/25101-AL.rst | 4 ++ .../deprecations/25101-AL.rst | 3 + lib/matplotlib/backend_bases.py | 57 +++++++++++++------ 3 files changed, 47 insertions(+), 17 deletions(-) create mode 100644 doc/api/next_api_changes/behavior/25101-AL.rst create mode 100644 doc/api/next_api_changes/deprecations/25101-AL.rst diff --git a/doc/api/next_api_changes/behavior/25101-AL.rst b/doc/api/next_api_changes/behavior/25101-AL.rst new file mode 100644 index 000000000000..b009c6a366c4 --- /dev/null +++ b/doc/api/next_api_changes/behavior/25101-AL.rst @@ -0,0 +1,4 @@ +Event objects emitted for ``axes_leave_event`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +``axes_leave_event`` now emits a synthetic `.LocationEvent`, instead of reusing +the last event object associated with a ``motion_notify_event``. diff --git a/doc/api/next_api_changes/deprecations/25101-AL.rst b/doc/api/next_api_changes/deprecations/25101-AL.rst new file mode 100644 index 000000000000..be937fb3cf0d --- /dev/null +++ b/doc/api/next_api_changes/deprecations/25101-AL.rst @@ -0,0 +1,3 @@ +``LocationEvent.lastevent`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~ +... is deprecated with no replacement. diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 8c11c73afb8c..dc5ffeeab603 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -37,6 +37,7 @@ import os import sys import time +import weakref from weakref import WeakKeyDictionary import numpy as np @@ -1331,7 +1332,11 @@ class LocationEvent(Event): The keyboard modifiers currently being pressed (except for KeyEvent). """ - lastevent = None # The last event processed so far. + # Fully delete all occurrences of lastevent after deprecation elapses. + _lastevent = None + lastevent = _api.deprecated("3.8")( + _api.classproperty(lambda cls: cls._lastevent)) + _last_axes_ref = None def __init__(self, name, canvas, x, y, guiEvent=None, *, modifiers=None): super().__init__(name, canvas, guiEvent=guiEvent) @@ -1348,20 +1353,25 @@ def __init__(self, name, canvas, x, y, guiEvent=None, *, modifiers=None): # cannot check if event was in Axes if no (x, y) info return - if self.canvas.mouse_grabber is None: - self.inaxes = self.canvas.inaxes((x, y)) - else: - self.inaxes = self.canvas.mouse_grabber + self._set_inaxes(self.canvas.inaxes((x, y)) + if self.canvas.mouse_grabber is None else + self.canvas.mouse_grabber, + (x, y)) + + # Splitting _set_inaxes out is useful for the axes_leave_event handler: it + # needs to generate synthetic LocationEvents with manually-set inaxes. In + # that latter case, xy has already been cast to int so it can directly be + # read from self.x, self.y; in the normal case, however, it is more + # accurate to pass the untruncated float x, y values passed to the ctor. - if self.inaxes is not None: + def _set_inaxes(self, inaxes, xy=None): + self.inaxes = inaxes + if inaxes is not None: try: - trans = self.inaxes.transData.inverted() - xdata, ydata = trans.transform((x, y)) + self.xdata, self.ydata = inaxes.transData.inverted().transform( + xy if xy is not None else (self.x, self.y)) except ValueError: pass - else: - self.xdata = xdata - self.ydata = ydata class MouseButton(IntEnum): @@ -1555,17 +1565,30 @@ def _mouse_handler(event): event.key = event.canvas._key # Emit axes_enter/axes_leave. if event.name == "motion_notify_event": - last = LocationEvent.lastevent - last_axes = last.inaxes if last is not None else None + last_ref = LocationEvent._last_axes_ref + last_axes = last_ref() if last_ref else None if last_axes != event.inaxes: if last_axes is not None: + # Create a synthetic LocationEvent for the axes_leave_event. + # Its inaxes attribute needs to be manually set (because the + # cursor is actually *out* of that axes at that point); this is + # done with the internal _set_inaxes method which ensures that + # the xdata and ydata attributes are also correct. try: - last.canvas.callbacks.process("axes_leave_event", last) + leave_event = LocationEvent( + "axes_leave_event", last_axes.figure.canvas, + event.x, event.y, event.guiEvent, + modifiers=event.modifiers) + leave_event._set_inaxes(last_axes) + last_axes.figure.canvas.callbacks.process( + "axes_leave_event", leave_event) except Exception: pass # The last canvas may already have been torn down. if event.inaxes is not None: event.canvas.callbacks.process("axes_enter_event", event) - LocationEvent.lastevent = ( + LocationEvent._last_axes_ref = ( + weakref.ref(event.inaxes) if event.inaxes else None) + LocationEvent._lastevent = ( None if event.name == "figure_leave_event" else event) @@ -1964,8 +1987,8 @@ def leave_notify_event(self, guiEvent=None): guiEvent The native UI event that generated the Matplotlib event. """ - self.callbacks.process('figure_leave_event', LocationEvent.lastevent) - LocationEvent.lastevent = None + self.callbacks.process('figure_leave_event', LocationEvent._lastevent) + LocationEvent._lastevent = None self._lastx, self._lasty = None, None @_api.deprecated("3.6", alternative=(