Skip to content

Commit ecc7f36

Browse files
committed
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).
1 parent 49724bf commit ecc7f36

File tree

10 files changed

+266
-129
lines changed

10 files changed

+266
-129
lines changed

lib/matplotlib/backend_bases.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1316,11 +1316,13 @@ class LocationEvent(Event):
13161316
xdata, ydata : float or None
13171317
Data coordinates of the mouse within *inaxes*, or *None* if the mouse
13181318
is not over an Axes.
1319+
modifiers : frozenset
1320+
The keyboard modifiers currently being pressed (except for KeyEvent).
13191321
"""
13201322

13211323
lastevent = None # The last event processed so far.
13221324

1323-
def __init__(self, name, canvas, x, y, guiEvent=None):
1325+
def __init__(self, name, canvas, x, y, guiEvent=None, *, modifiers=None):
13241326
super().__init__(name, canvas, guiEvent=guiEvent)
13251327
# x position - pixels from left of canvas
13261328
self.x = int(x) if x is not None else x
@@ -1329,6 +1331,7 @@ def __init__(self, name, canvas, x, y, guiEvent=None):
13291331
self.inaxes = None # the Axes instance the mouse is over
13301332
self.xdata = None # x coord of mouse in data coords
13311333
self.ydata = None # y coord of mouse in data coords
1334+
self.modifiers = frozenset(modifiers if modifiers is not None else [])
13321335

13331336
if x is None or y is None:
13341337
# cannot check if event was in Axes if no (x, y) info
@@ -1387,7 +1390,9 @@ class MouseEvent(LocationEvent):
13871390
This key is currently obtained from the last 'key_press_event' or
13881391
'key_release_event' that occurred within the canvas. Thus, if the
13891392
last change of keyboard state occurred while the canvas did not have
1390-
focus, this attribute will be wrong.
1393+
focus, this attribute will be wrong. On the other hand, the
1394+
``modifiers`` attribute should always be correct, but it can only
1395+
report on modifier keys.
13911396
13921397
step : float
13931398
The number of scroll steps (positive for 'up', negative for 'down').
@@ -1409,8 +1414,9 @@ def on_press(event):
14091414
"""
14101415

14111416
def __init__(self, name, canvas, x, y, button=None, key=None,
1412-
step=0, dblclick=False, guiEvent=None):
1413-
super().__init__(name, canvas, x, y, guiEvent=guiEvent)
1417+
step=0, dblclick=False, guiEvent=None, *, modifiers=None):
1418+
super().__init__(
1419+
name, canvas, x, y, guiEvent=guiEvent, modifiers=modifiers)
14141420
if button in MouseButton.__members__.values():
14151421
button = MouseButton(button)
14161422
if name == "scroll_event" and button is None:

lib/matplotlib/backends/_backend_tk.py

Lines changed: 34 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -273,16 +273,19 @@ def _event_mpl_coords(self, event):
273273
def motion_notify_event(self, event):
274274
MouseEvent("motion_notify_event", self,
275275
*self._event_mpl_coords(event),
276+
modifiers=self._mpl_modifiers(event),
276277
guiEvent=event)._process()
277278

278279
def enter_notify_event(self, event):
279280
LocationEvent("figure_enter_event", self,
280281
*self._event_mpl_coords(event),
282+
modifiers=self._mpl_modifiers(event),
281283
guiEvent=event)._process()
282284

283285
def leave_notify_event(self, event):
284286
LocationEvent("figure_leave_event", self,
285287
*self._event_mpl_coords(event),
288+
modifiers=self._mpl_modifiers(event),
286289
guiEvent=event)._process()
287290

288291
def button_press_event(self, event, dblclick=False):
@@ -294,6 +297,7 @@ def button_press_event(self, event, dblclick=False):
294297
num = {2: 3, 3: 2}.get(num, num)
295298
MouseEvent("button_press_event", self,
296299
*self._event_mpl_coords(event), num, dblclick=dblclick,
300+
modifiers=self._mpl_modifiers(event),
297301
guiEvent=event)._process()
298302

299303
def button_dblclick_event(self, event):
@@ -305,13 +309,15 @@ def button_release_event(self, event):
305309
num = {2: 3, 3: 2}.get(num, num)
306310
MouseEvent("button_release_event", self,
307311
*self._event_mpl_coords(event), num,
312+
modifiers=self._mpl_modifiers(event),
308313
guiEvent=event)._process()
309314

