Skip to content

Commit 9957c82

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 dc6ad91 commit 9957c82

File tree

10 files changed

+255
-119
lines changed

10 files changed

+255
-119
lines changed

lib/matplotlib/backend_bases.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1298,11 +1298,13 @@ class LocationEvent(Event):
12981298
xdata, ydata : float or None
12991299
Data coordinates of the mouse within *inaxes*, or *None* if the mouse
13001300
is not over an Axes.
1301+
modifiers : frozenset
1302+
The keyboard modifiers currently being pressed (except for KeyEvent).
13011303
"""
13021304

13031305
lastevent = None # The last event processed so far.
13041306

1305-
def __init__(self, name, canvas, x, y, guiEvent=None):
1307+
def __init__(self, name, canvas, x, y, guiEvent=None, *, modifiers=None):
13061308
super().__init__(name, canvas, guiEvent=guiEvent)
13071309
# x position - pixels from left of canvas
13081310
self.x = int(x) if x is not None else x
@@ -1311,6 +1313,7 @@ def __init__(self, name, canvas, x, y, guiEvent=None):
13111313
self.inaxes = None # the Axes instance the mouse is over
13121314
self.xdata = None # x coord of mouse in data coords
13131315
self.ydata = None # y coord of mouse in data coords
1316+
self.modifiers = frozenset(modifiers if modifiers is not None else [])
13141317

13151318
if x is None or y is None:
13161319
# cannot check if event was in Axes if no (x, y) info
@@ -1369,7 +1372,9 @@ class MouseEvent(LocationEvent):
13691372
This key is currently obtained from the last 'key_press_event' or
13701373
'key_release_event' that occurred within the canvas. Thus, if the
13711374
last change of keyboard state occurred while the canvas did not have
1372-
focus, this attribute will be wrong.
1375+
focus, this attribute will be wrong. On the other hand, the
1376+
``modifiers`` attribute should always be correct, but it can only
1377+
report on modifier keys.
13731378
13741379
step : float
13751380
The number of scroll steps (positive for 'up', negative for 'down').
@@ -1391,8 +1396,9 @@ def on_press(event):
13911396
"""
13921397

13931398
def __init__(self, name, canvas, x, y, button=None, key=None,
1394-
step=0, dblclick=False, guiEvent=None):
1395-
super().__init__(name, canvas, x, y, guiEvent=guiEvent)
1399+
step=0, dblclick=False, guiEvent=None, *, modifiers=None):
1400+
super().__init__(
1401+
name, canvas, x, y, guiEvent=guiEvent, modifiers=modifiers)
13961402
if button in MouseButton.__members__.values():
13971403
button = MouseButton(button)
13981404
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
@@ -275,16 +275,19 @@ def _event_mpl_coords(self, event):
275275
def motion_notify_event(self, event):
276276
MouseEvent("motion_notify_event", self,
277277
*self._event_mpl_coords(event),
278+
modifiers=self._mpl_modifiers(event),
278279
guiEvent=event)._process()
279280

280281
def enter_notify_event(self, event):
281282
LocationEvent("figure_enter_event", self,
282283
*self._event_mpl_coords(event),
284+
modifiers=self._mpl_modifiers(event),
283285
guiEvent=event)._process()
284286

285287
def leave_notify_event(self, event):
286288
LocationEvent("figure_leave_event", self,
287289
*self._event_mpl_coords(event),
290+
modifiers=self._mpl_modifiers(event),
288291
guiEvent=event)._process()
289292

290293
def button_press_event(self, event, dblclick=False):
@@ -296,6 +299,7 @@ def button_press_event(self, event, dblclick=False):
296299
num = {2: 3, 3: 2}.get(num, num)
297300
MouseEvent("button_press_event", self,
298301
*self._event_mpl_coords(event), num, dblclick=dblclick,
302+
modifiers=self._mpl_modifiers(event),
299303
guiEvent=event)._process()
300304

301305
def button_dblclick_event(self, event):
@@ -307,13 +311,15 @@ def button_release_event(self, event):
307311
num = {2: 3, 3: 2}.get(num, num)
308312
MouseEvent("button_release_event", self,
309313
*self._event_mpl_coords(event), num,
314+
modifiers=self._mpl_modifiers(event),
310315
guiEvent=event)._process()
311316

312317
def scroll_event(self, event):
313318
num = getattr(event, 'num', None)
314319
step = 1 if num == 4 else -1 if num == 5 else 0
315320
MouseEvent("scroll_event", self,
316321
*self._event_mpl_coords(event), step=step,
322+
modifiers=self._mpl_modifiers(event),
317323
guiEvent=event)._process()
318324

