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

Skip to content

Stop relying on dead-reckoning mouse buttons for motion_notify_event. #28453

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Oct 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 34 additions & 1 deletion lib/matplotlib/backend_bases.py
Original file line number Diff line number Diff line change
Expand Up @@ -1324,6 +1324,28 @@
If this is unset, *name* is "scroll_event", and *step* is nonzero, then
this will be set to "up" or "down" depending on the sign of *step*.

buttons : None or frozenset
For 'motion_notify_event', the mouse buttons currently being pressed
(a set of zero or more MouseButtons);
for other events, None.

.. note::
For 'motion_notify_event', this attribute is more accurate than
the ``button`` (singular) attribute, which is obtained from the last
'button_press_event' or 'button_release_event' that occurred within
the canvas (and thus 1. be wrong if the last change in mouse state
occurred when the canvas did not have focus, and 2. cannot report
when multiple buttons are pressed).

This attribute is not set for 'button_press_event' and
'button_release_event' because GUI toolkits are inconsistent as to
whether they report the button state *before* or *after* the
press/release occurred.

.. warning::
On macOS, the Tk backends only report a single button even if
multiple buttons are pressed.

key : None or str
The key pressed when the mouse event triggered, e.g. 'shift'.
See `KeyEvent`.
Expand Down Expand Up @@ -1356,7 +1378,8 @@
"""

def __init__(self, name, canvas, x, y, button=None, key=None,
step=0, dblclick=False, guiEvent=None, *, modifiers=None):
step=0, dblclick=False, guiEvent=None, *,
buttons=None, modifiers=None):
super().__init__(
name, canvas, x, y, guiEvent=guiEvent, modifiers=modifiers)
if button in MouseButton.__members__.values():
Expand All @@ -1367,6 +1390,16 @@
elif step < 0:
button = "down"
self.button = button
if name == "motion_notify_event":
self.buttons = frozenset(buttons if buttons is not None else [])
else:
# We don't support 'buttons' for button_press/release_event because
# toolkits are inconsistent as to whether they report the state
# before or after the event.
if buttons:
raise ValueError(

Check warning on line 1400 in lib/matplotlib/backend_bases.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/backend_bases.py#L1400

Added line #L1400 was not covered by tests
"'buttons' is only supported for 'motion_notify_event'")
self.buttons = None
self.key = key
self.step = step
self.dblclick = dblclick
Expand Down
1 change: 1 addition & 0 deletions lib/matplotlib/backend_bases.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@ class MouseEvent(LocationEvent):
dblclick: bool = ...,
guiEvent: Any | None = ...,
*,
buttons: Iterable[MouseButton] | None = ...,
modifiers: Iterable[str] | None = ...,
) -> None: ...

Expand Down
33 changes: 27 additions & 6 deletions lib/matplotlib/backends/_backend_tk.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from matplotlib import _api, backend_tools, cbook, _c_internal_utils
from matplotlib.backend_bases import (
_Backend, FigureCanvasBase, FigureManagerBase, NavigationToolbar2,
TimerBase, ToolContainerBase, cursors, _Mode,
TimerBase, ToolContainerBase, cursors, _Mode, MouseButton,
CloseEvent, KeyEvent, LocationEvent, MouseEvent, ResizeEvent)
from matplotlib._pylab_helpers import Gcf
from . import _tkagg
Expand Down Expand Up @@ -296,6 +296,7 @@
def motion_notify_event(self, event):
MouseEvent("motion_notify_event", self,
*self._event_mpl_coords(event),
buttons=self._mpl_buttons(event),
modifiers=self._mpl_modifiers(event),
guiEvent=event)._process()

Expand Down Expand Up @@ -357,13 +358,33 @@
x, y, step=step, modifiers=self._mpl_modifiers(event),
guiEvent=event)._process()

@staticmethod
def _mpl_buttons(event): # See _mpl_modifiers.
# NOTE: This fails to report multiclicks on macOS; only one button is
# reported (multiclicks work correctly on Linux & Windows).
modifiers = [

Check warning on line 365 in lib/matplotlib/backends/_backend_tk.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/backends/_backend_tk.py#L365

Added line #L365 was not covered by tests
# macOS appears to swap right and middle (look for "Swap buttons
# 2/3" in tk/macosx/tkMacOSXMouseEvent.c).
(MouseButton.LEFT, 1 << 8),
(MouseButton.RIGHT, 1 << 9),
(MouseButton.MIDDLE, 1 << 10),
(MouseButton.BACK, 1 << 11),
(MouseButton.FORWARD, 1 << 12),
] if sys.platform == "darwin" else [
(MouseButton.LEFT, 1 << 8),
(MouseButton.MIDDLE, 1 << 9),
(MouseButton.RIGHT, 1 << 10),
(MouseButton.BACK, 1 << 11),
(MouseButton.FORWARD, 1 << 12),
]
# State *before* press/release.
return [name for name, mask in modifiers if event.state & mask]

Check warning on line 381 in lib/matplotlib/backends/_backend_tk.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/backends/_backend_tk.py#L381

Added line #L381 was not covered by tests

@staticmethod
def _mpl_modifiers(event, *, exclude=None):
# add modifier keys to the key string. Bit details originate from
# http://effbot.org/tkinterbook/tkinter-events-and-bindings.htm
# BIT_SHIFT = 0x001; BIT_CAPSLOCK = 0x002; BIT_CONTROL = 0x004;
# BIT_LEFT_ALT = 0x008; BIT_NUMLOCK = 0x010; BIT_RIGHT_ALT = 0x080;
# BIT_MB_1 = 0x100; BIT_MB_2 = 0x200; BIT_MB_3 = 0x400;
# Add modifier keys to the key string. Bit values are inferred from
# the implementation of tkinter.Event.__repr__ (1, 2, 4, 8, ... =
# Shift, Lock, Control, Mod1, ..., Mod5, Button1, ..., Button5)
# In general, the modifier key is excluded from the modifier flag,
# however this is not the case on "darwin", so double check that
# we aren't adding repeat modifier flags to a modifier key.
Expand Down
17 changes: 15 additions & 2 deletions lib/matplotlib/backends/backend_gtk3.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
import matplotlib as mpl
from matplotlib import _api, backend_tools, cbook
from matplotlib.backend_bases import (
ToolContainerBase, CloseEvent, KeyEvent, LocationEvent, MouseEvent,
ResizeEvent)
ToolContainerBase, MouseButton,
CloseEvent, KeyEvent, LocationEvent, MouseEvent, ResizeEvent)

try:
import gi
Expand Down Expand Up @@ -156,6 +156,7 @@

def motion_notify_event(self, widget, event):
MouseEvent("motion_notify_event", self, *self._mpl_coords(event),
buttons=self._mpl_buttons(event.state),
modifiers=self._mpl_modifiers(event.state),
guiEvent=event)._process()
return False # finish event propagation?
Expand All @@ -182,6 +183,18 @@
ResizeEvent("resize_event", self)._process()
self.draw_idle()

@staticmethod
def _mpl_buttons(event_state):
modifiers = [

Check warning on line 188 in lib/matplotlib/backends/backend_gtk3.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/backends/backend_gtk3.py#L188

Added line #L188 was not covered by tests
(MouseButton.LEFT, Gdk.ModifierType.BUTTON1_MASK),
(MouseButton.MIDDLE, Gdk.ModifierType.BUTTON2_MASK),
(MouseButton.RIGHT, Gdk.ModifierType.BUTTON3_MASK),
(MouseButton.BACK, Gdk.ModifierType.BUTTON4_MASK),
(MouseButton.FORWARD, Gdk.ModifierType.BUTTON5_MASK),
]
# State *before* press/release.
return [name for name, mask in modifiers if event_state & mask]

Check warning on line 196 in lib/matplotlib/backends/backend_gtk3.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/backends/backend_gtk3.py#L196

Added line #L196 was not covered by tests

@staticmethod
def _mpl_modifiers(event_state, *, exclude=None):
modifiers = [
Expand Down
25 changes: 23 additions & 2 deletions lib/matplotlib/backends/backend_gtk4.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
import matplotlib as mpl
from matplotlib import _api, backend_tools, cbook
from matplotlib.backend_bases import (
ToolContainerBase, KeyEvent, LocationEvent, MouseEvent, ResizeEvent,
CloseEvent)
ToolContainerBase, MouseButton,
KeyEvent, LocationEvent, MouseEvent, ResizeEvent, CloseEvent)

try:
import gi
Expand Down Expand Up @@ -155,6 +155,7 @@
def motion_notify_event(self, controller, x, y):
MouseEvent(
"motion_notify_event", self, *self._mpl_coords((x, y)),
buttons=self._mpl_buttons(controller),
modifiers=self._mpl_modifiers(controller),
guiEvent=controller.get_current_event(),
)._process()
Expand Down Expand Up @@ -182,6 +183,26 @@
ResizeEvent("resize_event", self)._process()
self.draw_idle()

def _mpl_buttons(self, controller):
# NOTE: This spews "Broken accounting of active state" warnings on
# right click on macOS.
surface = self.get_native().get_surface()
is_over, x, y, event_state = surface.get_device_position(

Check warning on line 190 in lib/matplotlib/backends/backend_gtk4.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/backends/backend_gtk4.py#L189-L190

Added lines #L189 - L190 were not covered by tests
self.get_display().get_default_seat().get_pointer())
# NOTE: alternatively we could use
# event_state = controller.get_current_event_state()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that MouseEvent rejects passing buttons for anything but move events, should we use the non-spammy version?

Copy link
Contributor Author

@anntzer anntzer Jun 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both versions are spammy :/ Perhaps @QuLogic may be knowledgeable here?
I think this definitely needs to be fixed before merging; I would hope that gtk4 offers some non-broken way of checking mouse button state on macos... Fixing multiclicks+tk+macos, while desirable, seems to be less of a blocker for me; that issue won't prevent fixing the example in the initial message.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Also, this appears empirically to have possibly been fixed at some point between gtk4.14.2 and 4.14.5...)

# but for button_press/button_release this would report the state
# *prior* to the event rather than after it; the above reports the
# state *after* it.
mod_table = [

Check warning on line 197 in lib/matplotlib/backends/backend_gtk4.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/backends/backend_gtk4.py#L197

Added line #L197 was not covered by tests
(MouseButton.LEFT, Gdk.ModifierType.BUTTON1_MASK),
(MouseButton.MIDDLE, Gdk.ModifierType.BUTTON2_MASK),
(MouseButton.RIGHT, Gdk.ModifierType.BUTTON3_MASK),
(MouseButton.BACK, Gdk.ModifierType.BUTTON4_MASK),
(MouseButton.FORWARD, Gdk.ModifierType.BUTTON5_MASK),
]
return {name for name, mask in mod_table if event_state & mask}

Check warning on line 204 in lib/matplotlib/backends/backend_gtk4.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/backends/backend_gtk4.py#L204

Added line #L204 was not covered by tests

def _mpl_modifiers(self, controller=None):
if controller is None:
surface = self.get_native().get_surface()
Expand Down
8 changes: 8 additions & 0 deletions lib/matplotlib/backends/backend_qt.py
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,7 @@
return
MouseEvent("motion_notify_event", self,
*self.mouseEventCoords(event),
buttons=self._mpl_buttons(event.buttons()),
modifiers=self._mpl_modifiers(),
guiEvent=event)._process()

Expand Down Expand Up @@ -396,6 +397,13 @@
def minimumSizeHint(self):
return QtCore.QSize(10, 10)

@staticmethod
def _mpl_buttons(buttons):
buttons = _to_int(buttons)

Check warning on line 402 in lib/matplotlib/backends/backend_qt.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/backends/backend_qt.py#L402

Added line #L402 was not covered by tests
# State *after* press/release.
return {button for mask, button in FigureCanvasQT.buttond.items()

Check warning on line 404 in lib/matplotlib/backends/backend_qt.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/backends/backend_qt.py#L404

Added line #L404 was not covered by tests
if _to_int(mask) & buttons}

@staticmethod
def _mpl_modifiers(modifiers=None, *, exclude=None):
if modifiers is None:
Expand Down
19 changes: 14 additions & 5 deletions lib/matplotlib/backends/backend_webagg_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from matplotlib import _api, backend_bases, backend_tools
from matplotlib.backends import backend_agg
from matplotlib.backend_bases import (
_Backend, KeyEvent, LocationEvent, MouseEvent, ResizeEvent)
_Backend, MouseButton, KeyEvent, LocationEvent, MouseEvent, ResizeEvent)

_log = logging.getLogger(__name__)

Expand Down Expand Up @@ -283,10 +283,17 @@
y = event['y']
y = self.get_renderer().height - y
self._last_mouse_xy = x, y
# JavaScript button numbers and Matplotlib button numbers are off by 1.
button = event['button'] + 1

e_type = event['type']
button = event['button'] + 1 # JS numbers off by 1 compared to mpl.
buttons = { # JS ordering different compared to mpl.

Check warning on line 288 in lib/matplotlib/backends/backend_webagg_core.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/backends/backend_webagg_core.py#L287-L288

Added lines #L287 - L288 were not covered by tests
button for button, mask in [
(MouseButton.LEFT, 1),
(MouseButton.RIGHT, 2),
(MouseButton.MIDDLE, 4),
(MouseButton.BACK, 8),
(MouseButton.FORWARD, 16),
] if event['buttons'] & mask # State *after* press/release.
}
modifiers = event['modifiers']
guiEvent = event.get('guiEvent')
if e_type in ['button_press', 'button_release']:
Expand All @@ -300,10 +307,12 @@
modifiers=modifiers, guiEvent=guiEvent)._process()
elif e_type == 'motion_notify':
MouseEvent(e_type + '_event', self, x, y,
modifiers=modifiers, guiEvent=guiEvent)._process()
buttons=buttons, modifiers=modifiers, guiEvent=guiEvent,
)._process()
elif e_type in ['figure_enter', 'figure_leave']:
LocationEvent(e_type + '_event', self, x, y,
modifiers=modifiers, guiEvent=guiEvent)._process()

handle_button_press = handle_button_release = handle_dblclick = \
handle_figure_enter = handle_figure_leave = handle_motion_notify = \
handle_scroll = _handle_mouse
Expand Down
22 changes: 19 additions & 3 deletions lib/matplotlib/backends/backend_wx.py
Original file line number Diff line number Diff line change
Expand Up @@ -685,6 +685,22 @@
ResizeEvent("resize_event", self)._process()
self.draw_idle()

@staticmethod
def _mpl_buttons():
state = wx.GetMouseState()

Check warning on line 690 in lib/matplotlib/backends/backend_wx.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/backends/backend_wx.py#L690

Added line #L690 was not covered by tests
# NOTE: Alternatively, we could use event.LeftIsDown() / etc. but this
# fails to report multiclick drags on macOS (other OSes have not been
# verified).
mod_table = [

Check warning on line 694 in lib/matplotlib/backends/backend_wx.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/backends/backend_wx.py#L694

Added line #L694 was not covered by tests
(MouseButton.LEFT, state.LeftIsDown()),
(MouseButton.RIGHT, state.RightIsDown()),
(MouseButton.MIDDLE, state.MiddleIsDown()),
(MouseButton.BACK, state.Aux1IsDown()),
(MouseButton.FORWARD, state.Aux2IsDown()),
]
# State *after* press/release.
return {button for button, flag in mod_table if flag}

Check warning on line 702 in lib/matplotlib/backends/backend_wx.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/backends/backend_wx.py#L702

Added line #L702 was not covered by tests

@staticmethod
def _mpl_modifiers(event=None, *, exclude=None):
mod_table = [
Expand Down Expand Up @@ -794,9 +810,8 @@
MouseEvent("button_press_event", self, x, y, button,
modifiers=modifiers, guiEvent=event)._process()
elif event.ButtonDClick():
MouseEvent("button_press_event", self, x, y, button,
dblclick=True, modifiers=modifiers,
guiEvent=event)._process()
MouseEvent("button_press_event", self, x, y, button, dblclick=True,

Check warning on line 813 in lib/matplotlib/backends/backend_wx.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/backends/backend_wx.py#L813

Added line #L813 was not covered by tests
modifiers=modifiers, guiEvent=event)._process()
elif event.ButtonUp():
MouseEvent("button_release_event", self, x, y, button,
modifiers=modifiers, guiEvent=event)._process()
Expand Down Expand Up @@ -826,6 +841,7 @@
event.Skip()
MouseEvent("motion_notify_event", self,
*self._mpl_coords(event),
buttons=self._mpl_buttons(),
modifiers=self._mpl_modifiers(event),
guiEvent=event)._process()

Expand Down
1 change: 1 addition & 0 deletions lib/matplotlib/backends/web_backend/js/mpl.js
Original file line number Diff line number Diff line change
Expand Up @@ -644,6 +644,7 @@ mpl.figure.prototype.mouse_event = function (event, name) {
y: y,
button: event.button,
step: event.step,
buttons: event.buttons,
modifiers: getModifiers(event),
guiEvent: simpleKeys(event),
});
Expand Down
Loading
Loading