310315
def scroll_event(self, event):
311316
num = getattr(event, 'num', None)
312317
step = 1 if num == 4 else -1 if num == 5 else 0
313318
MouseEvent("scroll_event", self,
314319
*self._event_mpl_coords(event), step=step,
320+
modifiers=self._mpl_modifiers(event),
315321
guiEvent=event)._process()
316322

317323
def scroll_event_windows(self, event):
@@ -325,12 +331,10 @@ def scroll_event_windows(self, event):
325331
- self._tkcanvas.canvasy(event.y_root - w.winfo_rooty()))
326332
step = event.delta / 120
327333
MouseEvent("scroll_event", self,
328-
x, y, step=step, guiEvent=event)._process()
329-
330-
def _get_key(self, event):
331-
unikey = event.char
332-
key = cbook._unikey_or_keysym_to_mplkey(unikey, event.keysym)
334+
x, y, step=step, modifiers=self._mpl_modifiers(event),
335+
guiEvent=event)._process()
333336

337+
def _mpl_modifiers(self, event, *, exclude=None):
334338
# add modifier keys to the key string. Bit details originate from
335339
# http://effbot.org/tkinterbook/tkinter-events-and-bindings.htm
336340
# BIT_SHIFT = 0x001; BIT_CAPSLOCK = 0x002; BIT_CONTROL = 0x004;
@@ -339,32 +343,33 @@ def _get_key(self, event):
339343
# In general, the modifier key is excluded from the modifier flag,
340344
# however this is not the case on "darwin", so double check that
341345
# we aren't adding repeat modifier flags to a modifier key.
342-
if sys.platform == 'win32':
343-
modifiers = [(2, 'ctrl', 'control'),
344-
(17, 'alt', 'alt'),
345-
(0, 'shift', 'shift'),
346-
]
347-
elif sys.platform == 'darwin':
348-
modifiers = [(2, 'ctrl', 'control'),
349-
(4, 'alt', 'alt'),
350-
(0, 'shift', 'shift'),
351-
(3, 'super', 'super'),
352-
]
353-
else:
354-
modifiers = [(2, 'ctrl', 'control'),
355-
(3, 'alt', 'alt'),
356-
(0, 'shift', 'shift'),
357-
(6, 'super', 'super'),
358-
]
346+
modifiers = [
347+
("ctrl", 1 << 2, "control"),
348+
("alt", 1 << 17, "alt"),
349+
("shift", 1 << 0, "shift"),
350+
] if sys.platform == "win32" else [
351+
("ctrl", 1 << 2, "control"),
352+
("alt", 1 << 4, "alt"),
353+
("shift", 1 << 0, "shift"),
354+
("super", 1 << 3, "super"),
355+
] if sys.platform == "darwin" else [
356+
("ctrl", 1 << 2, "control"),
357+
("alt", 1 << 3, "alt"),
358+
("shift", 1 << 0, "shift"),
359+
("super", 1 << 6, "super"),
360+
]
361+
return [name for name, mask, key in modifiers
362+
if event.state & mask and exclude != key]
359363

364+
def _get_key(self, event):
365+
unikey = event.char
366+
key = cbook._unikey_or_keysym_to_mplkey(unikey, event.keysym)
360367
if key is not None:
361-
# shift is not added to the keys as this is already accounted for
362-
for bitmask, prefix, key_name in modifiers:
363-
if event.state & (1 << bitmask) and key_name not in key:
364-
if not (prefix == 'shift' and unikey):
365-
key = '{0}+{1}'.format(prefix, key)
366-
367-
return key
368+
mods = self._mpl_modifiers(event, exclude=key)
369+
# shift is not added to the keys as this is already accounted for.
370+
if "shift" in mods and unikey:
371+
mods.remove("shift")
372+
return "+".join([*mods, key])
368373

