Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit cd37b73

Browse files
committed
Stop relying on dead-reckoning mouse buttons for motion_notify_event.
Previously, for motion_notify_event, `event.attribute` was set by checking the last button_press_event/button_release event. This is brittle for the same reason as to why we introduced `event.modifiers` to improve over `event.key`, so introduce the more robust `event.buttons` (see detailed discussion in the attribute docstring). For a concrete example, consider e.g. from matplotlib import pyplot as plt from matplotlib.backends.qt_compat import QtWidgets def on_button_press(event): if event.button != 3: # Right-click. return menu = QtWidgets.QMenu() menu.addAction("Some menu action", lambda: None) menu.exec(event.guiEvent.globalPosition().toPoint()) fig = plt.figure() fig.canvas.mpl_connect("button_press_event", on_button_press) fig.add_subplot() plt.show() (connecting a contextual menu on right button click) where a right click while having selected zoom mode on the toolbar starts a zoom mode that stays on even after the mouse release (because the mouse release event is received by the menu widget, not by the main canvas). This PR does not fix the issue, but will allow a followup fix (where the motion_notify_event associated with zoom mode will be able to first check whether the button is indeed still pressed when the motion occurs). Limitations, on macOS only (everything works on Linux and Windows AFAICT): - tk only reports a single pressed button even if multiple buttons are pressed. - gtk4 spams the terminal with Gtk-WARNINGs: "Broken accounting of active state for widget ..." on right-clicks only; similar issues appear to have been reported a while ago to Gtk (https://gitlab.gnome.org/GNOME/gtk/-/issues/3356 and linked issues) but it's unclear whether any action was taken on their side. (Alternatively, some GUI toolkits have a "permissive" notion of drag events defined as mouse moves with a button pressed, which we could use as well to define event.button{,s} for motion_notify_event, but e.g. Qt attaches quite heavy semantics to drags which we probably don't want to bother with.)
1 parent ac1d009 commit cd37b73

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)