From 4e21912d2938b0e8812c4d1f7cd902c080062ff2 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Mon, 23 Mar 2020 19:36:12 +0100 Subject: [PATCH 1/6] 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. --- .../deprecations/16931-AL.rst | 11 ++ lib/matplotlib/artist.py | 4 +- lib/matplotlib/backend_bases.py | 112 +++++++++++--- lib/matplotlib/backends/_backend_tk.py | 62 +++++--- lib/matplotlib/backends/backend_gtk3.py | 66 ++++---- lib/matplotlib/backends/backend_gtk4.py | 53 ++++--- lib/matplotlib/backends/backend_macosx.py | 10 +- lib/matplotlib/backends/backend_qt.py | 79 ++++++---- .../backends/backend_webagg_core.py | 54 +++---- lib/matplotlib/backends/backend_wx.py | 80 ++++++---- lib/matplotlib/figure.py | 4 +- .../tests/test_backends_interactive.py | 8 +- lib/matplotlib/tests/test_offsetbox.py | 8 +- lib/matplotlib/tests/test_widgets.py | 25 ++- src/_macosx.m | 146 ++++++------------ 15 files changed, 407 insertions(+), 315 deletions(-) create mode 100644 doc/api/next_api_changes/deprecations/16931-AL.rst diff --git a/doc/api/next_api_changes/deprecations/16931-AL.rst b/doc/api/next_api_changes/deprecations/16931-AL.rst new file mode 100644 index 000000000000..3dfa7d2cbaf7 --- /dev/null +++ b/doc/api/next_api_changes/deprecations/16931-AL.rst @@ -0,0 +1,11 @@ +Event handlers +~~~~~~~~~~~~~~ +The ``draw_event``, ``resize_event``, ``close_event``, ``key_press_event``, +``key_release_event``, ``pick_event``, ``scroll_event``, +``button_press_event``, ``button_release_event``, ``motion_notify_event``, +``enter_notify_event`` and ``leave_notify_event`` methods of `.FigureCanvasBase` +are deprecated. They had inconsistent signatures across backends, and made it +difficult to improve event metadata. + +In order to trigger an event on a canvas, directly construct an `.Event` object +of the correct class and call ``canvas.callbacks.process(event.name, event)``. diff --git a/lib/matplotlib/artist.py b/lib/matplotlib/artist.py index 7f502700d11f..2e104e3d7fe4 100644 --- a/lib/matplotlib/artist.py +++ b/lib/matplotlib/artist.py @@ -498,6 +498,7 @@ def pick(self, mouseevent): -------- set_picker, get_picker, pickable """ + from .backend_bases import PickEvent # Circular import. # Pick self if self.pickable(): picker = self.get_picker() @@ -506,7 +507,8 @@ def pick(self, mouseevent): else: inside, prop = self.contains(mouseevent) if inside: - self.figure.canvas.pick_event(mouseevent, self, **prop) + PickEvent("pick_event", self.figure.canvas, + mouseevent, self, **prop)._process() # Pick children for a in self.get_children(): diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 6ccc3796df26..5574048d1ec4 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -1220,11 +1220,16 @@ class Event: guiEvent The GUI event that triggered the Matplotlib event. """ + def __init__(self, name, canvas, guiEvent=None): self.name = name self.canvas = canvas self.guiEvent = guiEvent + def _process(self): + """Generate an event with name ``self.name`` on ``self.canvas``.""" + self.canvas.callbacks.process(self.name, self) + class DrawEvent(Event): """ @@ -1267,14 +1272,28 @@ class ResizeEvent(Event): height : int Height of the canvas in pixels. """ + def __init__(self, name, canvas): super().__init__(name, canvas) self.width, self.height = canvas.get_width_height() + def _process(self): + super()._process() + self.canvas.draw_idle() + class CloseEvent(Event): """An event triggered by a figure being closed.""" + def _process(self): + try: + super()._process() + except (AttributeError, TypeError): + pass + # Suppress AttributeError/TypeError that occur when the python + # session is being killed. It may be that a better solution would + # be a mechanism to disconnect all callbacks upon shutdown. + class LocationEvent(Event): """ @@ -1294,7 +1313,7 @@ class LocationEvent(Event): is not over an Axes. """ - lastevent = None # the last event that was triggered before this one + lastevent = None # The last event processed so far. def __init__(self, name, canvas, x, y, guiEvent=None): super().__init__(name, canvas, guiEvent=guiEvent) @@ -1308,7 +1327,6 @@ def __init__(self, name, canvas, x, y, guiEvent=None): if x is None or y is None: # cannot check if event was in Axes if no (x, y) info - self._update_enter_leave() return if self.canvas.mouse_grabber is None: @@ -1326,33 +1344,21 @@ def __init__(self, name, canvas, x, y, guiEvent=None): self.xdata = xdata self.ydata = ydata - self._update_enter_leave() - - def _update_enter_leave(self): - """Process the figure/axes enter leave events.""" - if LocationEvent.lastevent is not None: - last = LocationEvent.lastevent - if last.inaxes != self.inaxes: - # process Axes enter/leave events + def _process(self): + last = LocationEvent.lastevent + last_axes = last.inaxes if last is not None else None + if last_axes != self.inaxes: + if last_axes is not None: try: - if last.inaxes is not None: - last.canvas.callbacks.process('axes_leave_event', last) + last.canvas.callbacks.process("axes_leave_event", last) except Exception: + # The last canvas may already have been torn down. pass - # See ticket 2901582. - # I think this is a valid exception to the rule - # against catching all exceptions; if anything goes - # wrong, we simply want to move on and process the - # current event. - if self.inaxes is not None: - self.canvas.callbacks.process('axes_enter_event', self) - - else: - # process a figure enter event if self.inaxes is not None: - self.canvas.callbacks.process('axes_enter_event', self) - - LocationEvent.lastevent = self + self.canvas.callbacks.process("axes_enter_event", self) + LocationEvent.lastevent = ( + None if self.name == "figure_leave_event" else self) + super()._process() class MouseButton(IntEnum): @@ -1375,11 +1381,15 @@ class MouseEvent(LocationEvent): ---------- button : None or `MouseButton` or {'up', 'down'} The button pressed. 'up' and 'down' are used for scroll events. + Note that LEFT and RIGHT actually refer to the "primary" and "secondary" buttons, i.e. if the user inverts their left and right buttons ("left-handed setting") then the LEFT button will be the one physically on the right. + If this is unset, *name* is "scroll_event", and *step* is nonzero, then + this will be set to "up" or "down" depending on the sign of *step*. + key : None or str The key pressed when the mouse event triggered, e.g. 'shift'. See `KeyEvent`. @@ -1413,6 +1423,11 @@ def __init__(self, name, canvas, x, y, button=None, key=None, step=0, dblclick=False, guiEvent=None): if button in MouseButton.__members__.values(): button = MouseButton(button) + if name == "scroll_event" and button is None: + if step > 0: + button = "up" + elif step < 0: + button = "down" self.button = button self.key = key self.step = step @@ -1422,6 +1437,17 @@ def __init__(self, name, canvas, x, y, button=None, key=None, # 'axes_enter_event', which requires a fully initialized event. super().__init__(name, canvas, x, y, guiEvent=guiEvent) + def _process(self): + if self.name == "button_press_event": + self.canvas._button = self.button + elif self.name == "button_release_event": + self.canvas._button = None + elif self.name == "motion_notify_event" and self.button is None: + self.button = self.canvas._button + if self.key is None: + self.key = self.canvas._key + super()._process() + def __str__(self): return (f"{self.name}: " f"xy=({self.x}, {self.y}) xydata=({self.xdata}, {self.ydata}) " @@ -1467,8 +1493,11 @@ def on_pick(event): cid = fig.canvas.mpl_connect('pick_event', on_pick) """ + def __init__(self, name, canvas, mouseevent, artist, guiEvent=None, **kwargs): + if guiEvent is None: + guiEvent = mouseevent.guiEvent super().__init__(name, canvas, guiEvent) self.mouseevent = mouseevent self.artist = artist @@ -1506,11 +1535,19 @@ def on_key(event): cid = fig.canvas.mpl_connect('key_press_event', on_key) """ + def __init__(self, name, canvas, key, x=0, y=0, guiEvent=None): self.key = key # super-init deferred to the end: callback errors if called before super().__init__(name, canvas, x, y, guiEvent=guiEvent) + def _process(self): + if self.name == "key_press_event": + self.canvas._key = self.key + elif self.name == "key_release_event": + self.canvas._key = None + super()._process() + def _get_renderer(figure, print_method=None): """ @@ -1720,12 +1757,16 @@ def resize(self, w, h): _api.warn_deprecated("3.6", name="resize", obj_type="method", alternative="FigureManagerBase.resize") + @_api.deprecated("3.6", alternative=( + "callbacks.process('draw_event', DrawEvent(...))")) def draw_event(self, renderer): """Pass a `DrawEvent` to all functions connected to ``draw_event``.""" s = 'draw_event' event = DrawEvent(s, self, renderer) self.callbacks.process(s, event) + @_api.deprecated("3.6", alternative=( + "callbacks.process('resize_event', ResizeEvent(...))")) def resize_event(self): """ Pass a `ResizeEvent` to all functions connected to ``resize_event``. @@ -1735,6 +1776,8 @@ def resize_event(self): self.callbacks.process(s, event) self.draw_idle() + @_api.deprecated("3.6", alternative=( + "callbacks.process('close_event', CloseEvent(...))")) def close_event(self, guiEvent=None): """ Pass a `CloseEvent` to all functions connected to ``close_event``. @@ -1751,6 +1794,8 @@ def close_event(self, guiEvent=None): # AttributeError occurs on OSX with qt4agg upon exiting # with an open window; 'callbacks' attribute no longer exists. + @_api.deprecated("3.6", alternative=( + "callbacks.process('key_press_event', KeyEvent(...))")) def key_press_event(self, key, guiEvent=None): """ Pass a `KeyEvent` to all functions connected to ``key_press_event``. @@ -1761,6 +1806,8 @@ def key_press_event(self, key, guiEvent=None): s, self, key, self._lastx, self._lasty, guiEvent=guiEvent) self.callbacks.process(s, event) + @_api.deprecated("3.6", alternative=( + "callbacks.process('key_release_event', KeyEvent(...))")) def key_release_event(self, key, guiEvent=None): """ Pass a `KeyEvent` to all functions connected to ``key_release_event``. @@ -1771,6 +1818,8 @@ def key_release_event(self, key, guiEvent=None): self.callbacks.process(s, event) self._key = None + @_api.deprecated("3.6", alternative=( + "callbacks.process('pick_event', PickEvent(...))")) def pick_event(self, mouseevent, artist, **kwargs): """ Callback processing for pick events. @@ -1787,6 +1836,8 @@ def pick_event(self, mouseevent, artist, **kwargs): **kwargs) self.callbacks.process(s, event) + @_api.deprecated("3.6", alternative=( + "callbacks.process('scroll_event', MouseEvent(...))")) def scroll_event(self, x, y, step, guiEvent=None): """ Callback processing for scroll events. @@ -1807,6 +1858,8 @@ def scroll_event(self, x, y, step, guiEvent=None): step=step, guiEvent=guiEvent) self.callbacks.process(s, mouseevent) + @_api.deprecated("3.6", alternative=( + "callbacks.process('button_press_event', MouseEvent(...))")) def button_press_event(self, x, y, button, dblclick=False, guiEvent=None): """ Callback processing for mouse button press events. @@ -1824,6 +1877,8 @@ def button_press_event(self, x, y, button, dblclick=False, guiEvent=None): dblclick=dblclick, guiEvent=guiEvent) self.callbacks.process(s, mouseevent) + @_api.deprecated("3.6", alternative=( + "callbacks.process('button_release_event', MouseEvent(...))")) def button_release_event(self, x, y, button, guiEvent=None): """ Callback processing for mouse button release events. @@ -1848,6 +1903,9 @@ def button_release_event(self, x, y, button, guiEvent=None): self.callbacks.process(s, event) self._button = None + # Also remove _lastx, _lasty when this goes away. + @_api.deprecated("3.6", alternative=( + "callbacks.process('motion_notify_event', MouseEvent(...))")) def motion_notify_event(self, x, y, guiEvent=None): """ Callback processing for mouse movement events. @@ -1873,6 +1931,8 @@ def motion_notify_event(self, x, y, guiEvent=None): guiEvent=guiEvent) self.callbacks.process(s, event) + @_api.deprecated("3.6", alternative=( + "callbacks.process('leave_notify_event', LocationEvent(...))")) def leave_notify_event(self, guiEvent=None): """ Callback processing for the mouse cursor leaving the canvas. @@ -1889,6 +1949,8 @@ def leave_notify_event(self, guiEvent=None): LocationEvent.lastevent = None self._lastx, self._lasty = None, None + @_api.deprecated("3.6", alternative=( + "callbacks.process('enter_notify_event', LocationEvent(...))")) def enter_notify_event(self, guiEvent=None, xy=None): """ Callback processing for the mouse cursor entering the canvas. diff --git a/lib/matplotlib/backends/_backend_tk.py b/lib/matplotlib/backends/_backend_tk.py index 189ac0575273..336f6e6d1a78 100644 --- a/lib/matplotlib/backends/_backend_tk.py +++ b/lib/matplotlib/backends/_backend_tk.py @@ -17,7 +17,8 @@ from matplotlib import _api, backend_tools, cbook, _c_internal_utils from matplotlib.backend_bases import ( _Backend, FigureCanvasBase, FigureManagerBase, NavigationToolbar2, - TimerBase, ToolContainerBase, cursors, _Mode) + TimerBase, ToolContainerBase, cursors, _Mode, + CloseEvent, KeyEvent, LocationEvent, MouseEvent, ResizeEvent) from matplotlib._pylab_helpers import Gcf from . import _tkagg @@ -206,7 +207,7 @@ def __init__(self, figure=None, master=None): # to the window and filter. def filter_destroy(event): if event.widget is self._tkcanvas: - self.close_event() + CloseEvent("close_event", self)._process() root.bind("", filter_destroy, "+") self._tkcanvas.focus_set() @@ -239,7 +240,7 @@ def resize(self, event): master=self._tkcanvas, width=int(width), height=int(height)) self._tkcanvas.create_image( int(width / 2), int(height / 2), image=self._tkphoto) - self.resize_event() + ResizeEvent("resize_event", self)._process() def draw_idle(self): # docstring inherited @@ -271,12 +272,19 @@ def _event_mpl_coords(self, event): self.figure.bbox.height - self._tkcanvas.canvasy(event.y)) def motion_notify_event(self, event): - super().motion_notify_event( - *self._event_mpl_coords(event), guiEvent=event) + MouseEvent("motion_notify_event", self, + *self._event_mpl_coords(event), + guiEvent=event)._process() def enter_notify_event(self, event): - super().enter_notify_event( - guiEvent=event, xy=self._event_mpl_coords(event)) + LocationEvent("figure_enter_event", self, + *self._event_mpl_coords(event), + guiEvent=event)._process() + + def leave_notify_event(self, event): + LocationEvent("figure_leave_event", self, + *self._event_mpl_coords(event), + guiEvent=event)._process() def button_press_event(self, event, dblclick=False): # set focus to the canvas so that it can receive keyboard events @@ -285,9 +293,9 @@ def button_press_event(self, event, dblclick=False): num = getattr(event, 'num', None) if sys.platform == 'darwin': # 2 and 3 are reversed. num = {2: 3, 3: 2}.get(num, num) - super().button_press_event( - *self._event_mpl_coords(event), num, dblclick=dblclick, - guiEvent=event) + MouseEvent("button_press_event", self, + *self._event_mpl_coords(event), num, dblclick=dblclick, + guiEvent=event)._process() def button_dblclick_event(self, event): self.button_press_event(event, dblclick=True) @@ -296,25 +304,29 @@ def button_release_event(self, event): num = getattr(event, 'num', None) if sys.platform == 'darwin': # 2 and 3 are reversed. num = {2: 3, 3: 2}.get(num, num) - super().button_release_event( - *self._event_mpl_coords(event), num, guiEvent=event) + MouseEvent("button_release_event", self, + *self._event_mpl_coords(event), num, + guiEvent=event)._process() def scroll_event(self, event): num = getattr(event, 'num', None) step = 1 if num == 4 else -1 if num == 5 else 0 - super().scroll_event( - *self._event_mpl_coords(event), step, guiEvent=event) + MouseEvent("scroll_event", self, + *self._event_mpl_coords(event), step=step, + guiEvent=event)._process() def scroll_event_windows(self, event): """MouseWheel event processor""" # need to find the window that contains the mouse w = event.widget.winfo_containing(event.x_root, event.y_root) - if w == self._tkcanvas: - x = self._tkcanvas.canvasx(event.x_root - w.winfo_rootx()) - y = (self.figure.bbox.height - - self._tkcanvas.canvasy(event.y_root - w.winfo_rooty())) - step = event.delta/120. - FigureCanvasBase.scroll_event(self, x, y, step, guiEvent=event) + if w != self._tkcanvas: + return + x = self._tkcanvas.canvasx(event.x_root - w.winfo_rootx()) + y = (self.figure.bbox.height + - self._tkcanvas.canvasy(event.y_root - w.winfo_rooty())) + step = event.delta / 120 + MouseEvent("scroll_event", self, + x, y, step=step, guiEvent=event)._process() def _get_key(self, event): unikey = event.char @@ -356,12 +368,14 @@ def _get_key(self, event): return key def key_press(self, event): - key = self._get_key(event) - FigureCanvasBase.key_press_event(self, key, guiEvent=event) + KeyEvent("key_press_event", self, + self._get_key(event), *self._event_mpl_coords(event), + guiEvent=event)._process() def key_release(self, event): - key = self._get_key(event) - FigureCanvasBase.key_release_event(self, key, guiEvent=event) + KeyEvent("key_release_event", self, + self._get_key(event), *self._event_mpl_coords(event), + guiEvent=event)._process() def new_timer(self, *args, **kwargs): # docstring inherited diff --git a/lib/matplotlib/backends/backend_gtk3.py b/lib/matplotlib/backends/backend_gtk3.py index d4a908033984..89e92690c7eb 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -6,7 +6,9 @@ import matplotlib as mpl from matplotlib import _api, backend_tools, cbook -from matplotlib.backend_bases import FigureCanvasBase, ToolContainerBase +from matplotlib.backend_bases import ( + FigureCanvasBase, ToolContainerBase, + CloseEvent, KeyEvent, LocationEvent, MouseEvent, ResizeEvent) from matplotlib.backend_tools import Cursors try: @@ -101,8 +103,8 @@ def __init__(self, figure=None): self.connect('key_press_event', self.key_press_event) self.connect('key_release_event', self.key_release_event) self.connect('motion_notify_event', self.motion_notify_event) - self.connect('leave_notify_event', self.leave_notify_event) self.connect('enter_notify_event', self.enter_notify_event) + self.connect('leave_notify_event', self.leave_notify_event) self.connect('size_allocate', self.size_allocate) self.set_events(self.__class__.event_mask) @@ -116,7 +118,7 @@ def __init__(self, figure=None): style_ctx.add_class("matplotlib-canvas") def destroy(self): - self.close_event() + CloseEvent("close_event", self)._process() def set_cursor(self, cursor): # docstring inherited @@ -126,9 +128,10 @@ def set_cursor(self, cursor): context = GLib.MainContext.default() context.iteration(True) - def _mouse_event_coords(self, event): + def _mpl_coords(self, event=None): """ - Calculate mouse coordinates in physical pixels. + Convert the position of a GTK event, or of the current cursor position + if *event* is None, to Matplotlib coordinates. GTK use logical pixels, but the figure is scaled to physical pixels for rendering. Transform to physical pixels so that all of the down-stream @@ -136,57 +139,66 @@ def _mouse_event_coords(self, event): Also, the origin is different and needs to be corrected. """ - x = event.x * self.device_pixel_ratio + if event is None: + window = self.get_window() + t, x, y, state = window.get_device_position( + window.get_display().get_device_manager().get_client_pointer()) + else: + x, y = event.x, event.y + x = x * self.device_pixel_ratio # flip y so y=0 is bottom of canvas - y = self.figure.bbox.height - event.y * self.device_pixel_ratio + y = self.figure.bbox.height - y * self.device_pixel_ratio return x, y def scroll_event(self, widget, event): - x, y = self._mouse_event_coords(event) step = 1 if event.direction == Gdk.ScrollDirection.UP else -1 - FigureCanvasBase.scroll_event(self, x, y, step, guiEvent=event) + MouseEvent("scroll_event", self, *self._mpl_coords(event), step=step, + guiEvent=event)._process() return False # finish event propagation? def button_press_event(self, widget, event): - x, y = self._mouse_event_coords(event) - FigureCanvasBase.button_press_event( - self, x, y, event.button, guiEvent=event) + MouseEvent("button_press_event", self, + *self._mpl_coords(event), event.button, + guiEvent=event)._process() return False # finish event propagation? def button_release_event(self, widget, event): - x, y = self._mouse_event_coords(event) - FigureCanvasBase.button_release_event( - self, x, y, event.button, guiEvent=event) + MouseEvent("button_release_event", self, + *self._mpl_coords(event), event.button, + guiEvent=event)._process() return False # finish event propagation? def key_press_event(self, widget, event): - key = self._get_key(event) - FigureCanvasBase.key_press_event(self, key, guiEvent=event) + KeyEvent("key_press_event", self, + self._get_key(event), *self._mpl_coords(), + guiEvent=event)._process() return True # stop event propagation def key_release_event(self, widget, event): - key = self._get_key(event) - FigureCanvasBase.key_release_event(self, key, guiEvent=event) + KeyEvent("key_release_event", self, + self._get_key(event), *self._mpl_coords(), + guiEvent=event)._process() return True # stop event propagation def motion_notify_event(self, widget, event): - x, y = self._mouse_event_coords(event) - FigureCanvasBase.motion_notify_event(self, x, y, guiEvent=event) + MouseEvent("motion_notify_event", self, *self._mpl_coords(event), + guiEvent=event)._process() return False # finish event propagation? - def leave_notify_event(self, widget, event): - FigureCanvasBase.leave_notify_event(self, event) - def enter_notify_event(self, widget, event): - x, y = self._mouse_event_coords(event) - FigureCanvasBase.enter_notify_event(self, guiEvent=event, xy=(x, y)) + LocationEvent("figure_enter_event", self, *self._mpl_coords(event), + guiEvent=event)._process() + + def leave_notify_event(self, widget, event): + LocationEvent("figure_leave_event", self, *self._mpl_coords(event), + guiEvent=event)._process() def size_allocate(self, widget, allocation): dpival = self.figure.dpi winch = allocation.width * self.device_pixel_ratio / dpival hinch = allocation.height * self.device_pixel_ratio / dpival self.figure.set_size_inches(winch, hinch, forward=False) - FigureCanvasBase.resize_event(self) + ResizeEvent("resize_event", self)._process() self.draw_idle() def _get_key(self, event): diff --git a/lib/matplotlib/backends/backend_gtk4.py b/lib/matplotlib/backends/backend_gtk4.py index 4cbe1e059a77..b92a73b5418d 100644 --- a/lib/matplotlib/backends/backend_gtk4.py +++ b/lib/matplotlib/backends/backend_gtk4.py @@ -4,7 +4,9 @@ import matplotlib as mpl from matplotlib import _api, backend_tools, cbook -from matplotlib.backend_bases import FigureCanvasBase, ToolContainerBase +from matplotlib.backend_bases import ( + FigureCanvasBase, ToolContainerBase, + CloseEvent, KeyEvent, LocationEvent, MouseEvent, ResizeEvent) try: import gi @@ -86,9 +88,10 @@ def set_cursor(self, cursor): # docstring inherited self.set_cursor_from_name(_backend_gtk.mpl_to_gtk_cursor_name(cursor)) - def _mouse_event_coords(self, x, y): + def _mpl_coords(self, xy=None): """ - Calculate mouse coordinates in physical pixels. + Convert the *xy* position of a GTK event, or of the current cursor + position if *xy* is None, to Matplotlib coordinates. GTK use logical pixels, but the figure is scaled to physical pixels for rendering. Transform to physical pixels so that all of the down-stream @@ -96,46 +99,56 @@ def _mouse_event_coords(self, x, y): Also, the origin is different and needs to be corrected. """ + if xy is None: + surface = self.get_native().get_surface() + is_over, x, y, mask = surface.get_device_position( + self.get_display().get_default_seat().get_pointer()) + else: + x, y = xy x = x * self.device_pixel_ratio # flip y so y=0 is bottom of canvas y = self.figure.bbox.height - y * self.device_pixel_ratio return x, y def scroll_event(self, controller, dx, dy): - FigureCanvasBase.scroll_event(self, 0, 0, dy) + MouseEvent("scroll_event", self, + *self._mpl_coords(), step=dy)._process() return True def button_press_event(self, controller, n_press, x, y): - x, y = self._mouse_event_coords(x, y) - FigureCanvasBase.button_press_event(self, x, y, - controller.get_current_button()) + MouseEvent("button_press_event", self, + *self._mpl_coords((x, y)), controller.get_current_button() + )._process() self.grab_focus() def button_release_event(self, controller, n_press, x, y): - x, y = self._mouse_event_coords(x, y) - FigureCanvasBase.button_release_event(self, x, y, - controller.get_current_button()) + MouseEvent("button_release_event", self, + *self._mpl_coords((x, y)), controller.get_current_button() + )._process() def key_press_event(self, controller, keyval, keycode, state): - key = self._get_key(keyval, keycode, state) - FigureCanvasBase.key_press_event(self, key) + KeyEvent("key_press_event", self, + self._get_key(keyval, keycode, state), *self._mpl_coords() + )._process() return True def key_release_event(self, controller, keyval, keycode, state): - key = self._get_key(keyval, keycode, state) - FigureCanvasBase.key_release_event(self, key) + KeyEvent("key_release_event", self, + self._get_key(keyval, keycode, state), *self._mpl_coords() + )._process() return True def motion_notify_event(self, controller, x, y): - x, y = self._mouse_event_coords(x, y) - FigureCanvasBase.motion_notify_event(self, x, y) + MouseEvent("motion_notify_event", self, + *self._mpl_coords((x, y)))._process() def leave_notify_event(self, controller): - FigureCanvasBase.leave_notify_event(self) + LocationEvent("figure_leave_event", self, + *self._mpl_coords())._process() def enter_notify_event(self, controller, x, y): - x, y = self._mouse_event_coords(x, y) - FigureCanvasBase.enter_notify_event(self, xy=(x, y)) + LocationEvent("figure_enter_event", self, + *self._mpl_coords((x, y)))._process() def resize_event(self, area, width, height): self._update_device_pixel_ratio() @@ -143,7 +156,7 @@ def resize_event(self, area, width, height): winch = width * self.device_pixel_ratio / dpi hinch = height * self.device_pixel_ratio / dpi self.figure.set_size_inches(winch, hinch, forward=False) - FigureCanvasBase.resize_event(self) + ResizeEvent("resize_event", self)._process() self.draw_idle() def _get_key(self, keyval, keycode, state): diff --git a/lib/matplotlib/backends/backend_macosx.py b/lib/matplotlib/backends/backend_macosx.py index e58e4a8756eb..cb078b9d3dd6 100644 --- a/lib/matplotlib/backends/backend_macosx.py +++ b/lib/matplotlib/backends/backend_macosx.py @@ -7,7 +7,7 @@ from .backend_agg import FigureCanvasAgg from matplotlib.backend_bases import ( _Backend, FigureCanvasBase, FigureManagerBase, NavigationToolbar2, - TimerBase) + ResizeEvent, TimerBase) from matplotlib.figure import Figure from matplotlib.widgets import SubplotTool @@ -28,10 +28,8 @@ class FigureCanvasMac(FigureCanvasAgg, _macosx.FigureCanvas, FigureCanvasBase): # and we can just as well lift the FCBase base up one level, keeping it *at # the end* to have the right method resolution order. - # Events such as button presses, mouse movements, and key presses - # are handled in the C code and the base class methods - # button_press_event, button_release_event, motion_notify_event, - # key_press_event, and key_release_event are called from there. + # Events such as button presses, mouse movements, and key presses are + # handled in C and events (MouseEvent, etc.) are triggered from there. required_interactive_framework = "macosx" _timer_cls = TimerMac @@ -100,7 +98,7 @@ def resize(self, width, height): width /= scale height /= scale self.figure.set_size_inches(width, height, forward=False) - FigureCanvasBase.resize_event(self) + ResizeEvent("resize_event", self)._process() self.draw_idle() diff --git a/lib/matplotlib/backends/backend_qt.py b/lib/matplotlib/backends/backend_qt.py index 5f997d287950..a83f760c243b 100644 --- a/lib/matplotlib/backends/backend_qt.py +++ b/lib/matplotlib/backends/backend_qt.py @@ -9,7 +9,8 @@ from matplotlib._pylab_helpers import Gcf from matplotlib.backend_bases import ( _Backend, FigureCanvasBase, FigureManagerBase, NavigationToolbar2, - TimerBase, cursors, ToolContainerBase, MouseButton) + TimerBase, cursors, ToolContainerBase, MouseButton, + CloseEvent, KeyEvent, LocationEvent, MouseEvent, ResizeEvent) import matplotlib.backends.qt_editor.figureoptions as figureoptions from . import qt_compat from .qt_compat import ( @@ -246,18 +247,7 @@ def set_cursor(self, cursor): # docstring inherited self.setCursor(_api.check_getitem(cursord, cursor=cursor)) - def enterEvent(self, event): - x, y = self.mouseEventCoords(self._get_position(event)) - FigureCanvasBase.enter_notify_event(self, guiEvent=event, xy=(x, y)) - - def leaveEvent(self, event): - QtWidgets.QApplication.restoreOverrideCursor() - FigureCanvasBase.leave_notify_event(self, guiEvent=event) - - _get_position = operator.methodcaller( - "position" if QT_API in ["PyQt6", "PySide6"] else "pos") - - def mouseEventCoords(self, pos): + def mouseEventCoords(self, pos=None): """ Calculate mouse coordinates in physical pixels. @@ -267,39 +257,56 @@ def mouseEventCoords(self, pos): Also, the origin is different and needs to be corrected. """ + if pos is None: + pos = self.mapFromGlobal(QtGui.QCursor.pos()) + elif hasattr(pos, "position"): # qt6 QtGui.QEvent + pos = pos.position() + elif hasattr(pos, "pos"): # qt5 QtCore.QEvent + pos = pos.pos() + # (otherwise, it's already a QPoint) x = pos.x() # flip y so y=0 is bottom of canvas y = self.figure.bbox.height / self.device_pixel_ratio - pos.y() return x * self.device_pixel_ratio, y * self.device_pixel_ratio + def enterEvent(self, event): + LocationEvent("figure_enter_event", self, + *self.mouseEventCoords(event), + guiEvent=event)._process() + + def leaveEvent(self, event): + QtWidgets.QApplication.restoreOverrideCursor() + LocationEvent("figure_leave_event", self, + *self.mouseEventCoords(), + guiEvent=event)._process() + def mousePressEvent(self, event): - x, y = self.mouseEventCoords(self._get_position(event)) button = self.buttond.get(event.button()) if button is not None: - FigureCanvasBase.button_press_event(self, x, y, button, - guiEvent=event) + MouseEvent("button_press_event", self, + *self.mouseEventCoords(event), button, + guiEvent=event)._process() def mouseDoubleClickEvent(self, event): - x, y = self.mouseEventCoords(self._get_position(event)) button = self.buttond.get(event.button()) if button is not None: - FigureCanvasBase.button_press_event(self, x, y, - button, dblclick=True, - guiEvent=event) + MouseEvent("button_press_event", self, + *self.mouseEventCoords(event), button, dblclick=True, + guiEvent=event)._process() def mouseMoveEvent(self, event): - x, y = self.mouseEventCoords(self._get_position(event)) - FigureCanvasBase.motion_notify_event(self, x, y, guiEvent=event) + MouseEvent("motion_notify_event", self, + *self.mouseEventCoords(event), + guiEvent=event)._process() def mouseReleaseEvent(self, event): - x, y = self.mouseEventCoords(self._get_position(event)) button = self.buttond.get(event.button()) if button is not None: - FigureCanvasBase.button_release_event(self, x, y, button, - guiEvent=event) + MouseEvent("button_release_event", self, + *self.mouseEventCoords(event), button, + guiEvent=event)._process() def wheelEvent(self, event): - x, y = self.mouseEventCoords(self._get_position(event)) # from QWheelEvent::pixelDelta doc: pixelDelta is sometimes not # provided (`isNull()`) and is unreliable on X11 ("xcb"). if (event.pixelDelta().isNull() @@ -308,18 +315,23 @@ def wheelEvent(self, event): else: steps = event.pixelDelta().y() if steps: - FigureCanvasBase.scroll_event( - self, x, y, steps, guiEvent=event) + MouseEvent("scroll_event", self, + *self.mouseEventCoords(event), step=steps, + guiEvent=event)._process() def keyPressEvent(self, event): key = self._get_key(event) if key is not None: - FigureCanvasBase.key_press_event(self, key, guiEvent=event) + KeyEvent("key_press_event", self, + key, *self.mouseEventCoords(), + guiEvent=event)._process() def keyReleaseEvent(self, event): key = self._get_key(event) if key is not None: - FigureCanvasBase.key_release_event(self, key, guiEvent=event) + KeyEvent("key_release_event", self, + key, *self.mouseEventCoords(), + guiEvent=event)._process() def resizeEvent(self, event): frame = sys._getframe() @@ -328,7 +340,6 @@ def resizeEvent(self, event): return w = event.size().width() * self.device_pixel_ratio h = event.size().height() * self.device_pixel_ratio - dpival = self.figure.dpi winch = w / dpival hinch = h / dpival @@ -336,7 +347,7 @@ def resizeEvent(self, event): # pass back into Qt to let it finish QtWidgets.QWidget.resizeEvent(self, event) # emit our resize events - FigureCanvasBase.resize_event(self) + ResizeEvent("resize_event", self)._process() def sizeHint(self): w, h = self.get_width_height() @@ -503,7 +514,9 @@ class FigureManagerQT(FigureManagerBase): def __init__(self, canvas, num): self.window = MainWindow() super().__init__(canvas, num) - self.window.closing.connect(canvas.close_event) + self.window.closing.connect( + # The lambda prevents the event from being immediately gc'd. + lambda: CloseEvent("close_event", self.canvas)._process()) self.window.closing.connect(self._widgetclosed) if sys.platform != "darwin": diff --git a/lib/matplotlib/backends/backend_webagg_core.py b/lib/matplotlib/backends/backend_webagg_core.py index 3ca4e6906d2a..46d5862ab439 100644 --- a/lib/matplotlib/backends/backend_webagg_core.py +++ b/lib/matplotlib/backends/backend_webagg_core.py @@ -23,7 +23,8 @@ from matplotlib import _api, backend_bases, backend_tools from matplotlib.backends import backend_agg -from matplotlib.backend_bases import _Backend +from matplotlib.backend_bases import ( + _Backend, KeyEvent, LocationEvent, MouseEvent, ResizeEvent) _log = logging.getLogger(__name__) @@ -162,23 +163,21 @@ class FigureCanvasWebAggCore(backend_agg.FigureCanvasAgg): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - # Set to True when the renderer contains data that is newer # than the PNG buffer. self._png_is_old = True - # Set to True by the `refresh` message so that the next frame # sent to the clients will be a full frame. self._force_full = True - # The last buffer, for diff mode. self._last_buff = np.empty((0, 0)) - # Store the current image mode so that at any point, clients can # request the information. This should be changed by calling # self.set_image_mode(mode) so that the notification can be given # to the connected clients. self._current_image_mode = 'full' + # Track mouse events to fill in the x, y position of key events. + self._last_mouse_xy = (None, None) def show(self): # show the figure window @@ -285,40 +284,35 @@ def _handle_mouse(self, event): x = event['x'] y = event['y'] y = self.get_renderer().height - y - - # JavaScript button numbers and matplotlib button numbers are - # off by 1 + self._last_mouse_xy = x, y + # JavaScript button numbers and Matplotlib button numbers are off by 1. button = event['button'] + 1 e_type = event['type'] - guiEvent = event.get('guiEvent', None) - if e_type == 'button_press': - self.button_press_event(x, y, button, guiEvent=guiEvent) + guiEvent = event.get('guiEvent') + if e_type in ['button_press', 'button_release']: + MouseEvent(e_type + '_event', self, x, y, button, + guiEvent=guiEvent)._process() elif e_type == 'dblclick': - self.button_press_event(x, y, button, dblclick=True, - guiEvent=guiEvent) - elif e_type == 'button_release': - self.button_release_event(x, y, button, guiEvent=guiEvent) - elif e_type == 'motion_notify': - self.motion_notify_event(x, y, guiEvent=guiEvent) - elif e_type == 'figure_enter': - self.enter_notify_event(xy=(x, y), guiEvent=guiEvent) - elif e_type == 'figure_leave': - self.leave_notify_event() + MouseEvent('button_press_event', self, x, y, button, dblclick=True, + guiEvent=guiEvent)._process() elif e_type == 'scroll': - self.scroll_event(x, y, event['step'], guiEvent=guiEvent) + MouseEvent('scroll_event', self, x, y, step=event['step'], + guiEvent=guiEvent)._process() + elif e_type == 'motion_notify': + MouseEvent(e_type + '_event', self, x, y, + guiEvent=guiEvent)._process() + elif e_type in ['figure_enter', 'figure_leave']: + LocationEvent(e_type + '_event', self, x, y, + guiEvent=guiEvent)._process() handle_button_press = handle_button_release = handle_dblclick = \ handle_figure_enter = handle_figure_leave = handle_motion_notify = \ handle_scroll = _handle_mouse def _handle_key(self, event): - key = _handle_key(event['key']) - e_type = event['type'] - guiEvent = event.get('guiEvent', None) - if e_type == 'key_press': - self.key_press_event(key, guiEvent=guiEvent) - elif e_type == 'key_release': - self.key_release_event(key, guiEvent=guiEvent) + KeyEvent(event['type'] + '_event', self, + _handle_key(event['key']), *self._last_mouse_xy, + guiEvent=event.get('guiEvent'))._process() handle_key_press = handle_key_release = _handle_key def handle_toolbar_button(self, event): @@ -348,7 +342,7 @@ def handle_resize(self, event): # identical or within a pixel or so). self._png_is_old = True self.manager.resize(*fig.bbox.size, forward=False) - self.resize_event() + ResizeEvent('resize_event', self)._process() def handle_send_image_mode(self, event): # The client requests notification of what the current image mode is. diff --git a/lib/matplotlib/backends/backend_wx.py b/lib/matplotlib/backends/backend_wx.py index 3eaf59e3f502..0ba0d272f6f7 100644 --- a/lib/matplotlib/backends/backend_wx.py +++ b/lib/matplotlib/backends/backend_wx.py @@ -19,9 +19,10 @@ import matplotlib as mpl from matplotlib.backend_bases import ( - _Backend, FigureCanvasBase, FigureManagerBase, GraphicsContextBase, - MouseButton, NavigationToolbar2, RendererBase, TimerBase, - ToolContainerBase, cursors) + _Backend, FigureCanvasBase, FigureManagerBase, + GraphicsContextBase, MouseButton, NavigationToolbar2, RendererBase, + TimerBase, ToolContainerBase, cursors, + CloseEvent, KeyEvent, LocationEvent, MouseEvent, ResizeEvent) from matplotlib import _api, cbook, backend_tools from matplotlib._pylab_helpers import Gcf @@ -529,8 +530,8 @@ def __init__(self, parent, id, figure=None): self.Bind(wx.EVT_MOUSE_AUX2_DCLICK, self._on_mouse_button) self.Bind(wx.EVT_MOUSEWHEEL, self._on_mouse_wheel) self.Bind(wx.EVT_MOTION, self._on_motion) - self.Bind(wx.EVT_LEAVE_WINDOW, self._on_leave) self.Bind(wx.EVT_ENTER_WINDOW, self._on_enter) + self.Bind(wx.EVT_LEAVE_WINDOW, self._on_leave) self.Bind(wx.EVT_MOUSE_CAPTURE_CHANGED, self._on_capture_lost) self.Bind(wx.EVT_MOUSE_CAPTURE_LOST, self._on_capture_lost) @@ -703,7 +704,7 @@ def _on_size(self, event): # so no need to do anything here except to make sure # the whole background is repainted. self.Refresh(eraseBackground=False) - FigureCanvasBase.resize_event(self) + ResizeEvent("resize_event", self)._process() def _get_key(self, event): @@ -730,17 +731,32 @@ def _get_key(self, event): return key + def _mpl_coords(self, pos=None): + """ + Convert a wx position, defaulting to the current cursor position, to + Matplotlib coordinates. + """ + if pos is None: + pos = wx.GetMouseState() + x, y = self.ScreenToClient(pos.X, pos.Y) + else: + x, y = pos.X, pos.Y + # flip y so y=0 is bottom of canvas + return x, self.figure.bbox.height - y + def _on_key_down(self, event): """Capture key press.""" - key = self._get_key(event) - FigureCanvasBase.key_press_event(self, key, guiEvent=event) + KeyEvent("key_press_event", self, + self._get_key(event), *self._mpl_coords(), + guiEvent=event)._process() if self: event.Skip() def _on_key_up(self, event): """Release key.""" - key = self._get_key(event) - FigureCanvasBase.key_release_event(self, key, guiEvent=event) + KeyEvent("key_release_event", self, + self._get_key(event), *self._mpl_coords(), + guiEvent=event)._process() if self: event.Skip() @@ -773,8 +789,7 @@ def _on_mouse_button(self, event): """Start measuring on an axis.""" event.Skip() self._set_capture(event.ButtonDown() or event.ButtonDClick()) - x = event.X - y = self.figure.bbox.height - event.Y + x, y = self._mpl_coords(event) button_map = { wx.MOUSE_BTN_LEFT: MouseButton.LEFT, wx.MOUSE_BTN_MIDDLE: MouseButton.MIDDLE, @@ -785,18 +800,18 @@ def _on_mouse_button(self, event): button = event.GetButton() button = button_map.get(button, button) if event.ButtonDown(): - self.button_press_event(x, y, button, guiEvent=event) + MouseEvent("button_press_event", self, + x, y, button, guiEvent=event)._process() elif event.ButtonDClick(): - self.button_press_event(x, y, button, dblclick=True, - guiEvent=event) + MouseEvent("button_press_event", self, + x, y, button, dblclick=True, guiEvent=event)._process() elif event.ButtonUp(): - self.button_release_event(x, y, button, guiEvent=event) + MouseEvent("button_release_event", self, + x, y, button, guiEvent=event)._process() def _on_mouse_wheel(self, event): """Translate mouse wheel events into matplotlib events""" - # Determine mouse location - x = event.GetX() - y = self.figure.bbox.height - event.GetY() + x, y = self._mpl_coords(event) # Convert delta/rotation/rate into a floating point step size step = event.LinesPerAction * event.WheelRotation / event.WheelDelta # Done handling event @@ -810,26 +825,29 @@ def _on_mouse_wheel(self, event): return # Return without processing event else: self._skipwheelevent = True - FigureCanvasBase.scroll_event(self, x, y, step, guiEvent=event) + MouseEvent("scroll_event", self, + x, y, step=step, guiEvent=event)._process() def _on_motion(self, event): """Start measuring on an axis.""" - x = event.GetX() - y = self.figure.bbox.height - event.GetY() - event.Skip() - FigureCanvasBase.motion_notify_event(self, x, y, guiEvent=event) - - def _on_leave(self, event): - """Mouse has left the window.""" event.Skip() - FigureCanvasBase.leave_notify_event(self, guiEvent=event) + MouseEvent("motion_notify_event", self, + *self._mpl_coords(event), + guiEvent=event)._process() def _on_enter(self, event): """Mouse has entered the window.""" - x = event.GetX() - y = self.figure.bbox.height - event.GetY() event.Skip() - FigureCanvasBase.enter_notify_event(self, guiEvent=event, xy=(x, y)) + LocationEvent("figure_enter_event", self, + *self._mpl_coords(event), + guiEvent=event)._process() + + def _on_leave(self, event): + """Mouse has left the window.""" + event.Skip() + LocationEvent("figure_leave_event", self, + *self._mpl_coords(event), + guiEvent=event)._process() class FigureCanvasWx(_FigureCanvasWxBase): @@ -945,7 +963,7 @@ def get_figure_manager(self): def _on_close(self, event): _log.debug("%s - on_close()", type(self)) - self.canvas.close_event() + CloseEvent("close_event", self.canvas)._process() self.canvas.stop_event_loop() # set FigureManagerWx.frame to None to prevent repeated attempts to # close this frame from FigureManagerWx.destroy() diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index b0d3f7f149d5..4eec5b70c6a5 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -27,7 +27,7 @@ from matplotlib.artist import ( Artist, allow_rasterization, _finalize_rasterization) from matplotlib.backend_bases import ( - FigureCanvasBase, NonGuiException, MouseButton, _get_renderer) + DrawEvent, FigureCanvasBase, NonGuiException, MouseButton, _get_renderer) import matplotlib._api as _api import matplotlib.cbook as cbook import matplotlib.colorbar as cbar @@ -2979,7 +2979,7 @@ def draw(self, renderer): finally: self.stale = False - self.canvas.draw_event(renderer) + DrawEvent("draw_event", self.canvas, renderer)._process() def draw_without_rendering(self): """ diff --git a/lib/matplotlib/tests/test_backends_interactive.py b/lib/matplotlib/tests/test_backends_interactive.py index 9e920e33d355..fddb41a7a5aa 100644 --- a/lib/matplotlib/tests/test_backends_interactive.py +++ b/lib/matplotlib/tests/test_backends_interactive.py @@ -82,6 +82,7 @@ def _get_testable_interactive_backends(): # early. Also, gtk3 redefines key_press_event with a different signature, so # we directly invoke it from the superclass instead. def _test_interactive_impl(): + import importlib import importlib.util import io import json @@ -90,8 +91,7 @@ def _test_interactive_impl(): import matplotlib as mpl from matplotlib import pyplot as plt, rcParams - from matplotlib.backend_bases import FigureCanvasBase - + from matplotlib.backend_bases import KeyEvent rcParams.update({ "webagg.open_in_browser": False, "webagg.port_retries": 1, @@ -140,8 +140,8 @@ def check_alt_backend(alt_backend): if fig.canvas.toolbar: # i.e toolbar2. fig.canvas.toolbar.draw_rubberband(None, 1., 1, 2., 2) - timer = fig.canvas.new_timer(1.) # Test floats casting to int as needed. - timer.add_callback(FigureCanvasBase.key_press_event, fig.canvas, "q") + timer = fig.canvas.new_timer(1.) # Test that floats are cast to int. + timer.add_callback(KeyEvent("key_press_event", fig.canvas, "q")._process) # Trigger quitting upon draw. fig.canvas.mpl_connect("draw_event", lambda event: timer.start()) fig.canvas.mpl_connect("close_event", print) diff --git a/lib/matplotlib/tests/test_offsetbox.py b/lib/matplotlib/tests/test_offsetbox.py index 561fe230c2f7..dc71e14cdb08 100644 --- a/lib/matplotlib/tests/test_offsetbox.py +++ b/lib/matplotlib/tests/test_offsetbox.py @@ -9,7 +9,7 @@ import matplotlib.pyplot as plt import matplotlib.patches as mpatches import matplotlib.lines as mlines -from matplotlib.backend_bases import MouseButton +from matplotlib.backend_bases import MouseButton, MouseEvent from matplotlib.offsetbox import ( AnchoredOffsetbox, AnnotationBbox, AnchoredText, DrawingArea, OffsetBox, @@ -228,7 +228,8 @@ def test_picking(child_type, boxcoords): x, y = ax.transAxes.transform_point((0.5, 0.5)) fig.canvas.draw() calls.clear() - fig.canvas.button_press_event(x, y, MouseButton.LEFT) + MouseEvent( + "button_press_event", fig.canvas, x, y, MouseButton.LEFT)._process() assert len(calls) == 1 and calls[0].artist == ab # Annotation should *not* be picked by an event at its original center @@ -237,7 +238,8 @@ def test_picking(child_type, boxcoords): ax.set_ylim(-1, 0) fig.canvas.draw() calls.clear() - fig.canvas.button_press_event(x, y, MouseButton.LEFT) + MouseEvent( + "button_press_event", fig.canvas, x, y, MouseButton.LEFT)._process() assert len(calls) == 0 diff --git a/lib/matplotlib/tests/test_widgets.py b/lib/matplotlib/tests/test_widgets.py index 515b60a6da4f..b1028a6c6d6b 100644 --- a/lib/matplotlib/tests/test_widgets.py +++ b/lib/matplotlib/tests/test_widgets.py @@ -1,6 +1,7 @@ import functools from matplotlib._api.deprecation import MatplotlibDeprecationWarning +from matplotlib.backend_bases import MouseEvent import matplotlib.colors as mcolors import matplotlib.widgets as widgets import matplotlib.pyplot as plt @@ -1486,16 +1487,22 @@ def test_polygon_selector_box(ax): canvas = ax.figure.canvas # Scale to half size using the top right corner of the bounding box - canvas.button_press_event(*t.transform((40, 40)), 1) - canvas.motion_notify_event(*t.transform((20, 20))) - canvas.button_release_event(*t.transform((20, 20)), 1) + MouseEvent( + "button_press_event", canvas, *t.transform((40, 40)), 1)._process() + MouseEvent( + "motion_notify_event", canvas, *t.transform((20, 20)))._process() + MouseEvent( + "button_release_event", canvas, *t.transform((20, 20)), 1)._process() np.testing.assert_allclose( tool.verts, [(10, 0), (0, 10), (10, 20), (20, 10)]) # Move using the center of the bounding box - canvas.button_press_event(*t.transform((10, 10)), 1) - canvas.motion_notify_event(*t.transform((30, 30))) - canvas.button_release_event(*t.transform((30, 30)), 1) + MouseEvent( + "button_press_event", canvas, *t.transform((10, 10)), 1)._process() + MouseEvent( + "motion_notify_event", canvas, *t.transform((30, 30)))._process() + MouseEvent( + "button_release_event", canvas, *t.transform((30, 30)), 1)._process() np.testing.assert_allclose( tool.verts, [(30, 20), (20, 30), (30, 40), (40, 30)]) @@ -1503,8 +1510,10 @@ def test_polygon_selector_box(ax): np.testing.assert_allclose( tool._box.extents, (20.0, 40.0, 20.0, 40.0)) - canvas.button_press_event(*t.transform((30, 20)), 3) - canvas.button_release_event(*t.transform((30, 20)), 3) + MouseEvent( + "button_press_event", canvas, *t.transform((30, 20)), 3)._process() + MouseEvent( + "button_release_event", canvas, *t.transform((30, 20)), 3)._process() np.testing.assert_allclose( tool.verts, [(20, 30), (30, 40), (40, 30)]) np.testing.assert_allclose( diff --git a/src/_macosx.m b/src/_macosx.m index e98c2086b62d..d95c1a082a33 100755 --- a/src/_macosx.m +++ b/src/_macosx.m @@ -254,6 +254,21 @@ static void gil_call_method(PyObject* obj, const char* name) PyGILState_Release(gstate); } +#define PROCESS_EVENT(cls_name, fmt, ...) \ +{ \ + PyGILState_STATE gstate = PyGILState_Ensure(); \ + PyObject* module = NULL, * event = NULL, * result = NULL; \ + if (!(module = PyImport_ImportModule("matplotlib.backend_bases")) \ + || !(event = PyObject_CallMethod(module, cls_name, fmt, __VA_ARGS__)) \ + || !(result = PyObject_CallMethod(event, "_process", ""))) { \ + PyErr_Print(); \ + } \ + Py_XDECREF(module); \ + Py_XDECREF(event); \ + Py_XDECREF(result); \ + PyGILState_Release(gstate); \ +} + static bool backend_inited = false; static void lazy_init(void) { @@ -1337,16 +1352,7 @@ - (void)windowDidResize: (NSNotification*)notification - (void)windowWillClose:(NSNotification*)notification { - PyGILState_STATE gstate; - PyObject* result; - - gstate = PyGILState_Ensure(); - result = PyObject_CallMethod(canvas, "close_event", ""); - if (result) - Py_DECREF(result); - else - PyErr_Print(); - PyGILState_Release(gstate); + PROCESS_EVENT("CloseEvent", "sO", "close_event", canvas); } - (BOOL)windowShouldClose:(NSNotification*)notification @@ -1372,38 +1378,22 @@ - (BOOL)windowShouldClose:(NSNotification*)notification - (void)mouseEntered:(NSEvent *)event { - PyGILState_STATE gstate; - PyObject* result; - int x, y; NSPoint location = [event locationInWindow]; location = [self convertPoint: location fromView: nil]; x = location.x * device_scale; y = location.y * device_scale; - - gstate = PyGILState_Ensure(); - result = PyObject_CallMethod(canvas, "enter_notify_event", "O(ii)", - Py_None, x, y); - - if (result) - Py_DECREF(result); - else - PyErr_Print(); - PyGILState_Release(gstate); + PROCESS_EVENT("LocationEvent", "sOii", "figure_enter_event", canvas, x, y); } - (void)mouseExited:(NSEvent *)event { - PyGILState_STATE gstate; - PyObject* result; - - gstate = PyGILState_Ensure(); - result = PyObject_CallMethod(canvas, "leave_notify_event", ""); - if (result) - Py_DECREF(result); - else - PyErr_Print(); - PyGILState_Release(gstate); + int x, y; + NSPoint location = [event locationInWindow]; + location = [self convertPoint: location fromView: nil]; + x = location.x * device_scale; + y = location.y * device_scale; + PROCESS_EVENT("LocationEvent", "sOii", "figure_leave_event", canvas, x, y); } - (void)mouseDown:(NSEvent *)event @@ -1411,8 +1401,6 @@ - (void)mouseDown:(NSEvent *)event int x, y; int num; int dblclick = 0; - PyObject* result; - PyGILState_STATE gstate; NSPoint location = [event locationInWindow]; location = [self convertPoint: location fromView: nil]; x = location.x * device_scale; @@ -1441,22 +1429,14 @@ - (void)mouseDown:(NSEvent *)event if ([event clickCount] == 2) { dblclick = 1; } - gstate = PyGILState_Ensure(); - result = PyObject_CallMethod(canvas, "button_press_event", "iiii", x, y, num, dblclick); - if (result) - Py_DECREF(result); - else - PyErr_Print(); - - PyGILState_Release(gstate); + PROCESS_EVENT("MouseEvent", "sOiiiOii", "button_press_event", canvas, + x, y, num, Py_None /* key */, 0 /* step */, dblclick); } - (void)mouseUp:(NSEvent *)event { int num; int x, y; - PyObject* result; - PyGILState_STATE gstate; NSPoint location = [event locationInWindow]; location = [self convertPoint: location fromView: nil]; x = location.x * device_scale; @@ -1471,14 +1451,8 @@ - (void)mouseUp:(NSEvent *)event case NSEventTypeRightMouseUp: num = 3; break; default: return; /* Unknown mouse event */ } - gstate = PyGILState_Ensure(); - result = PyObject_CallMethod(canvas, "button_release_event", "iii", x, y, num); - if (result) - Py_DECREF(result); - else - PyErr_Print(); - - PyGILState_Release(gstate); + PROCESS_EVENT("MouseEvent", "sOiii", "button_release_event", canvas, + x, y, num); } - (void)mouseMoved:(NSEvent *)event @@ -1488,14 +1462,7 @@ - (void)mouseMoved:(NSEvent *)event location = [self convertPoint: location fromView: nil]; x = location.x * device_scale; y = location.y * device_scale; - PyGILState_STATE gstate = PyGILState_Ensure(); - PyObject* result = PyObject_CallMethod(canvas, "motion_notify_event", "ii", x, y); - if (result) - Py_DECREF(result); - else - PyErr_Print(); - - PyGILState_Release(gstate); + PROCESS_EVENT("MouseEvent", "sOii", "motion_notify_event", canvas, x, y); } - (void)mouseDragged:(NSEvent *)event @@ -1505,14 +1472,7 @@ - (void)mouseDragged:(NSEvent *)event location = [self convertPoint: location fromView: nil]; x = location.x * device_scale; y = location.y * device_scale; - PyGILState_STATE gstate = PyGILState_Ensure(); - PyObject* result = PyObject_CallMethod(canvas, "motion_notify_event", "ii", x, y); - if (result) - Py_DECREF(result); - else - PyErr_Print(); - - PyGILState_Release(gstate); + PROCESS_EVENT("MouseEvent", "sOii", "motion_notify_event", canvas, x, y); } - (void)rightMouseDown:(NSEvent *)event { [self mouseDown: event]; } @@ -1623,38 +1583,30 @@ - (const char*)convertKeyEvent:(NSEvent*)event - (void)keyDown:(NSEvent*)event { - PyObject* result; const char* s = [self convertKeyEvent: event]; - PyGILState_STATE gstate = PyGILState_Ensure(); - if (!s) { - result = PyObject_CallMethod(canvas, "key_press_event", "O", Py_None); + NSPoint location = [[self window] mouseLocationOutsideOfEventStream]; + location = [self convertPoint: location fromView: nil]; + int x = location.x * device_scale, + y = location.y * device_scale; + if (s) { + PROCESS_EVENT("KeyEvent", "sOsii", "key_press_event", canvas, s, x, y); } else { - result = PyObject_CallMethod(canvas, "key_press_event", "s", s); + PROCESS_EVENT("KeyEvent", "sOOii", "key_press_event", canvas, Py_None, x, y); } - if (result) - Py_DECREF(result); - else - PyErr_Print(); - - PyGILState_Release(gstate); } - (void)keyUp:(NSEvent*)event { - PyObject* result; const char* s = [self convertKeyEvent: event]; - PyGILState_STATE gstate = PyGILState_Ensure(); - if (!s) { - result = PyObject_CallMethod(canvas, "key_release_event", "O", Py_None); + NSPoint location = [[self window] mouseLocationOutsideOfEventStream]; + location = [self convertPoint: location fromView: nil]; + int x = location.x * device_scale, + y = location.y * device_scale; + if (s) { + PROCESS_EVENT("KeyEvent", "sOsii", "key_release_event", canvas, s, x, y); } else { - result = PyObject_CallMethod(canvas, "key_release_event", "s", s); + PROCESS_EVENT("KeyEvent", "sOOii", "key_release_event", canvas, Py_None, x, y); } - if (result) - Py_DECREF(result); - else - PyErr_Print(); - - PyGILState_Release(gstate); } - (void)scrollWheel:(NSEvent*)event @@ -1668,16 +1620,8 @@ - (void)scrollWheel:(NSEvent*)event NSPoint point = [self convertPoint: location fromView: nil]; int x = (int)round(point.x * device_scale); int y = (int)round(point.y * device_scale - 1); - - PyObject* result; - PyGILState_STATE gstate = PyGILState_Ensure(); - result = PyObject_CallMethod(canvas, "scroll_event", "iii", x, y, step); - if (result) - Py_DECREF(result); - else - PyErr_Print(); - - PyGILState_Release(gstate); + PROCESS_EVENT("MouseEvent", "sOiiOOi", "scroll_event", canvas, + x, y, Py_None /* button */, Py_None /* key */, step); } - (BOOL)acceptsFirstResponder From e26189be2703c4b6eb356c0c59b3fdf738f7649d Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Fri, 24 Sep 2021 13:52:40 +0200 Subject: [PATCH 2/6] Move emission of axes_enter/leave_event to a separate handler. --- lib/matplotlib/backend_bases.py | 32 ++++++++++++++++---------------- lib/matplotlib/figure.py | 4 +++- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 5574048d1ec4..114e6e2b8fef 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -1344,22 +1344,6 @@ def __init__(self, name, canvas, x, y, guiEvent=None): self.xdata = xdata self.ydata = ydata - def _process(self): - last = LocationEvent.lastevent - last_axes = last.inaxes if last is not None else None - if last_axes != self.inaxes: - if last_axes is not None: - try: - last.canvas.callbacks.process("axes_leave_event", last) - except Exception: - # The last canvas may already have been torn down. - pass - if self.inaxes is not None: - self.canvas.callbacks.process("axes_enter_event", self) - LocationEvent.lastevent = ( - None if self.name == "figure_leave_event" else self) - super()._process() - class MouseButton(IntEnum): LEFT = 1 @@ -1549,6 +1533,22 @@ def _process(self): super()._process() +def _axes_enter_leave_emitter(event): + last = LocationEvent.lastevent + last_axes = last.inaxes if last is not None else None + if last_axes != event.inaxes: + if last_axes is not None: + try: + last.canvas.callbacks.process("axes_leave_event", last) + except Exception: + # The last canvas may already have been torn down. + pass + if event.inaxes is not None: + event.canvas.callbacks.process("axes_enter_event", event) + LocationEvent.lastevent = ( + None if event.name == "figure_leave_event" else event) + + def _get_renderer(figure, print_method=None): """ Get the renderer that would be used to save a `.Figure`, and cache it on diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index 4eec5b70c6a5..73d8adccb2e9 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -23,7 +23,7 @@ import numpy as np import matplotlib as mpl -from matplotlib import _blocking_input, _docstring, projections +from matplotlib import _blocking_input, backend_bases, _docstring, projections from matplotlib.artist import ( Artist, allow_rasterization, _finalize_rasterization) from matplotlib.backend_bases import ( @@ -2376,6 +2376,8 @@ def __init__(self, 'button_press_event', self.pick) self._scroll_pick_id = self._canvas_callbacks._connect_picklable( 'scroll_event', self.pick) + self._axes_enter_leave_id = self._canvas_callbacks.connect( + 'motion_notify_event', backend_bases._axes_enter_leave_emitter) if figsize is None: figsize = mpl.rcParams['figure.figsize'] From f265579696bbf89ce94251e2d527ef888b30d0a2 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Tue, 12 Jul 2022 22:15:55 +0200 Subject: [PATCH 3/6] Remove unneeded delayed super-inits. These were only needed back when axes_enter_events were generated in the LocationEvent constructor, but this is now done by a standard callback. --- lib/matplotlib/backend_bases.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 114e6e2b8fef..2f93fb9f9ac3 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -1405,6 +1405,7 @@ def on_press(event): def __init__(self, name, canvas, x, y, button=None, key=None, step=0, dblclick=False, guiEvent=None): + super().__init__(name, canvas, x, y, guiEvent=guiEvent) if button in MouseButton.__members__.values(): button = MouseButton(button) if name == "scroll_event" and button is None: @@ -1417,10 +1418,6 @@ def __init__(self, name, canvas, x, y, button=None, key=None, self.step = step self.dblclick = dblclick - # super-init is deferred to the end because it calls back on - # 'axes_enter_event', which requires a fully initialized event. - super().__init__(name, canvas, x, y, guiEvent=guiEvent) - def _process(self): if self.name == "button_press_event": self.canvas._button = self.button @@ -1521,9 +1518,8 @@ def on_key(event): """ def __init__(self, name, canvas, key, x=0, y=0, guiEvent=None): - self.key = key - # super-init deferred to the end: callback errors if called before super().__init__(name, canvas, x, y, guiEvent=guiEvent) + self.key = key def _process(self): if self.name == "key_press_event": From f293fc6145a2e550fa43c3729d2dfe1a4f52fdfa Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Tue, 12 Jul 2022 22:27:33 +0200 Subject: [PATCH 4/6] Move key/mouse dead reckoning to standard callbacks. --- lib/matplotlib/backend_bases.py | 66 +++++++++++++++++---------------- lib/matplotlib/figure.py | 12 +++++- 2 files changed, 44 insertions(+), 34 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 2f93fb9f9ac3..85d4306f68db 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -1418,17 +1418,6 @@ def __init__(self, name, canvas, x, y, button=None, key=None, self.step = step self.dblclick = dblclick - def _process(self): - if self.name == "button_press_event": - self.canvas._button = self.button - elif self.name == "button_release_event": - self.canvas._button = None - elif self.name == "motion_notify_event" and self.button is None: - self.button = self.canvas._button - if self.key is None: - self.key = self.canvas._key - super()._process() - def __str__(self): return (f"{self.name}: " f"xy=({self.x}, {self.y}) xydata=({self.xdata}, {self.ydata}) " @@ -1521,28 +1510,41 @@ def __init__(self, name, canvas, key, x=0, y=0, guiEvent=None): super().__init__(name, canvas, x, y, guiEvent=guiEvent) self.key = key - def _process(self): - if self.name == "key_press_event": - self.canvas._key = self.key - elif self.name == "key_release_event": - self.canvas._key = None - super()._process() - -def _axes_enter_leave_emitter(event): - last = LocationEvent.lastevent - last_axes = last.inaxes if last is not None else None - if last_axes != event.inaxes: - if last_axes is not None: - try: - last.canvas.callbacks.process("axes_leave_event", last) - except Exception: - # The last canvas may already have been torn down. - pass - if event.inaxes is not None: - event.canvas.callbacks.process("axes_enter_event", event) - LocationEvent.lastevent = ( - None if event.name == "figure_leave_event" else event) +# Default callback for key events. +def _key_handler(event): + # Dead reckoning of key. + if event.name == "key_press_event": + event.canvas._key = event.key + elif event.name == "key_release_event": + event.canvas._key = None + + +# Default callback for mouse events. +def _mouse_handler(event): + # Dead-reckoning of button and key. + if event.name == "button_press_event": + event.canvas._button = event.button + elif event.name == "button_release_event": + event.canvas._button = None + elif event.name == "motion_notify_event" and event.button is None: + event.button = event.canvas._button + if event.key is None: + 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 + if last_axes != event.inaxes: + if last_axes is not None: + try: + last.canvas.callbacks.process("axes_leave_event", last) + 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 = ( + None if event.name == "figure_leave_event" else event) def _get_renderer(figure, print_method=None): diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index 73d8adccb2e9..c55864243a75 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -2376,8 +2376,16 @@ def __init__(self, 'button_press_event', self.pick) self._scroll_pick_id = self._canvas_callbacks._connect_picklable( 'scroll_event', self.pick) - self._axes_enter_leave_id = self._canvas_callbacks.connect( - 'motion_notify_event', backend_bases._axes_enter_leave_emitter) + connect = self._canvas_callbacks._connect_picklable + self._mouse_key_ids = [ + connect('key_press_event', backend_bases._key_handler), + connect('key_release_event', backend_bases._key_handler), + connect('key_release_event', backend_bases._key_handler), + connect('button_press_event', backend_bases._mouse_handler), + connect('button_release_event', backend_bases._mouse_handler), + connect('scroll_event', backend_bases._mouse_handler), + connect('motion_notify_event', backend_bases._mouse_handler), + ] if figsize is None: figsize = mpl.rcParams['figure.figsize'] From 6fe42adb22a8bd779806b76a1643a1a3c76b6f05 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Tue, 12 Jul 2022 22:36:00 +0200 Subject: [PATCH 5/6] Remove exception suppression in CloseEvent processing. CallbackRegistry already replaces exceptions by printed tracebacks, which seems better than fully suppressing everything. --- lib/matplotlib/backend_bases.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 85d4306f68db..3fb7a08401e5 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -1285,15 +1285,6 @@ def _process(self): class CloseEvent(Event): """An event triggered by a figure being closed.""" - def _process(self): - try: - super()._process() - except (AttributeError, TypeError): - pass - # Suppress AttributeError/TypeError that occur when the python - # session is being killed. It may be that a better solution would - # be a mechanism to disconnect all callbacks upon shutdown. - class LocationEvent(Event): """ From ff21516634242f6b6aed7eb21ba7f92dff3251fc Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Tue, 12 Jul 2022 22:37:24 +0200 Subject: [PATCH 6/6] Remove auto-call to canvas.draw_idle in ResizeEvent processing. Backends can call draw_idle themselves. Note that 1) this was already done by the gtk backends, and 2) this may actually be unneeded, as figure.set_size_inches (which is always called a bit earlier by the various resize handlers) also marks the figure as stale, which should trigger a redraw too. Still, let's add the draw_idle calls to be safe, they shouldn't be costly as both draws should get compressed together; we can always investigate removing them later. --- lib/matplotlib/backend_bases.py | 4 ---- lib/matplotlib/backends/_backend_tk.py | 1 + lib/matplotlib/backends/backend_qt.py | 1 + lib/matplotlib/backends/backend_webagg_core.py | 1 + lib/matplotlib/backends/backend_wx.py | 1 + 5 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 3fb7a08401e5..a86ca6727f43 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -1277,10 +1277,6 @@ def __init__(self, name, canvas): super().__init__(name, canvas) self.width, self.height = canvas.get_width_height() - def _process(self): - super()._process() - self.canvas.draw_idle() - class CloseEvent(Event): """An event triggered by a figure being closed.""" diff --git a/lib/matplotlib/backends/_backend_tk.py b/lib/matplotlib/backends/_backend_tk.py index 336f6e6d1a78..5d92e35469c2 100644 --- a/lib/matplotlib/backends/_backend_tk.py +++ b/lib/matplotlib/backends/_backend_tk.py @@ -241,6 +241,7 @@ def resize(self, event): self._tkcanvas.create_image( int(width / 2), int(height / 2), image=self._tkphoto) ResizeEvent("resize_event", self)._process() + self.draw_idle() def draw_idle(self): # docstring inherited diff --git a/lib/matplotlib/backends/backend_qt.py b/lib/matplotlib/backends/backend_qt.py index a83f760c243b..fd78ed1fce8d 100644 --- a/lib/matplotlib/backends/backend_qt.py +++ b/lib/matplotlib/backends/backend_qt.py @@ -348,6 +348,7 @@ def resizeEvent(self, event): QtWidgets.QWidget.resizeEvent(self, event) # emit our resize events ResizeEvent("resize_event", self)._process() + self.draw_idle() def sizeHint(self): w, h = self.get_width_height() diff --git a/lib/matplotlib/backends/backend_webagg_core.py b/lib/matplotlib/backends/backend_webagg_core.py index 46d5862ab439..9e31efb83622 100644 --- a/lib/matplotlib/backends/backend_webagg_core.py +++ b/lib/matplotlib/backends/backend_webagg_core.py @@ -343,6 +343,7 @@ def handle_resize(self, event): self._png_is_old = True self.manager.resize(*fig.bbox.size, forward=False) ResizeEvent('resize_event', self)._process() + self.draw_idle() def handle_send_image_mode(self, event): # The client requests notification of what the current image mode is. diff --git a/lib/matplotlib/backends/backend_wx.py b/lib/matplotlib/backends/backend_wx.py index 0ba0d272f6f7..811261bee475 100644 --- a/lib/matplotlib/backends/backend_wx.py +++ b/lib/matplotlib/backends/backend_wx.py @@ -705,6 +705,7 @@ def _on_size(self, event): # the whole background is repainted. self.Refresh(eraseBackground=False) ResizeEvent("resize_event", self)._process() + self.draw_idle() def _get_key(self, event):