From 49724bfc05ff798df3d7a00d17aef15da189edc9 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Sun, 1 Aug 2021 12:28:08 +0200 Subject: [PATCH 1/2] Construct events with kwargs in macosx. --- src/_macosx.m | 115 +++++++++++++++++++++++++++++++++----------------- 1 file changed, 76 insertions(+), 39 deletions(-) diff --git a/src/_macosx.m b/src/_macosx.m index 2a2d9ccd6259..36e687d043c1 100755 --- a/src/_macosx.m +++ b/src/_macosx.m @@ -250,19 +250,30 @@ 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); \ +void process_event(char const* cls_name, char const* fmt, ...) +{ + PyGILState_STATE gstate = PyGILState_Ensure(); + PyObject* module = NULL, * cls = NULL, + * args = NULL, * kwargs = NULL, + * event = NULL, * result = NULL; + va_list argp; + va_start(argp, fmt); + if (!(module = PyImport_ImportModule("matplotlib.backend_bases")) + || !(cls = PyObject_GetAttrString(module, cls_name)) + || !(args = PyTuple_New(0)) + || !(kwargs = Py_VaBuildValue(fmt, argp)) + || !(event = PyObject_Call(cls, args, kwargs)) + || !(result = PyObject_CallMethod(event, "_process", ""))) { + PyErr_Print(); + } + va_end(argp); + Py_XDECREF(module); + Py_XDECREF(cls); + Py_XDECREF(args); + Py_XDECREF(kwargs); + Py_XDECREF(event); + Py_XDECREF(result); + PyGILState_Release(gstate); } static bool backend_inited = false; @@ -1363,7 +1374,9 @@ - (void)updateDevicePixelRatio:(double)scale } if (PyObject_IsTrue(change)) { // Notify that there was a resize_event that took place - PROCESS_EVENT("ResizeEvent", "sO", "resize_event", canvas); + process_event( + "ResizeEvent", "{s:s, s:O}", + "name", "resize_event", "canvas", canvas); gil_call_method(canvas, "draw_idle"); [self setNeedsDisplay: YES]; } @@ -1405,7 +1418,9 @@ - (void)windowDidResize: (NSNotification*)notification - (void)windowWillClose:(NSNotification*)notification { - PROCESS_EVENT("CloseEvent", "sO", "close_event", canvas); + process_event( + "CloseEvent", "{s:s, s:O}", + "name", "close_event", "canvas", canvas); } - (BOOL)windowShouldClose:(NSNotification*)notification @@ -1436,7 +1451,9 @@ - (void)mouseEntered:(NSEvent *)event location = [self convertPoint: location fromView: nil]; x = location.x * device_scale; y = location.y * device_scale; - PROCESS_EVENT("LocationEvent", "sOii", "figure_enter_event", canvas, x, y); + process_event( + "LocationEvent", "{s:s, s:O, s:i, s:i}", + "name", "figure_enter_event", "canvas", canvas, "x", x, "y", y); } - (void)mouseExited:(NSEvent *)event @@ -1446,13 +1463,15 @@ - (void)mouseExited:(NSEvent *)event 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); + process_event( + "LocationEvent", "{s:s, s:O, s:i, s:i}", + "name", "figure_leave_event", "canvas", canvas, "x", x, "y", y); } - (void)mouseDown:(NSEvent *)event { int x, y; - int num; + int button; int dblclick = 0; NSPoint location = [event locationInWindow]; location = [self convertPoint: location fromView: nil]; @@ -1463,32 +1482,34 @@ - (void)mouseDown:(NSEvent *)event { unsigned int modifier = [event modifierFlags]; if (modifier & NSEventModifierFlagControl) /* emulate a right-button click */ - num = 3; + button = 3; else if (modifier & NSEventModifierFlagOption) /* emulate a middle-button click */ - num = 2; + button = 2; else { - num = 1; + button = 1; if ([NSCursor currentCursor]==[NSCursor openHandCursor]) [[NSCursor closedHandCursor] set]; } break; } - case NSEventTypeOtherMouseDown: num = 2; break; - case NSEventTypeRightMouseDown: num = 3; break; + case NSEventTypeOtherMouseDown: button = 2; break; + case NSEventTypeRightMouseDown: button = 3; break; default: return; /* Unknown mouse event */ } if ([event clickCount] == 2) { dblclick = 1; } - PROCESS_EVENT("MouseEvent", "sOiiiOii", "button_press_event", canvas, - x, y, num, Py_None /* key */, 0 /* step */, dblclick); + process_event( + "MouseEvent", "{s:s, s:O, s:i, s:i, s:i, s:i}", + "name", "button_press_event", "canvas", canvas, "x", x, "y", y, + "button", button, "dblclick", dblclick); } - (void)mouseUp:(NSEvent *)event { - int num; + int button; int x, y; NSPoint location = [event locationInWindow]; location = [self convertPoint: location fromView: nil]; @@ -1496,16 +1517,18 @@ - (void)mouseUp:(NSEvent *)event y = location.y * device_scale; switch ([event type]) { case NSEventTypeLeftMouseUp: - num = 1; + button = 1; if ([NSCursor currentCursor]==[NSCursor closedHandCursor]) [[NSCursor openHandCursor] set]; break; - case NSEventTypeOtherMouseUp: num = 2; break; - case NSEventTypeRightMouseUp: num = 3; break; + case NSEventTypeOtherMouseUp: button = 2; break; + case NSEventTypeRightMouseUp: button = 3; break; default: return; /* Unknown mouse event */ } - PROCESS_EVENT("MouseEvent", "sOiii", "button_release_event", canvas, - x, y, num); + process_event( + "MouseEvent", "{s:s, s:O, s:i, s:i, s:i}", + "name", "button_release_event", "canvas", canvas, "x", x, "y", y, + "button", button); } - (void)mouseMoved:(NSEvent *)event @@ -1515,7 +1538,9 @@ - (void)mouseMoved:(NSEvent *)event location = [self convertPoint: location fromView: nil]; x = location.x * device_scale; y = location.y * device_scale; - PROCESS_EVENT("MouseEvent", "sOii", "motion_notify_event", canvas, x, y); + process_event( + "MouseEvent", "{s:s, s:O, s:i, s:i}", + "name", "motion_notify_event", "canvas", canvas, "x", x, "y", y); } - (void)mouseDragged:(NSEvent *)event @@ -1525,7 +1550,9 @@ - (void)mouseDragged:(NSEvent *)event location = [self convertPoint: location fromView: nil]; x = location.x * device_scale; y = location.y * device_scale; - PROCESS_EVENT("MouseEvent", "sOii", "motion_notify_event", canvas, x, y); + process_event( + "MouseEvent", "{s:s, s:O, s:i, s:i}", + "name", "motion_notify_event", "canvas", canvas, "x", x, "y", y); } - (void)rightMouseDown:(NSEvent *)event { [self mouseDown: event]; } @@ -1644,9 +1671,13 @@ - (void)keyDown:(NSEvent*)event int x = location.x * device_scale, y = location.y * device_scale; if (s) { - PROCESS_EVENT("KeyEvent", "sOsii", "key_press_event", canvas, s, x, y); + process_event( + "KeyEvent", "{s:s, s:O, s:s, s:i, s:i}", + "name", "key_press_event", "canvas", canvas, "key", s, "x", x, "y", y); } else { - PROCESS_EVENT("KeyEvent", "sOOii", "key_press_event", canvas, Py_None, x, y); + process_event( + "KeyEvent", "{s:s, s:O, s:O, s:i, s:i}", + "name", "key_press_event", "canvas", canvas, "key", Py_None, "x", x, "y", y); } } @@ -1658,9 +1689,13 @@ - (void)keyUp:(NSEvent*)event int x = location.x * device_scale, y = location.y * device_scale; if (s) { - PROCESS_EVENT("KeyEvent", "sOsii", "key_release_event", canvas, s, x, y); + process_event( + "KeyEvent", "{s:s, s:O, s:s, s:i, s:i}", + "name", "key_release_event", "canvas", canvas, "key", s, "x", x, "y", y); } else { - PROCESS_EVENT("KeyEvent", "sOOii", "key_release_event", canvas, Py_None, x, y); + process_event( + "KeyEvent", "{s:s, s:O, s:O, s:i, s:i}", + "name", "key_release_event", "canvas", canvas, "key", Py_None, "x", x, "y", y); } } @@ -1675,8 +1710,10 @@ - (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); - PROCESS_EVENT("MouseEvent", "sOiiOOi", "scroll_event", canvas, - x, y, Py_None /* button */, Py_None /* key */, step); + process_event( + "MouseEvent", "{s:s, s:O, s:i, s:i, s:i}", + "name", "scroll_event", "canvas", canvas, + "x", x, "y", y, "step", step); } - (BOOL)acceptsFirstResponder From b4e9e3131cdd7f1ad33ea06e21e7d3e51762af91 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Sun, 1 Aug 2021 16:23:49 +0200 Subject: [PATCH 2/2] Separately track modifier keys for mouse events. Whether the event modifiers are directly available on enter/leave events depends on the backend, but all are handled here (except possibly for macos, which I haven't checked). --- lib/matplotlib/backend_bases.py | 14 ++- lib/matplotlib/backends/_backend_tk.py | 64 +++++++------ lib/matplotlib/backends/backend_gtk3.py | 42 ++++++--- lib/matplotlib/backends/backend_gtk4.py | 91 ++++++++++++------- lib/matplotlib/backends/backend_qt.py | 33 +++++-- .../backends/backend_webagg_core.py | 11 ++- lib/matplotlib/backends/backend_wx.py | 55 ++++++----- lib/matplotlib/backends/web_backend/js/mpl.js | 18 ++++ lib/matplotlib/tests/test_backend_qt.py | 6 +- src/_macosx.m | 67 +++++++++++--- 10 files changed, 272 insertions(+), 129 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 7100f2ed34a8..0fd39b04c470 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -1316,11 +1316,13 @@ class LocationEvent(Event): xdata, ydata : float or None Data coordinates of the mouse within *inaxes*, or *None* if the mouse is not over an Axes. + modifiers : frozenset + The keyboard modifiers currently being pressed (except for KeyEvent). """ lastevent = None # The last event processed so far. - def __init__(self, name, canvas, x, y, guiEvent=None): + def __init__(self, name, canvas, x, y, guiEvent=None, *, modifiers=None): super().__init__(name, canvas, guiEvent=guiEvent) # x position - pixels from left of canvas self.x = int(x) if x is not None else x @@ -1329,6 +1331,7 @@ def __init__(self, name, canvas, x, y, guiEvent=None): self.inaxes = None # the Axes instance the mouse is over self.xdata = None # x coord of mouse in data coords self.ydata = None # y coord of mouse in data coords + self.modifiers = frozenset(modifiers if modifiers is not None else []) if x is None or y is None: # cannot check if event was in Axes if no (x, y) info @@ -1387,7 +1390,9 @@ class MouseEvent(LocationEvent): This key is currently obtained from the last 'key_press_event' or 'key_release_event' that occurred within the canvas. Thus, if the last change of keyboard state occurred while the canvas did not have - focus, this attribute will be wrong. + focus, this attribute will be wrong. On the other hand, the + ``modifiers`` attribute should always be correct, but it can only + report on modifier keys. step : float The number of scroll steps (positive for 'up', negative for 'down'). @@ -1409,8 +1414,9 @@ 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) + step=0, dblclick=False, guiEvent=None, *, modifiers=None): + super().__init__( + name, canvas, x, y, guiEvent=guiEvent, modifiers=modifiers) if button in MouseButton.__members__.values(): button = MouseButton(button) if name == "scroll_event" and button is None: diff --git a/lib/matplotlib/backends/_backend_tk.py b/lib/matplotlib/backends/_backend_tk.py index 2a691f55b974..bdbbd6ad6c77 100644 --- a/lib/matplotlib/backends/_backend_tk.py +++ b/lib/matplotlib/backends/_backend_tk.py @@ -273,16 +273,19 @@ def _event_mpl_coords(self, event): def motion_notify_event(self, event): MouseEvent("motion_notify_event", self, *self._event_mpl_coords(event), + modifiers=self._mpl_modifiers(event), guiEvent=event)._process() def enter_notify_event(self, event): LocationEvent("figure_enter_event", self, *self._event_mpl_coords(event), + modifiers=self._mpl_modifiers(event), guiEvent=event)._process() def leave_notify_event(self, event): LocationEvent("figure_leave_event", self, *self._event_mpl_coords(event), + modifiers=self._mpl_modifiers(event), guiEvent=event)._process() def button_press_event(self, event, dblclick=False): @@ -294,6 +297,7 @@ def button_press_event(self, event, dblclick=False): num = {2: 3, 3: 2}.get(num, num) MouseEvent("button_press_event", self, *self._event_mpl_coords(event), num, dblclick=dblclick, + modifiers=self._mpl_modifiers(event), guiEvent=event)._process() def button_dblclick_event(self, event): @@ -305,6 +309,7 @@ def button_release_event(self, event): num = {2: 3, 3: 2}.get(num, num) MouseEvent("button_release_event", self, *self._event_mpl_coords(event), num, + modifiers=self._mpl_modifiers(event), guiEvent=event)._process() def scroll_event(self, event): @@ -312,6 +317,7 @@ def scroll_event(self, event): step = 1 if num == 4 else -1 if num == 5 else 0 MouseEvent("scroll_event", self, *self._event_mpl_coords(event), step=step, + modifiers=self._mpl_modifiers(event), guiEvent=event)._process() def scroll_event_windows(self, event): @@ -325,12 +331,11 @@ def scroll_event_windows(self, event): - 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 - key = cbook._unikey_or_keysym_to_mplkey(unikey, event.keysym) + x, y, step=step, modifiers=self._mpl_modifiers(event), + guiEvent=event)._process() + @staticmethod + def _mpl_modifiers(event, *, exclude=None): # add modifier keys to the key string. Bit details originate from # http://effbot.org/tkinterbook/tkinter-events-and-bindings.htm # BIT_SHIFT = 0x001; BIT_CAPSLOCK = 0x002; BIT_CONTROL = 0x004; @@ -339,32 +344,33 @@ def _get_key(self, event): # In general, the modifier key is excluded from the modifier flag, # however this is not the case on "darwin", so double check that # we aren't adding repeat modifier flags to a modifier key. - if sys.platform == 'win32': - modifiers = [(2, 'ctrl', 'control'), - (17, 'alt', 'alt'), - (0, 'shift', 'shift'), - ] - elif sys.platform == 'darwin': - modifiers = [(2, 'ctrl', 'control'), - (4, 'alt', 'alt'), - (0, 'shift', 'shift'), - (3, 'super', 'super'), - ] - else: - modifiers = [(2, 'ctrl', 'control'), - (3, 'alt', 'alt'), - (0, 'shift', 'shift'), - (6, 'super', 'super'), - ] + modifiers = [ + ("ctrl", 1 << 2, "control"), + ("alt", 1 << 17, "alt"), + ("shift", 1 << 0, "shift"), + ] if sys.platform == "win32" else [ + ("ctrl", 1 << 2, "control"), + ("alt", 1 << 4, "alt"), + ("shift", 1 << 0, "shift"), + ("super", 1 << 3, "super"), + ] if sys.platform == "darwin" else [ + ("ctrl", 1 << 2, "control"), + ("alt", 1 << 3, "alt"), + ("shift", 1 << 0, "shift"), + ("super", 1 << 6, "super"), + ] + return [name for name, mask, key in modifiers + if event.state & mask and exclude != key] + def _get_key(self, event): + unikey = event.char + key = cbook._unikey_or_keysym_to_mplkey(unikey, event.keysym) if key is not None: - # shift is not added to the keys as this is already accounted for - for bitmask, prefix, key_name in modifiers: - if event.state & (1 << bitmask) and key_name not in key: - if not (prefix == 'shift' and unikey): - key = '{0}+{1}'.format(prefix, key) - - return key + mods = self._mpl_modifiers(event, exclude=key) + # shift is not added to the keys as this is already accounted for. + if "shift" in mods and unikey: + mods.remove("shift") + return "+".join([*mods, key]) def key_press(self, event): KeyEvent("key_press_event", self, diff --git a/lib/matplotlib/backends/backend_gtk3.py b/lib/matplotlib/backends/backend_gtk3.py index 4994e434987d..dbb0982ee752 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -133,19 +133,23 @@ def _mpl_coords(self, event=None): def scroll_event(self, widget, event): step = 1 if event.direction == Gdk.ScrollDirection.UP else -1 - MouseEvent("scroll_event", self, *self._mpl_coords(event), step=step, + MouseEvent("scroll_event", self, + *self._mpl_coords(event), step=step, + modifiers=self._mpl_modifiers(event.state), guiEvent=event)._process() return False # finish event propagation? def button_press_event(self, widget, event): MouseEvent("button_press_event", self, *self._mpl_coords(event), event.button, + modifiers=self._mpl_modifiers(event.state), guiEvent=event)._process() return False # finish event propagation? def button_release_event(self, widget, event): MouseEvent("button_release_event", self, *self._mpl_coords(event), event.button, + modifiers=self._mpl_modifiers(event.state), guiEvent=event)._process() return False # finish event propagation? @@ -163,15 +167,22 @@ def key_release_event(self, widget, event): def motion_notify_event(self, widget, event): MouseEvent("motion_notify_event", self, *self._mpl_coords(event), + modifiers=self._mpl_modifiers(event.state), guiEvent=event)._process() return False # finish event propagation? def enter_notify_event(self, widget, event): + gtk_mods = Gdk.Keymap.get_for_display( + self.get_display()).get_modifier_state() LocationEvent("figure_enter_event", self, *self._mpl_coords(event), + modifiers=self._mpl_modifiers(gtk_mods), guiEvent=event)._process() def leave_notify_event(self, widget, event): + gtk_mods = Gdk.Keymap.get_for_display( + self.get_display()).get_modifier_state() LocationEvent("figure_leave_event", self, *self._mpl_coords(event), + modifiers=self._mpl_modifiers(gtk_mods), guiEvent=event)._process() def size_allocate(self, widget, allocation): @@ -182,22 +193,25 @@ def size_allocate(self, widget, allocation): ResizeEvent("resize_event", self)._process() self.draw_idle() + @staticmethod + def _mpl_modifiers(event_state, *, exclude=None): + modifiers = [ + ("ctrl", Gdk.ModifierType.CONTROL_MASK, "control"), + ("alt", Gdk.ModifierType.MOD1_MASK, "alt"), + ("shift", Gdk.ModifierType.SHIFT_MASK, "shift"), + ("super", Gdk.ModifierType.MOD4_MASK, "super"), + ] + return [name for name, mask, key in modifiers + if exclude != key and event_state & mask] + def _get_key(self, event): unikey = chr(Gdk.keyval_to_unicode(event.keyval)) key = cbook._unikey_or_keysym_to_mplkey( - unikey, - Gdk.keyval_name(event.keyval)) - modifiers = [ - (Gdk.ModifierType.CONTROL_MASK, 'ctrl'), - (Gdk.ModifierType.MOD1_MASK, 'alt'), - (Gdk.ModifierType.SHIFT_MASK, 'shift'), - (Gdk.ModifierType.MOD4_MASK, 'super'), - ] - for key_mask, prefix in modifiers: - if event.state & key_mask: - if not (prefix == 'shift' and unikey.isprintable()): - key = f'{prefix}+{key}' - return key + unikey, Gdk.keyval_name(event.keyval)) + mods = self._mpl_modifiers(event.state, exclude=key) + if "shift" in mods and unikey.isprintable(): + mods.remove("shift") + return "+".join([*mods, key]) def _update_device_pixel_ratio(self, *args, **kwargs): # We need to be careful in cases with mixed resolution displays if diff --git a/lib/matplotlib/backends/backend_gtk4.py b/lib/matplotlib/backends/backend_gtk4.py index a058fc6f8a2d..8628e14de096 100644 --- a/lib/matplotlib/backends/backend_gtk4.py +++ b/lib/matplotlib/backends/backend_gtk4.py @@ -109,44 +109,58 @@ def _mpl_coords(self, xy=None): return x, y def scroll_event(self, controller, dx, dy): - MouseEvent("scroll_event", self, - *self._mpl_coords(), step=dy)._process() + MouseEvent( + "scroll_event", self, *self._mpl_coords(), step=dy, + modifiers=self._mpl_modifiers(controller), + )._process() return True def button_press_event(self, controller, n_press, x, y): - MouseEvent("button_press_event", self, - *self._mpl_coords((x, y)), controller.get_current_button() - )._process() + MouseEvent( + "button_press_event", self, *self._mpl_coords((x, y)), + controller.get_current_button(), + modifiers=self._mpl_modifiers(controller), + )._process() self.grab_focus() def button_release_event(self, controller, n_press, x, y): - MouseEvent("button_release_event", self, - *self._mpl_coords((x, y)), controller.get_current_button() - )._process() + MouseEvent( + "button_release_event", self, *self._mpl_coords((x, y)), + controller.get_current_button(), + modifiers=self._mpl_modifiers(controller), + )._process() def key_press_event(self, controller, keyval, keycode, state): - KeyEvent("key_press_event", self, - self._get_key(keyval, keycode, state), *self._mpl_coords() - )._process() + 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): - KeyEvent("key_release_event", self, - self._get_key(keyval, keycode, state), *self._mpl_coords() - )._process() + 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): - MouseEvent("motion_notify_event", self, - *self._mpl_coords((x, y)))._process() - - def leave_notify_event(self, controller): - LocationEvent("figure_leave_event", self, - *self._mpl_coords())._process() + MouseEvent( + "motion_notify_event", self, *self._mpl_coords((x, y)), + modifiers=self._mpl_modifiers(controller), + )._process() def enter_notify_event(self, controller, x, y): - LocationEvent("figure_enter_event", self, - *self._mpl_coords((x, y)))._process() + LocationEvent( + "figure_enter_event", self, *self._mpl_coords((x, y)), + modifiers=self._mpl_modifiers(), + )._process() + + def leave_notify_event(self, controller): + LocationEvent( + "figure_leave_event", self, *self._mpl_coords(), + modifiers=self._mpl_modifiers(), + )._process() def resize_event(self, area, width, height): self._update_device_pixel_ratio() @@ -157,22 +171,37 @@ def resize_event(self, area, width, height): ResizeEvent("resize_event", self)._process() self.draw_idle() + def _mpl_modifiers(self, controller=None): + if controller is None: + surface = self.get_native().get_surface() + is_over, x, y, event_state = surface.get_device_position( + self.get_display().get_default_seat().get_pointer()) + else: + event_state = controller.get_current_event_state() + mod_table = [ + ("ctrl", Gdk.ModifierType.CONTROL_MASK), + ("alt", Gdk.ModifierType.ALT_MASK), + ("shift", Gdk.ModifierType.SHIFT_MASK), + ("super", Gdk.ModifierType.SUPER_MASK), + ] + return [name for name, mask in mod_table if event_state & mask] + def _get_key(self, keyval, keycode, state): unikey = chr(Gdk.keyval_to_unicode(keyval)) key = cbook._unikey_or_keysym_to_mplkey( unikey, Gdk.keyval_name(keyval)) modifiers = [ - (Gdk.ModifierType.CONTROL_MASK, 'ctrl'), - (Gdk.ModifierType.ALT_MASK, 'alt'), - (Gdk.ModifierType.SHIFT_MASK, 'shift'), - (Gdk.ModifierType.SUPER_MASK, 'super'), + ("ctrl", Gdk.ModifierType.CONTROL_MASK, "control"), + ("alt", Gdk.ModifierType.ALT_MASK, "alt"), + ("shift", Gdk.ModifierType.SHIFT_MASK, "shift"), + ("super", Gdk.ModifierType.SUPER_MASK, "super"), ] - for key_mask, prefix in modifiers: - if state & key_mask: - if not (prefix == 'shift' and unikey.isprintable()): - key = f'{prefix}+{key}' - return key + mods = [ + mod for mod, mask, mod_key in modifiers + if (mod_key != key and state & mask + and not (mod == "shift" and unikey.isprintable()))] + return "+".join([*mods, key]) def _update_device_pixel_ratio(self, *args, **kwargs): # We need to be careful in cases with mixed resolution displays if diff --git a/lib/matplotlib/backends/backend_qt.py b/lib/matplotlib/backends/backend_qt.py index c19cc240ba48..ad9be6000a23 100644 --- a/lib/matplotlib/backends/backend_qt.py +++ b/lib/matplotlib/backends/backend_qt.py @@ -268,14 +268,19 @@ def mouseEventCoords(self, pos=None): return x * self.device_pixel_ratio, y * self.device_pixel_ratio def enterEvent(self, event): + # Force querying of the modifiers, as the cached modifier state can + # have been invalidated while the window was out of focus. + mods = QtWidgets.QApplication.instance().queryKeyboardModifiers() LocationEvent("figure_enter_event", self, *self.mouseEventCoords(event), + modifiers=self._mpl_modifiers(mods), guiEvent=event)._process() def leaveEvent(self, event): QtWidgets.QApplication.restoreOverrideCursor() LocationEvent("figure_leave_event", self, *self.mouseEventCoords(), + modifiers=self._mpl_modifiers(), guiEvent=event)._process() def mousePressEvent(self, event): @@ -283,6 +288,7 @@ def mousePressEvent(self, event): if button is not None: MouseEvent("button_press_event", self, *self.mouseEventCoords(event), button, + modifiers=self._mpl_modifiers(), guiEvent=event)._process() def mouseDoubleClickEvent(self, event): @@ -290,11 +296,13 @@ def mouseDoubleClickEvent(self, event): if button is not None: MouseEvent("button_press_event", self, *self.mouseEventCoords(event), button, dblclick=True, + modifiers=self._mpl_modifiers(), guiEvent=event)._process() def mouseMoveEvent(self, event): MouseEvent("motion_notify_event", self, *self.mouseEventCoords(event), + modifiers=self._mpl_modifiers(), guiEvent=event)._process() def mouseReleaseEvent(self, event): @@ -302,6 +310,7 @@ def mouseReleaseEvent(self, event): if button is not None: MouseEvent("button_release_event", self, *self.mouseEventCoords(event), button, + modifiers=self._mpl_modifiers(), guiEvent=event)._process() def wheelEvent(self, event): @@ -315,6 +324,7 @@ def wheelEvent(self, event): if steps: MouseEvent("scroll_event", self, *self.mouseEventCoords(event), step=steps, + modifiers=self._mpl_modifiers(), guiEvent=event)._process() def keyPressEvent(self, event): @@ -357,18 +367,23 @@ def sizeHint(self): def minumumSizeHint(self): return QtCore.QSize(10, 10) - def _get_key(self, event): - event_key = event.key() - event_mods = _to_int(event.modifiers()) # actually a bitmask - + @staticmethod + def _mpl_modifiers(modifiers=None, *, exclude=None): + if modifiers is None: + modifiers = QtWidgets.QApplication.instance().keyboardModifiers() + modifiers = _to_int(modifiers) # get names of the pressed modifier keys # 'control' is named 'control' when a standalone key, but 'ctrl' when a # modifier - # bit twiddling to pick out modifier keys from event_mods bitmask, - # if event_key is a MODIFIER, it should not be duplicated in mods - mods = [SPECIAL_KEYS[key].replace('control', 'ctrl') - for mod, key in _MODIFIER_KEYS - if event_key != key and event_mods & mod] + # bit twiddling to pick out modifier keys from modifiers bitmask, + # if exclude is a MODIFIER, it should not be duplicated in mods + return [SPECIAL_KEYS[key].replace('control', 'ctrl') + for mask, key in _MODIFIER_KEYS + if exclude != key and modifiers & mask] + + def _get_key(self, event): + event_key = event.key() + mods = self._mpl_modifiers(exclude=event_key) try: # for certain keys (enter, left, backspace, etc) use a word for the # key, rather than Unicode diff --git a/lib/matplotlib/backends/backend_webagg_core.py b/lib/matplotlib/backends/backend_webagg_core.py index ee2d73d0cf95..232fa10616b4 100644 --- a/lib/matplotlib/backends/backend_webagg_core.py +++ b/lib/matplotlib/backends/backend_webagg_core.py @@ -290,22 +290,23 @@ def _handle_mouse(self, event): button = event['button'] + 1 e_type = event['type'] + modifiers = event['modifiers'] guiEvent = event.get('guiEvent') if e_type in ['button_press', 'button_release']: MouseEvent(e_type + '_event', self, x, y, button, - guiEvent=guiEvent)._process() + modifiers=modifiers, guiEvent=guiEvent)._process() elif e_type == 'dblclick': MouseEvent('button_press_event', self, x, y, button, dblclick=True, - guiEvent=guiEvent)._process() + modifiers=modifiers, guiEvent=guiEvent)._process() elif e_type == 'scroll': MouseEvent('scroll_event', self, x, y, step=event['step'], - guiEvent=guiEvent)._process() + modifiers=modifiers, guiEvent=guiEvent)._process() elif e_type == 'motion_notify': MouseEvent(e_type + '_event', self, x, y, - guiEvent=guiEvent)._process() + modifiers=modifiers, guiEvent=guiEvent)._process() elif e_type in ['figure_enter', 'figure_leave']: LocationEvent(e_type + '_event', self, x, y, - guiEvent=guiEvent)._process() + modifiers=modifiers, guiEvent=guiEvent)._process() handle_button_press = handle_button_release = handle_dblclick = \ handle_figure_enter = handle_figure_leave = handle_motion_notify = \ handle_scroll = _handle_mouse diff --git a/lib/matplotlib/backends/backend_wx.py b/lib/matplotlib/backends/backend_wx.py index 0333f512ae66..8de879a3c02d 100644 --- a/lib/matplotlib/backends/backend_wx.py +++ b/lib/matplotlib/backends/backend_wx.py @@ -696,8 +696,22 @@ def _on_size(self, event): ResizeEvent("resize_event", self)._process() self.draw_idle() - def _get_key(self, event): + @staticmethod + def _mpl_modifiers(event=None, *, exclude=None): + mod_table = [ + ("ctrl", wx.MOD_CONTROL, wx.WXK_CONTROL), + ("alt", wx.MOD_ALT, wx.WXK_ALT), + ("shift", wx.MOD_SHIFT, wx.WXK_SHIFT), + ] + if event is not None: + modifiers = event.GetModifiers() + return [name for name, mod, key in mod_table + if modifiers & mod and exclude != key] + else: + return [name for name, mod, key in mod_table + if wx.GetKeyState(key)] + def _get_key(self, event): keyval = event.KeyCode if keyval in self.keyvald: key = self.keyvald[keyval] @@ -708,18 +722,11 @@ def _get_key(self, event): if not event.ShiftDown(): key = key.lower() else: - key = None - - for meth, prefix, key_name in [ - (event.ControlDown, 'ctrl', 'control'), - (event.AltDown, 'alt', 'alt'), - (event.ShiftDown, 'shift', 'shift'), - ]: - if meth() and key_name != key: - if not (key_name == 'shift' and key.isupper()): - key = '{0}+{1}'.format(prefix, key) - - return key + return None + mods = self._mpl_modifiers(event, exclude=keyval) + if "shift" in mods and key.isupper(): + mods.remove("shift") + return "+".join([*mods, key]) def _mpl_coords(self, pos=None): """ @@ -789,15 +796,17 @@ def _on_mouse_button(self, event): } button = event.GetButton() button = button_map.get(button, button) + modifiers = self._mpl_modifiers(event) if event.ButtonDown(): - MouseEvent("button_press_event", self, - x, y, button, guiEvent=event)._process() + MouseEvent("button_press_event", self, x, y, button, + modifiers=modifiers, guiEvent=event)._process() elif event.ButtonDClick(): - MouseEvent("button_press_event", self, - x, y, button, dblclick=True, guiEvent=event)._process() + MouseEvent("button_press_event", self, x, y, button, + dblclick=True, modifiers=modifiers, + guiEvent=event)._process() elif event.ButtonUp(): - MouseEvent("button_release_event", self, - x, y, button, guiEvent=event)._process() + MouseEvent("button_release_event", self, x, y, button, + modifiers=modifiers, guiEvent=event)._process() def _on_mouse_wheel(self, event): """Translate mouse wheel events into matplotlib events""" @@ -815,14 +824,16 @@ def _on_mouse_wheel(self, event): return # Return without processing event else: self._skipwheelevent = True - MouseEvent("scroll_event", self, - x, y, step=step, guiEvent=event)._process() + MouseEvent("scroll_event", self, x, y, step=step, + modifiers=self._mpl_modifiers(event), + guiEvent=event)._process() def _on_motion(self, event): """Start measuring on an axis.""" event.Skip() MouseEvent("motion_notify_event", self, *self._mpl_coords(event), + modifiers=self._mpl_modifiers(event), guiEvent=event)._process() def _on_enter(self, event): @@ -830,6 +841,7 @@ def _on_enter(self, event): event.Skip() LocationEvent("figure_enter_event", self, *self._mpl_coords(event), + modifiers=self._mpl_modifiers(), guiEvent=event)._process() def _on_leave(self, event): @@ -837,6 +849,7 @@ def _on_leave(self, event): event.Skip() LocationEvent("figure_leave_event", self, *self._mpl_coords(event), + modifiers=self._mpl_modifiers(), guiEvent=event)._process() diff --git a/lib/matplotlib/backends/web_backend/js/mpl.js b/lib/matplotlib/backends/web_backend/js/mpl.js index e77ba1aaf9e9..2862d9689304 100644 --- a/lib/matplotlib/backends/web_backend/js/mpl.js +++ b/lib/matplotlib/backends/web_backend/js/mpl.js @@ -589,6 +589,23 @@ mpl.figure.prototype._make_on_message_function = function (fig) { }; +function getModifiers(event) { + var mods = []; + if (event.ctrlKey) { + mods.push('ctrl'); + } + if (event.altKey) { + mods.push('alt'); + } + if (event.shiftKey) { + mods.push('shift'); + } + if (event.metaKey) { + mods.push('meta'); + } + return mods; +} + /* * return a copy of an object with only non-object keys * we need this to avoid circular references @@ -619,6 +636,7 @@ mpl.figure.prototype.mouse_event = function (event, name) { y: y, button: event.button, step: event.step, + modifiers: getModifiers(event), guiEvent: simpleKeys(event), }); diff --git a/lib/matplotlib/tests/test_backend_qt.py b/lib/matplotlib/tests/test_backend_qt.py index 14a9283ef480..f79546323c47 100644 --- a/lib/matplotlib/tests/test_backend_qt.py +++ b/lib/matplotlib/tests/test_backend_qt.py @@ -300,7 +300,7 @@ def custom_handler(signum, frame): 'QtAgg', marks=pytest.mark.backend('QtAgg', skip_on_importerror=True)), ]) -def test_correct_key(backend, qt_core, qt_key, qt_mods, answer): +def test_correct_key(backend, qt_core, qt_key, qt_mods, answer, monkeypatch): """ Make a figure. Send a key_press_event event (using non-public, qtX backend specific api). @@ -321,7 +321,9 @@ def test_correct_key(backend, qt_core, qt_key, qt_mods, answer): class _Event: def isAutoRepeat(self): return False def key(self): return _to_int(getattr(_enum("QtCore.Qt.Key"), qt_key)) - def modifiers(self): return qt_mod + + monkeypatch.setattr(QtWidgets.QApplication, "keyboardModifiers", + lambda self: qt_mod) def on_key_press(event): nonlocal result diff --git a/src/_macosx.m b/src/_macosx.m index 36e687d043c1..dcb236a861f3 100755 --- a/src/_macosx.m +++ b/src/_macosx.m @@ -316,6 +316,41 @@ static CGFloat _get_device_scale(CGContextRef cr) return pixelSize.width; } +int mpl_check_modifier( + NSUInteger modifiers, NSEventModifierFlags flag, + PyObject* list, char const* name) +{ + int status = 0; + if (modifiers & flag) { + PyObject* py_name = NULL; + if (!(py_name = PyUnicode_FromString(name)) + || PyList_Append(list, py_name)) { + status = -1; // failure + } + Py_XDECREF(py_name); + } + return status; +} + +PyObject* mpl_modifiers(NSEvent* event) +{ + PyGILState_STATE gstate = PyGILState_Ensure(); + PyObject* list = NULL; + if (!(list = PyList_New(0))) { + goto exit; + } + NSUInteger modifiers = [event modifierFlags]; + if (mpl_check_modifier(modifiers, NSEventModifierFlagControl, list, "ctrl") + || mpl_check_modifier(modifiers, NSEventModifierFlagOption, list, "alt") + || mpl_check_modifier(modifiers, NSEventModifierFlagShift, list, "shift") + || mpl_check_modifier(modifiers, NSEventModifierFlagCommand, list, "cmd")) { + Py_CLEAR(list); // On failure, return NULL with an exception set. + } +exit: + PyGILState_Release(gstate); + return list; +} + typedef struct { PyObject_HEAD View* view; @@ -1452,8 +1487,9 @@ - (void)mouseEntered:(NSEvent *)event x = location.x * device_scale; y = location.y * device_scale; process_event( - "LocationEvent", "{s:s, s:O, s:i, s:i}", - "name", "figure_enter_event", "canvas", canvas, "x", x, "y", y); + "LocationEvent", "{s:s, s:O, s:i, s:i, s:N}", + "name", "figure_enter_event", "canvas", canvas, "x", x, "y", y, + "modifiers", mpl_modifiers(event)); } - (void)mouseExited:(NSEvent *)event @@ -1464,8 +1500,9 @@ - (void)mouseExited:(NSEvent *)event x = location.x * device_scale; y = location.y * device_scale; process_event( - "LocationEvent", "{s:s, s:O, s:i, s:i}", - "name", "figure_leave_event", "canvas", canvas, "x", x, "y", y); + "LocationEvent", "{s:s, s:O, s:i, s:i, s:N}", + "name", "figure_leave_event", "canvas", canvas, "x", x, "y", y, + "modifiers", mpl_modifiers(event)); } - (void)mouseDown:(NSEvent *)event @@ -1502,9 +1539,9 @@ - (void)mouseDown:(NSEvent *)event dblclick = 1; } process_event( - "MouseEvent", "{s:s, s:O, s:i, s:i, s:i, s:i}", + "MouseEvent", "{s:s, s:O, s:i, s:i, s:i, s:i, s:N}", "name", "button_press_event", "canvas", canvas, "x", x, "y", y, - "button", button, "dblclick", dblclick); + "button", button, "dblclick", dblclick, "modifiers", mpl_modifiers(event)); } - (void)mouseUp:(NSEvent *)event @@ -1526,9 +1563,9 @@ - (void)mouseUp:(NSEvent *)event default: return; /* Unknown mouse event */ } process_event( - "MouseEvent", "{s:s, s:O, s:i, s:i, s:i}", + "MouseEvent", "{s:s, s:O, s:i, s:i, s:i, s:N}", "name", "button_release_event", "canvas", canvas, "x", x, "y", y, - "button", button); + "button", button, "modifiers", mpl_modifiers(event)); } - (void)mouseMoved:(NSEvent *)event @@ -1539,8 +1576,9 @@ - (void)mouseMoved:(NSEvent *)event x = location.x * device_scale; y = location.y * device_scale; process_event( - "MouseEvent", "{s:s, s:O, s:i, s:i}", - "name", "motion_notify_event", "canvas", canvas, "x", x, "y", y); + "MouseEvent", "{s:s, s:O, s:i, s:i, s:N}", + "name", "motion_notify_event", "canvas", canvas, "x", x, "y", y, + "modifiers", mpl_modifiers(event)); } - (void)mouseDragged:(NSEvent *)event @@ -1551,8 +1589,9 @@ - (void)mouseDragged:(NSEvent *)event x = location.x * device_scale; y = location.y * device_scale; process_event( - "MouseEvent", "{s:s, s:O, s:i, s:i}", - "name", "motion_notify_event", "canvas", canvas, "x", x, "y", y); + "MouseEvent", "{s:s, s:O, s:i, s:i, s:N}", + "name", "motion_notify_event", "canvas", canvas, "x", x, "y", y, + "modifiers", mpl_modifiers(event)); } - (void)rightMouseDown:(NSEvent *)event { [self mouseDown: event]; } @@ -1711,9 +1750,9 @@ - (void)scrollWheel:(NSEvent*)event int x = (int)round(point.x * device_scale); int y = (int)round(point.y * device_scale - 1); process_event( - "MouseEvent", "{s:s, s:O, s:i, s:i, s:i}", + "MouseEvent", "{s:s, s:O, s:i, s:i, s:i, s:N}", "name", "scroll_event", "canvas", canvas, - "x", x, "y", y, "step", step); + "x", x, "y", y, "step", step, "modifiers", mpl_modifiers(event)); } - (BOOL)acceptsFirstResponder