Skip to content

Commit 6a8a80d

Browse files
authored
Merge pull request matplotlib#28453 from anntzer/buttons-nodeadreckoning
Stop relying on dead-reckoning mouse buttons for motion_notify_event.
2 parents 61833b8 + cd37b73 commit 6a8a80d

File tree

10 files changed

+187
-44
lines changed

10 files changed

+187
-44
lines changed

lib/matplotlib/backend_bases.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1324,6 +1324,28 @@ class MouseEvent(LocationEvent):
13241324
If this is unset, *name* is "scroll_event", and *step* is nonzero, then
13251325
this will be set to "up" or "down" depending on the sign of *step*.
13261326
1327+
buttons : None or frozenset
1328+
For 'motion_notify_event', the mouse buttons currently being pressed
1329+
(a set of zero or more MouseButtons);
1330+
for other events, None.
1331+
1332+
.. note::
1333+
For 'motion_notify_event', this attribute is more accurate than
1334+
the ``button`` (singular) attribute, which is obtained from the last
1335+
'button_press_event' or 'button_release_event' that occurred within
1336+
the canvas (and thus 1. be wrong if the last change in mouse state
1337+
occurred when the canvas did not have focus, and 2. cannot report
1338+
when multiple buttons are pressed).
1339+
1340+
This attribute is not set for 'button_press_event' and
1341+
'button_release_event' because GUI toolkits are inconsistent as to
1342+
whether they report the button state *before* or *after* the
1343+
press/release occurred.
1344+
1345+
.. warning::
1346+
On macOS, the Tk backends only report a single button even if
1347+
multiple buttons are pressed.
1348+
13271349
key : None or str
13281350
The key pressed when the mouse event triggered, e.g. 'shift'.
13291351
See `KeyEvent`.
@@ -1356,7 +1378,8 @@ def on_press(event):
13561378
"""
13571379

13581380
def __init__(self, name, canvas, x, y, button=None, key=None,
1359-
step=0, dblclick=False, guiEvent=None, *, modifiers=None):
1381+
step=0, dblclick=False, guiEvent=None, *,
1382+
buttons=None, modifiers=None):
13601383
super().__init__(
13611384
name, canvas, x, y, guiEvent=guiEvent, modifiers=modifiers)
13621385
if button in MouseButton.__members__.values():
@@ -1367,6 +1390,16 @@ def __init__(self, name, canvas, x, y, button=None, key=None,
13671390
elif step < 0:
13681391
button = "down"
13691392
self.button = button
1393+
if name == "motion_notify_event":
1394+
self.buttons = frozenset(buttons if buttons is not None else [])
1395+
else:
1396+
# We don't support 'buttons' for button_press/release_event because
1397+
# toolkits are inconsistent as to whether they report the state
1398+
# before or after the event.
1399+
if buttons:
1400+
raise ValueError(
1401+
"'buttons' is only supported for 'motion_notify_event'")
1402+
self.buttons = None
13701403
self.key = key
13711404
self.step = step
13721405
self.dblclick = dblclick

lib/matplotlib/backend_bases.pyi

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,7 @@ class MouseEvent(LocationEvent):
258258
dblclick: bool = ...,
259259
guiEvent: Any | None = ...,
260260
*,
261+
buttons: Iterable[MouseButton] | None = ...,
261262
modifiers: Iterable[str] | None = ...,
262263
) -> None: ...
263264

lib/matplotlib/backends/_backend_tk.py

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from matplotlib import _api, backend_tools, cbook, _c_internal_utils
2020
from matplotlib.backend_bases import (
2121
_Backend, FigureCanvasBase, FigureManagerBase, NavigationToolbar2,
22-
TimerBase, ToolContainerBase, cursors, _Mode,
22+
TimerBase, ToolContainerBase, cursors, _Mode, MouseButton,
2323
CloseEvent, KeyEvent, LocationEvent, MouseEvent, ResizeEvent)
2424
from matplotlib._pylab_helpers import Gcf
2525
from . import _tkagg
@@ -296,6 +296,7 @@ def _event_mpl_coords(self, event):
296296
def motion_notify_event(self, event):
297297
MouseEvent("motion_notify_event", self,
298298
*self._event_mpl_coords(event),
299+
buttons=self._mpl_buttons(event),
299300
modifiers=self._mpl_modifiers(event),
300301
guiEvent=event)._process()
301302

@@ -357,13 +358,33 @@ def scroll_event_windows(self, event):
357358
x, y, step=step, modifiers=self._mpl_modifiers(event),
358359
guiEvent=event)._process()
359360

361+
@staticmethod
362+
def _mpl_buttons(event): # See _mpl_modifiers.
363+
# NOTE: This fails to report multiclicks on macOS; only one button is
364+
# reported (multiclicks work correctly on Linux & Windows).
365+
modifiers = [
366+
# macOS appears to swap right and middle (look for "Swap buttons
367+
# 2/3" in tk/macosx/tkMacOSXMouseEvent.c).
368+
(MouseButton.LEFT, 1 << 8),
369+
(MouseButton.RIGHT, 1 << 9),
370+
(MouseButton.MIDDLE, 1 << 10),
371+
(MouseButton.BACK, 1 << 11),
372+
(MouseButton.FORWARD, 1 << 12),
373+
] if sys.platform == "darwin" else [
374+
(MouseButton.LEFT, 1 << 8),
375+
(MouseButton.MIDDLE, 1 << 9),
376+
(MouseButton.RIGHT, 1 << 10),
377+
(MouseButton.BACK, 1 << 11),
378+
(MouseButton.FORWARD, 1 << 12),
379+
]
380+
# State *before* press/release.
381+
return [name for name, mask in modifiers if event.state & mask]
382+
360383
@staticmethod
361384
def _mpl_modifiers(event, *, exclude=None):
362-
# add modifier keys to the key string. Bit details originate from
363-
# http://effbot.org/tkinterbook/tkinter-events-and-bindings.htm
364-
# BIT_SHIFT = 0x001; BIT_CAPSLOCK = 0x002; BIT_CONTROL = 0x004;
365-
# BIT_LEFT_ALT = 0x008; BIT_NUMLOCK = 0x010; BIT_RIGHT_ALT = 0x080;
366-
# BIT_MB_1 = 0x100; BIT_MB_2 = 0x200; BIT_MB_3 = 0x400;
385+
# Add modifier keys to the key string. Bit values are inferred from
386+
# the implementation of tkinter.Event.__repr__ (1, 2, 4, 8, ... =
387+
# Shift, Lock, Control, Mod1, ..., Mod5, Button1, ..., Button5)
367388
# In general, the modifier key is excluded from the modifier flag,
368389
# however this is not the case on "darwin", so double check that
369390
# we aren't adding repeat modifier flags to a modifier key.

lib/matplotlib/backends/backend_gtk3.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
import matplotlib as mpl
77
from matplotlib import _api, backend_tools, cbook
88
from matplotlib.backend_bases import (
9-
ToolContainerBase, CloseEvent, KeyEvent, LocationEvent, MouseEvent,
10-
ResizeEvent)
9+
ToolContainerBase, MouseButton,
10+
CloseEvent, KeyEvent, LocationEvent, MouseEvent, ResizeEvent)
1111

1212
try:
1313
import gi
@@ -156,6 +156,7 @@ def key_release_event(self, widget, event):
156156

157157
def motion_notify_event(self, widget, event):
158158
MouseEvent("motion_notify_event", self, *self._mpl_coords(event),
159+
buttons=self._mpl_buttons(event.state),
159160
modifiers=self._mpl_modifiers(event.state),
160161
guiEvent=event)._process()
161162
return False # finish event propagation?
@@ -182,6 +183,18 @@ def size_allocate(self, widget, allocation):
182183
ResizeEvent("resize_event", self)._process()
183184
self.draw_idle()
184185

186+
@staticmethod
187+
def _mpl_buttons(event_state):
188+
modifiers = [
189+
(MouseButton.LEFT, Gdk.ModifierType.BUTTON1_MASK),
190+
(MouseButton.MIDDLE, Gdk.ModifierType.BUTTON2_MASK),
191+
(MouseButton.RIGHT, Gdk.ModifierType.BUTTON3_MASK),
192+
(MouseButton.BACK, Gdk.ModifierType.BUTTON4_MASK),
193+
(MouseButton.FORWARD, Gdk.ModifierType.BUTTON5_MASK),
194+
]
195+
# State *before* press/release.
196+
return [name for name, mask in modifiers if event_state & mask]
197+
185198
@staticmethod
186199
def _mpl_modifiers(event_state, *, exclude=None):
187200
modifiers = [

lib/matplotlib/backends/backend_gtk4.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
import matplotlib as mpl
66
from matplotlib import _api, backend_tools, cbook
77
from matplotlib.backend_bases import (
8-
ToolContainerBase, KeyEvent, LocationEvent, MouseEvent, ResizeEvent,
9-
CloseEvent)
8+
ToolContainerBase, MouseButton,
9+
KeyEvent, LocationEvent, MouseEvent, ResizeEvent, CloseEvent)
1010

1111
try:
1212
import gi
@@ -155,6 +155,7 @@ def key_release_event(self, controller, keyval, keycode, state):
155155
def motion_notify_event(self, controller, x, y):
156156
MouseEvent(
157157
"motion_notify_event", self, *self._mpl_coords((x, y)),
158+
buttons=self._mpl_buttons(controller),
158159
modifiers=self._mpl_modifiers(controller),
159160
guiEvent=controller.get_current_event(),
160161
)._process()
@@ -182,6 +183,26 @@ def resize_event(self, area, width, height):
182183
ResizeEvent("resize_event", self)._process()
183184
self.draw_idle()
184185

186+
def _mpl_buttons(self, controller):
187+
# NOTE: This spews "Broken accounting of active state" warnings on
188+
# right click on macOS.
189+
surface = self.get_native().get_surface()
190+
is_over, x, y, event_state = surface.get_device_position(
191+
self.get_display().get_default_seat().get_pointer())
192+
# NOTE: alternatively we could use
193+
# event_state = controller.get_current_event_state()
194+
# but for button_press/button_release this would report the state
195+
# *prior* to the event rather than after it; the above reports the
196+
# state *after* it.
197+
mod_table = [
198+
(MouseButton.LEFT, Gdk.ModifierType.BUTTON1_MASK),
199+
(MouseButton.MIDDLE, Gdk.ModifierType.BUTTON2_MASK),
200+
(MouseButton.RIGHT, Gdk.ModifierType.BUTTON3_MASK),
201+
(MouseButton.BACK, Gdk.ModifierType.BUTTON4_MASK),
202+
(MouseButton.FORWARD, Gdk.ModifierType.BUTTON5_MASK),
203+
]
204+
return {name for name, mask in mod_table if event_state & mask}
205+
185206
def _mpl_modifiers(self, controller=None):
186207
if controller is None:
187208
surface = self.get_native().get_surface()

lib/matplotlib/backends/backend_qt.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,7 @@ def mouseMoveEvent(self, event):
329329
return
330330
MouseEvent("motion_notify_event", self,
331331
*self.mouseEventCoords(event),
332+
buttons=self._mpl_buttons(event.buttons()),
332333
modifiers=self._mpl_modifiers(),
333334
guiEvent=event)._process()
334335

@@ -396,6 +397,13 @@ def sizeHint(self):
396397
def minimumSizeHint(self):
397398
return QtCore.QSize(10, 10)
398399

400+
@staticmethod
401+
def _mpl_buttons(buttons):
402+
buttons = _to_int(buttons)
403+
# State *after* press/release.
404+
return {button for mask, button in FigureCanvasQT.buttond.items()
405+
if _to_int(mask) & buttons}
406+
399407
@staticmethod
400408
def _mpl_modifiers(modifiers=None, *, exclude=None):
401409
if modifiers is None:

lib/matplotlib/backends/backend_webagg_core.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
from matplotlib import _api, backend_bases, backend_tools
2323
from matplotlib.backends import backend_agg
2424
from matplotlib.backend_bases import (
25-
_Backend, KeyEvent, LocationEvent, MouseEvent, ResizeEvent)
25+
_Backend, MouseButton, KeyEvent, LocationEvent, MouseEvent, ResizeEvent)
2626

2727
_log = logging.getLogger(__name__)
2828

@@ -283,10 +283,17 @@ def _handle_mouse(self, event):
283283
y = event['y']
284284
y = self.get_renderer().height - y
285285
self._last_mouse_xy = x, y
286-
# JavaScript button numbers and Matplotlib button numbers are off by 1.
287-
button = event['button'] + 1
288-
289286
e_type = event['type']
287+
button = event['button'] + 1 # JS numbers off by 1 compared to mpl.
288+
buttons = { # JS ordering different compared to mpl.
289+
button for button, mask in [
290+
(MouseButton.LEFT, 1),
291+
(MouseButton.RIGHT, 2),
292+
(MouseButton.MIDDLE, 4),
293+
(MouseButton.BACK, 8),
294+
(MouseButton.FORWARD, 16),
295+
] if event['buttons'] & mask # State *after* press/release.
296+
}
290297
modifiers = event['modifiers']
291298
guiEvent = event.get('guiEvent')
292299
if e_type in ['button_press', 'button_release']:
@@ -300,10 +307,12 @@ def _handle_mouse(self, event):
300307
modifiers=modifiers, guiEvent=guiEvent)._process()
301308
elif e_type == 'motion_notify':
302309
MouseEvent(e_type + '_event', self, x, y,
303-
modifiers=modifiers, guiEvent=guiEvent)._process()
310+
buttons=buttons, modifiers=modifiers, guiEvent=guiEvent,
311+
)._process()
304312
elif e_type in ['figure_enter', 'figure_leave']:
305313
LocationEvent(e_type + '_event', self, x, y,
306314
modifiers=modifiers, guiEvent=guiEvent)._process()
315+
307316
handle_button_press = handle_button_release = handle_dblclick = \
308317
handle_figure_enter = handle_figure_leave = handle_motion_notify = \
309318
handle_scroll = _handle_mouse

lib/matplotlib/backends/backend_wx.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -685,6 +685,22 @@ def _on_size(self, event):
685685
ResizeEvent("resize_event", self)._process()
686686
self.draw_idle()
687687

688+
@staticmethod
689+
def _mpl_buttons():
690+
state = wx.GetMouseState()
691+
# NOTE: Alternatively, we could use event.LeftIsDown() / etc. but this
692+
# fails to report multiclick drags on macOS (other OSes have not been
693+
# verified).
694+
mod_table = [
695+
(MouseButton.LEFT, state.LeftIsDown()),
696+
(MouseButton.RIGHT, state.RightIsDown()),
697+
(MouseButton.MIDDLE, state.MiddleIsDown()),
698+
(MouseButton.BACK, state.Aux1IsDown()),
699+
(MouseButton.FORWARD, state.Aux2IsDown()),
700+
]
701+
# State *after* press/release.
702+
return {button for button, flag in mod_table if flag}
703+
688704
@staticmethod
689705
def _mpl_modifiers(event=None, *, exclude=None):
690706
mod_table = [
@@ -794,9 +810,8 @@ def _on_mouse_button(self, event):
794810
MouseEvent("button_press_event", self, x, y, button,
795811
modifiers=modifiers, guiEvent=event)._process()
796812
elif event.ButtonDClick():
797-
MouseEvent("button_press_event", self, x, y, button,
798-
dblclick=True, modifiers=modifiers,
799-
guiEvent=event)._process()
813+
MouseEvent("button_press_event", self, x, y, button, dblclick=True,
814+
modifiers=modifiers, guiEvent=event)._process()
800815
elif event.ButtonUp():
801816
MouseEvent("button_release_event", self, x, y, button,
802817
modifiers=modifiers, guiEvent=event)._process()
@@ -826,6 +841,7 @@ def _on_motion(self, event):
826841
event.Skip()
827842
MouseEvent("motion_notify_event", self,
828843
*self._mpl_coords(event),
844+
buttons=self._mpl_buttons(),
829845
modifiers=self._mpl_modifiers(event),
830846
guiEvent=event)._process()
831847

lib/matplotlib/backends/web_backend/js/mpl.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -644,6 +644,7 @@ mpl.figure.prototype.mouse_event = function (event, name) {
644644
y: y,
645645
button: event.button,
646646
step: event.step,
647+
buttons: event.buttons,
647648
modifiers: getModifiers(event),
648649
guiEvent: simpleKeys(event),
649650
});

0 commit comments

Comments
 (0)