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

Skip to content

Commit 03589b1

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 c010a36 commit 03589b1

File tree

9 files changed

+178
-44
lines changed

9 files changed

+178
-44
lines changed

lib/matplotlib/backend_bases.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1342,6 +1342,28 @@ class MouseEvent(LocationEvent):
13421342
If this is unset, *name* is "scroll_event", and *step* is nonzero, then
13431343
this will be set to "up" or "down" depending on the sign of *step*.
13441344
1345+
buttons : None or frozenset
1346+
For 'motion_notify_event', the mouse buttons currently being pressed
1347+
(a set of zero or more MouseButtons);
1348+
for other events, None.
1349+
1350+
.. note::
1351+
For 'motion_notify_event', this attribute is more accurate than
1352+
the ``button`` (singular) attribute, which is obtained from the last
1353+
'button_press_event' or 'button_release_event' that occurred within
1354+
the canvas (and thus 1. be wrong if the last change in mouse state
1355+
occurred when the canvas did not have focus, and 2. cannot report
1356+
when multiple buttons are pressed).
1357+
1358+
This attribute is not set for 'button_press_event' and
1359+
'button_release_event' because GUI toolkits are inconsistent as to
1360+
whether they report the button state *before* or *after* the
1361+
press/release occurred.
1362+
1363+
.. warning::
1364+
On macOS, the Tk backends only report a single button even if
1365+
multiple buttons are pressed.
1366+
13451367
key : None or str
13461368
The key pressed when the mouse event triggered, e.g. 'shift'.
13471369
See `KeyEvent`.
@@ -1374,7 +1396,8 @@ def on_press(event):
13741396
"""
13751397

13761398
def __init__(self, name, canvas, x, y, button=None, key=None,
1377-
step=0, dblclick=False, guiEvent=None, *, modifiers=None):
1399+
step=0, dblclick=False, guiEvent=None, *,
1400+
buttons=None, modifiers=None):
13781401
super().__init__(
13791402
name, canvas, x, y, guiEvent=guiEvent, modifiers=modifiers)
13801403
if button in MouseButton.__members__.values():
@@ -1385,6 +1408,16 @@ def __init__(self, name, canvas, x, y, button=None, key=None,
13851408
elif step < 0:
13861409
button = "down"
13871410
self.button = button
1411+
if name == "motion_notify_event":
1412+
self.buttons = frozenset(buttons if buttons is not None else [])
1413+
else:
1414+
# We don't support 'buttons' for button_press/release_event because
1415+
# toolkits are inconsistent as to whether they report the state
1416+
# before or after the event.
1417+
if buttons:
1418+
raise ValueError(
1419+
"'buttons' is only supported for 'motion_notify_event'")
1420+
self.buttons = None
13881421
self.key = key
13891422
self.step = step
13901423
self.dblclick = dblclick

lib/matplotlib/backends/_backend_tk.py

Lines changed: 19 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
@@ -293,6 +293,7 @@ def _event_mpl_coords(self, event):
293293
def motion_notify_event(self, event):
294294
MouseEvent("motion_notify_event", self,
295295
*self._event_mpl_coords(event),
296+
buttons=self._mpl_buttons(event),
296297
modifiers=self._mpl_modifiers(event),
297298
guiEvent=event)._process()
298299

@@ -354,13 +355,25 @@ def scroll_event_windows(self, event):
354355
x, y, step=step, modifiers=self._mpl_modifiers(event),
355356
guiEvent=event)._process()
356357

358+
@staticmethod
359+
def _mpl_buttons(event): # See _mpl_modifiers.
360+
# NOTE: This fails to report multiclicks on macOS; only one button is
361+
# reported (multiclicks work correctly on Linux & Windows).
362+
modifiers = [
363+
(MouseButton.LEFT, 1 << 8),
364+
(MouseButton.RIGHT, 1 << 9),
365+
(MouseButton.MIDDLE, 1 << 10),
366+
(MouseButton.BACK, 1 << 11),
367+
(MouseButton.FORWARD, 1 << 12),
368+
]
369+
# State *before* press/release.
370+
return [name for name, mask in modifiers if event.state & mask]
371+
357372
@staticmethod
358373
def _mpl_modifiers(event, *, exclude=None):
359-
# add modifier keys to the key string. Bit details originate from
360-
# http://effbot.org/tkinterbook/tkinter-events-and-bindings.htm
361-
# BIT_SHIFT = 0x001; BIT_CAPSLOCK = 0x002; BIT_CONTROL = 0x004;
362-
# BIT_LEFT_ALT = 0x008; BIT_NUMLOCK = 0x010; BIT_RIGHT_ALT = 0x080;
363-
# BIT_MB_1 = 0x100; BIT_MB_2 = 0x200; BIT_MB_3 = 0x400;
374+
# Add modifier keys to the key string. Bit values are inferred from
375+
# the implementation of tkinter.Event.__repr__ (1, 2, 4, 8, ... =
376+
# Shift, Lock, Control, Mod1, ..., Mod5, Button1, ..., Button5)
364377
# In general, the modifier key is excluded from the modifier flag,
365378
# however this is not the case on "darwin", so double check that
366379
# 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
@@ -151,6 +151,7 @@ def key_release_event(self, controller, keyval, keycode, state):
151151
def motion_notify_event(self, controller, x, y):
152152
MouseEvent(
153153
"motion_notify_event", self, *self._mpl_coords((x, y)),
154+
buttons=self._mpl_buttons(controller),
154155
modifiers=self._mpl_modifiers(controller),
155156
)._process()
156157

@@ -175,6 +176,26 @@ def resize_event(self, area, width, height):
175176
ResizeEvent("resize_event", self)._process()
176177
self.draw_idle()
177178

179+
def _mpl_buttons(self, controller):
180+
# NOTE: This spews "Broken accounting of active state" warnings on
181+
# right click on macOS.
182+
surface = self.get_native().get_surface()
183+
is_over, x, y, event_state = surface.get_device_position(
184+
self.get_display().get_default_seat().get_pointer())
185+
# NOTE: alternatively we could use
186+
# event_state = controller.get_current_event_state()
187+
# but for button_press/button_release this would report the state
188+
# *prior* to the event rather than after it; the above reports the
189+
# state *after* it.
190+
mod_table = [
191+
(MouseButton.LEFT, Gdk.ModifierType.BUTTON1_MASK),
192+
(MouseButton.MIDDLE, Gdk.ModifierType.BUTTON2_MASK),
193+
(MouseButton.RIGHT, Gdk.ModifierType.BUTTON3_MASK),
194+
(MouseButton.BACK, Gdk.ModifierType.BUTTON4_MASK),
195+
(MouseButton.FORWARD, Gdk.ModifierType.BUTTON5_MASK),
196+
]
197+
return {name for name, mask in mod_table if event_state & mask}
198+
178199
def _mpl_modifiers(self, controller=None):
179200
if controller is None:
180201
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 minumumSizeHint(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)