369374
def key_press(self, event):
370375
KeyEvent("key_press_event", self,

lib/matplotlib/backends/backend_gtk3.py

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -133,19 +133,23 @@ def _mpl_coords(self, event=None):
133133

134134
def scroll_event(self, widget, event):
135135
step = 1 if event.direction == Gdk.ScrollDirection.UP else -1
136-
MouseEvent("scroll_event", self, *self._mpl_coords(event), step=step,
136+
MouseEvent("scroll_event", self,
137+
*self._mpl_coords(event), step=step,
138+
modifiers=self._mpl_modifiers(event.state),
137139
guiEvent=event)._process()
138140
return False # finish event propagation?
139141

140142
def button_press_event(self, widget, event):
141143
MouseEvent("button_press_event", self,
142144
*self._mpl_coords(event), event.button,
145+
modifiers=self._mpl_modifiers(event.state),
143146
guiEvent=event)._process()
144147
return False # finish event propagation?
145148

146149
def button_release_event(self, widget, event):
147150
MouseEvent("button_release_event", self,
148151
*self._mpl_coords(event), event.button,
152+
modifiers=self._mpl_modifiers(event.state),
149153
guiEvent=event)._process()
150154
return False # finish event propagation?
151155

@@ -163,15 +167,22 @@ def key_release_event(self, widget, event):
163167

164168
def motion_notify_event(self, widget, event):
165169
MouseEvent("motion_notify_event", self, *self._mpl_coords(event),
170+
modifiers=self._mpl_modifiers(event.state),
166171
guiEvent=event)._process()
167172
return False # finish event propagation?
168173

169174
def enter_notify_event(self, widget, event):
175+
gtk_mods = Gdk.Keymap.get_for_display(
176+
self.get_display()).get_modifier_state()
170177
LocationEvent("figure_enter_event", self, *self._mpl_coords(event),
178+
modifiers=self._mpl_modifiers(gtk_mods),
171179
guiEvent=event)._process()
172180

173181
def leave_notify_event(self, widget, event):
182+
gtk_mods = Gdk.Keymap.get_for_display(
183+
self.get_display()).get_modifier_state()
174184
LocationEvent("figure_leave_event", self, *self._mpl_coords(event),
185+
modifiers=self._mpl_modifiers(gtk_mods),
175186
guiEvent=event)._process()
176187

177188
def size_allocate(self, widget, allocation):
@@ -182,22 +193,25 @@ def size_allocate(self, widget, allocation):
182193
ResizeEvent("resize_event", self)._process()
183194
self.draw_idle()
184195

196+
@staticmethod
197+
def _mpl_modifiers(event_state, *, exclude=None):
198+
modifiers = [
199+
("ctrl", Gdk.ModifierType.CONTROL_MASK, "control"),
200+
("alt", Gdk.ModifierType.MOD1_MASK, "alt"),
201+
("shift", Gdk.ModifierType.SHIFT_MASK, "shift"),
202+
("super", Gdk.ModifierType.MOD4_MASK, "super"),
203+
]
204+
return [name for name, mask, key in modifiers
205+
if exclude != key and event_state & mask]
206+
185207
def _get_key(self, event):
186208
unikey = chr(Gdk.keyval_to_unicode(event.keyval))
187209
key = cbook._unikey_or_keysym_to_mplkey(
188-
unikey,
189-
Gdk.keyval_name(event.keyval))
190-
modifiers = [
191-
(Gdk.ModifierType.CONTROL_MASK, 'ctrl'),
192-
(Gdk.ModifierType.MOD1_MASK, 'alt'),
193-
(Gdk.ModifierType.SHIFT_MASK, 'shift'),
194-
(Gdk.ModifierType.MOD4_MASK, 'super'),
195-
]
196-
for key_mask, prefix in modifiers:
197-
if event.state & key_mask:
198-
if not (prefix == 'shift' and unikey.isprintable()):
199-
key = f'{prefix}+{key}'
200-
return key
210+
unikey, Gdk.keyval_name(event.keyval))
211+
mods = self._mpl_modifiers(event.state, exclude=key)
212+
if "shift" in mods and unikey.isprintable():
213+
mods.remove("shift")
214+
return "+".join([*mods, key])
201215

202216
def _update_device_pixel_ratio(self, *args, **kwargs):
203217
# We need to be careful in cases with mixed resolution displays if

lib/matplotlib/backends/backend_gtk4.py

Lines changed: 60 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -109,44 +109,58 @@ def _mpl_coords(self, xy=None):
109109
return x, y
110110

111111
def scroll_event(self, controller, dx, dy):
112-
MouseEvent("scroll_event", self,
113-
*self._mpl_coords(), step=dy)._process()
112+
MouseEvent(
113+
"scroll_event", self, *self._mpl_coords(), step=dy,
114+
modifiers=self._mpl_modifiers(controller),
115+
)._process()
114116
return True
115117

116118
def button_press_event(self, controller, n_press, x, y):
117-
MouseEvent("button_press_event", self,
118-
*self._mpl_coords((x, y)), controller.get_current_button()
119-
)._process()
119+
MouseEvent(
120+
"button_press_event", self, *self._mpl_coords((x, y)),
121+
controller.get_current_button(),
122+
modifiers=self._mpl_modifiers(controller),
123+
)._process()
120124
self.grab_focus()
121125

122126
def button_release_event(self, controller, n_press, x, y):
123-
MouseEvent("button_release_event", self,
124-
*self._mpl_coords((x, y)), controller.get_current_button()
125-
)._process()
127+
MouseEvent(
128+
"button_release_event", self, *self._mpl_coords((x, y)),
129+
controller.get_current_button(),
130+
modifiers=self._mpl_modifiers(controller),
131+
)._process()
126132

127133
def key_press_event(self, controller, keyval, keycode, state):
128-
KeyEvent("key_press_event", self,
129-
self._get_key(keyval, keycode, state), *self._mpl_coords()
130-
)._process()
134+
KeyEvent(
135+
"key_press_event", self, self._get_key(keyval, keycode, state),
136+
*self._mpl_coords(),
137+
)._process()
131138
return True
132139

133140
def key_release_event(self, controller, keyval, keycode, state):
134-
KeyEvent("key_release_event", self,
135-
self._get_key(keyval, keycode, state), *self._mpl_coords()
136-
)._process()
141+
KeyEvent(
142+
"key_release_event", self, self._get_key(keyval, keycode, state),
143+
*self._mpl_coords(),
144+
)._process()
137145
return True
138146

139147
def motion_notify_event(self, controller, x, y):
140-
MouseEvent("motion_notify_event", self,
141-
*self._mpl_coords((x, y)))._process()
142-
143-
def leave_notify_event(self, controller):
144-
LocationEvent("figure_leave_event", self,
145-
*self._mpl_coords())._process()
148+
MouseEvent(
149+
"motion_notify_event", self, *self._mpl_coords((x, y)),
150+
modifiers=self._mpl_modifiers(controller),
151+
)._process()
146152

147153
def enter_notify_event(self, controller, x, y):
148-
LocationEvent("figure_enter_event", self,
149-
*self._mpl_coords((x, y)))._process()
154+
LocationEvent(
155+
"figure_enter_event", self, *self._mpl_coords((x, y)),
156+
modifiers=self._mpl_modifiers(),
157+
)._process()
158+
159+
def leave_notify_event(self, controller):
160+
LocationEvent(
161+
"figure_leave_event", self, *self._mpl_coords(),
162+
modifiers=self._mpl_modifiers(),
163+
)._process()
150164

151165
def resize_event(self, area, width, height):
152166
self._update_device_pixel_ratio()
@@ -157,22 +171,37 @@ def resize_event(self, area, width, height):
157171
ResizeEvent("resize_event", self)._process()
158172
self.draw_idle()
159173

174+
def _mpl_modifiers(self, controller=None):
175+
if controller is None:
176+
surface = self.get_native().get_surface()
177+
is_over, x, y, event_state = surface.get_device_position(
178+
self.get_display().get_default_seat().get_pointer())
179+
else:
180+
event_state = controller.get_current_event_state()
181+
mod_table = [
182+
("ctrl", Gdk.ModifierType.CONTROL_MASK),
183+
("alt", Gdk.ModifierType.ALT_MASK),
184+
("shift", Gdk.ModifierType.SHIFT_MASK),
185+
("super", Gdk.ModifierType.SUPER_MASK),
186+
]
187+
return [name for name, mask in mod_table if event_state & mask]
188+
160189
def _get_key(self, keyval, keycode, state):
161190
unikey = chr(Gdk.keyval_to_unicode(keyval))
162191
key = cbook._unikey_or_keysym_to_mplkey(
163192
unikey,
164193
Gdk.keyval_name(keyval))
165194
modifiers = [
166-
(Gdk.ModifierType.CONTROL_MASK, 'ctrl'),
167-
(Gdk.ModifierType.ALT_MASK, 'alt'),
168-
(Gdk.ModifierType.SHIFT_MASK, 'shift'),
169-
(Gdk.ModifierType.SUPER_MASK, 'super'),
195+
("ctrl", Gdk.ModifierType.CONTROL_MASK, "control"),
196+
("alt", Gdk.ModifierType.ALT_MASK, "alt"),
197+
("shift", Gdk.ModifierType.SHIFT_MASK, "shift"),
198+
("super", Gdk.ModifierType.SUPER_MASK, "super"),
170199
]
171-
for key_mask, prefix in modifiers:
172-
if state & key_mask:
173-
if not (prefix == 'shift' and unikey.isprintable()):
174-
key = f'{prefix}+{key}'
175-
return key
200+
mods = [
201+
mod for mod, mask, mod_key in modifiers
202+
if (mod_key != key and state & mask
203+
and not (mod == "shift" and unikey.isprintable()))]
204+
return "+".join([*mods, key])
176205

177206
def _update_device_pixel_ratio(self, *args, **kwargs):
178207
# We need to be careful in cases with mixed resolution displays if

0 commit comments

Comments
 (0)