319325
def scroll_event_windows(self, event):
@@ -327,12 +333,10 @@ def scroll_event_windows(self, event):
327333
- self._tkcanvas.canvasy(event.y_root - w.winfo_rooty()))
328334
step = event.delta / 120
329335
MouseEvent("scroll_event", self,
330-
x, y, step=step, guiEvent=event)._process()
331-
332-
def _get_key(self, event):
333-
unikey = event.char
334-
key = cbook._unikey_or_keysym_to_mplkey(unikey, event.keysym)
336+
x, y, step=step, modifiers=self._mpl_modifiers(event),
337+
guiEvent=event)._process()
335338

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

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

371376
def key_press(self, event):
372377
KeyEvent("key_press_event", self,

lib/matplotlib/backends/backend_gtk3.py

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

153153
def scroll_event(self, widget, event):
154154
step = 1 if event.direction == Gdk.ScrollDirection.UP else -1
155-
MouseEvent("scroll_event", self, *self._mpl_coords(event), step=step,
155+
MouseEvent("scroll_event", self,
156+
*self._mpl_coords(event), step=step,
157+
modifiers=self._mpl_modifiers(event.state),
156158
guiEvent=event)._process()
157159
return False # finish event propagation?
158160

159161
def button_press_event(self, widget, event):
160162
MouseEvent("button_press_event", self,
161163
*self._mpl_coords(event), event.button,
164+
modifiers=self._mpl_modifiers(event.state),
162165
guiEvent=event)._process()
163166
return False # finish event propagation?
164167

165168
def button_release_event(self, widget, event):
166169
MouseEvent("button_release_event", self,
167170
*self._mpl_coords(event), event.button,
171+
modifiers=self._mpl_modifiers(event.state),
168172
guiEvent=event)._process()
169173
return False # finish event propagation?
170174

@@ -182,15 +186,22 @@ def key_release_event(self, widget, event):
182186

183187
def motion_notify_event(self, widget, event):
184188
MouseEvent("motion_notify_event", self, *self._mpl_coords(event),
189+
modifiers=self._mpl_modifiers(event.state),
185190
guiEvent=event)._process()
186191
return False # finish event propagation?
187192

188193
def enter_notify_event(self, widget, event):
194+
gtk_mods = Gdk.Keymap.get_for_display(
195+
self.get_display()).get_modifier_state()
189196
LocationEvent("figure_enter_event", self, *self._mpl_coords(event),
197+
modifiers=self._mpl_modifiers(gtk_mods),
190198
guiEvent=event)._process()
191199

192200
def leave_notify_event(self, widget, event):
201+
gtk_mods = Gdk.Keymap.get_for_display(
202+
self.get_display()).get_modifier_state()
193203
LocationEvent("figure_leave_event", self, *self._mpl_coords(event),
204+
modifiers=self._mpl_modifiers(gtk_mods),
194205
guiEvent=event)._process()
195206

196207
def size_allocate(self, widget, allocation):
@@ -201,22 +212,24 @@ def size_allocate(self, widget, allocation):
201212
ResizeEvent("resize_event", self)._process()
202213
self.draw_idle()
203214

215+
@staticmethod
216+
def _mpl_modifiers(event_state):
217+
mod_table = [
218+
("ctrl", Gdk.ModifierType.CONTROL_MASK),
219+
("alt", Gdk.ModifierType.MOD1_MASK),
220+
("shift", Gdk.ModifierType.SHIFT_MASK),
221+
("super", Gdk.ModifierType.MOD4_MASK),
222+
]
223+
return [name for name, mask in mod_table if event_state & mask]
224+
204225
def _get_key(self, event):
205226
unikey = chr(Gdk.keyval_to_unicode(event.keyval))
206227
key = cbook._unikey_or_keysym_to_mplkey(
207-
unikey,
208-
Gdk.keyval_name(event.keyval))
209-
modifiers = [
210-
(Gdk.ModifierType.CONTROL_MASK, 'ctrl'),
211-
(Gdk.ModifierType.MOD1_MASK, 'alt'),
212-
(Gdk.ModifierType.SHIFT_MASK, 'shift'),
213-
(Gdk.ModifierType.MOD4_MASK, 'super'),
214-
]
215-
for key_mask, prefix in modifiers:
216-
if event.state & key_mask:
217-
if not (prefix == 'shift' and unikey.isprintable()):
218-
key = f'{prefix}+{key}'
219-
return key
228+
unikey, Gdk.keyval_name(event.keyval))
229+
mods = self._mpl_modifiers(event.state)
230+
if "shift" in mods and unikey.isprintable():
231+
mods.remove("shift")
232+
return "+".join([*mods, key])
220233

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

lib/matplotlib/backends/backend_gtk4.py

Lines changed: 51 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -111,44 +111,58 @@ def _mpl_coords(self, xy=None):
111111
return x, y
112112

113113
def scroll_event(self, controller, dx, dy):
114-
MouseEvent("scroll_event", self,
115-
*self._mpl_coords(), step=dy)._process()
114+
MouseEvent(
115+
"scroll_event", self, *self._mpl_coords(), step=dy,
116+
modifiers=self._mpl_modifiers(controller),
117+
)._process()
116118
return True
117119

118120
def button_press_event(self, controller, n_press, x, y):
119-
MouseEvent("button_press_event", self,
120-
*self._mpl_coords((x, y)), controller.get_current_button()
121-
)._process()
121+
MouseEvent(
122+
"button_press_event", self, *self._mpl_coords((x, y)),
123+
controller.get_current_button(),
124+
modifiers=self._mpl_modifiers(controller),
125+
)._process()
122126
self.grab_focus()
123127

124128
def button_release_event(self, controller, n_press, x, y):
125-
MouseEvent("button_release_event", self,
126-
*self._mpl_coords((x, y)), controller.get_current_button()
127-
)._process()
129+
MouseEvent(
130+
"button_release_event", self, *self._mpl_coords((x, y)),
131+
controller.get_current_button(),
132+
modifiers=self._mpl_modifiers(controller),
133+
)._process()
128134

129135
def key_press_event(self, controller, keyval, keycode, state):
130-
KeyEvent("key_press_event", self,
131-
self._get_key(keyval, keycode, state), *self._mpl_coords()
132-
)._process()
136+
KeyEvent(
137+
"key_press_event", self, self._get_key(keyval, keycode, state),
138+
*self._mpl_coords(),
139+
)._process()
133140
return True
134141

135142
def key_release_event(self, controller, keyval, keycode, state):
136-
KeyEvent("key_release_event", self,
137-
self._get_key(keyval, keycode, state), *self._mpl_coords()
138-
)._process()
143+
KeyEvent(
144+
"key_release_event", self, self._get_key(keyval, keycode, state),
145+
*self._mpl_coords(),
146+
)._process()
139147
return True
140148

141149
def motion_notify_event(self, controller, x, y):
142-
MouseEvent("motion_notify_event", self,
143-
*self._mpl_coords((x, y)))._process()
144-
145-
def leave_notify_event(self, controller):
146-
LocationEvent("figure_leave_event", self,
147-
*self._mpl_coords())._process()
150+
MouseEvent(
151+
"motion_notify_event", self, *self._mpl_coords((x, y)),
152+
modifiers=self._mpl_modifiers(controller),
153+
)._process()
148154

149155
def enter_notify_event(self, controller, x, y):
150-
LocationEvent("figure_enter_event", self,
151-
*self._mpl_coords((x, y)))._process()
156+
LocationEvent(
157+
"figure_enter_event", self, *self._mpl_coords((x, y)),
158+
modifiers=self._mpl_modifiers(),
159+
)._process()
160+
161+
def leave_notify_event(self, controller):
162+
LocationEvent(
163+
"figure_leave_event", self, *self._mpl_coords(),
164+
modifiers=self._mpl_modifiers(),
165+
)._process()
152166

153167
def resize_event(self, area, width, height):
154168
self._update_device_pixel_ratio()
@@ -159,6 +173,21 @@ def resize_event(self, area, width, height):
159173
ResizeEvent("resize_event", self)._process()
160174
self.draw_idle()
161175

176+
def _mpl_modifiers(self, controller=None):
177+
if controller is None:
178+
surface = self.get_native().get_surface()
179+
is_over, x, y, event_state = surface.get_device_position(
180+
self.get_display().get_default_seat().get_pointer())
181+
else:
182+
event_state = controller.get_current_event_state()
183+
mod_table = [
184+
("ctrl", Gdk.ModifierType.CONTROL_MASK),
185+
("alt", Gdk.ModifierType.ALT_MASK),
186+
("shift", Gdk.ModifierType.SHIFT_MASK),
187+
("super", Gdk.ModifierType.SUPER_MASK),
188+
]
189+
return [name for name, mask in mod_table if event_state & mask]
190+
162191
def _get_key(self, keyval, keycode, state):
163192
unikey = chr(Gdk.keyval_to_unicode(keyval))
164193
key = cbook._unikey_or_keysym_to_mplkey(

0 commit comments

Comments
 (0)