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..a86ca6727f43 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,6 +1272,7 @@ 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() @@ -1294,7 +1300,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 +1314,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,34 +1331,6 @@ 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 - try: - if last.inaxes is not None: - last.canvas.callbacks.process('axes_leave_event', last) - except Exception: - 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 - class MouseButton(IntEnum): LEFT = 1 @@ -1375,11 +1352,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`. @@ -1411,17 +1392,19 @@ 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: + if step > 0: + button = "up" + elif step < 0: + button = "down" self.button = button self.key = key 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 __str__(self): return (f"{self.name}: " f"xy=({self.x}, {self.y}) xydata=({self.xdata}, {self.ydata}) " @@ -1467,8 +1450,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,10 +1492,46 @@ 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) + self.key = key + + +# 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): @@ -1720,12 +1742,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 +1761,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 +1779,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 +1791,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 +1803,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 +1821,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 +1843,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 +1862,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 +1888,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 +1916,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 +1934,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..5d92e35469c2 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,8 @@ 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() + self.draw_idle() def draw_idle(self): # docstring inherited @@ -271,12 +273,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 +294,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 +305,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 +369,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..fd78ed1fce8d 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,8 @@ 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() + self.draw_idle() def sizeHint(self): w, h = self.get_width_height() @@ -503,7 +515,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..9e31efb83622 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,8 @@ 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() + 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 3eaf59e3f502..811261bee475 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,8 @@ 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() + self.draw_idle() def _get_key(self, event): @@ -730,17 +732,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 +790,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 +801,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 +826,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 +964,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..c55864243a75 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -23,11 +23,11 @@ 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 ( - 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 @@ -2376,6 +2376,16 @@ def __init__(self, 'button_press_event', self.pick) self._scroll_pick_id = self._canvas_callbacks._connect_picklable( 'scroll_event', self.pick) + 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'] @@ -2979,7 +2989,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