Skip to content
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 @@ class MouseEvent(LocationEvent):
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 on_press(event):
"""

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 @@ def __init__(self, name, canvas, x, y, button=None, key=None,
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(
"'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 _event_mpl_coords(self, event):
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 @@ def scroll_event_windows(self, event):
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 = [
# 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]

@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 key_release_event(self, widget, event):

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 @@ def size_allocate(self, widget, allocation):
ResizeEvent("resize_event", self)._process()
self.draw_idle()

@staticmethod
def _mpl_buttons(event_state):
modifiers = [
(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]

@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 key_release_event(self, controller, keyval, keycode, state):
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 @@ def resize_event(self, area, width, height):
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(
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 = [
(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}

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 @@ def mouseMoveEvent(self, event):
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 sizeHint(self):
def minimumSizeHint(self):
return QtCore.QSize(10, 10)

@staticmethod
def _mpl_buttons(buttons):
buttons = _to_int(buttons)
# State *after* press/release.
return {button for mask, button in FigureCanvasQT.buttond.items()
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 @@ def _handle_mouse(self, event):
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.
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 @@ def _handle_mouse(self, event):
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 @@ def _on_size(self, event):
ResizeEvent("resize_event", self)._process()
self.draw_idle()

@staticmethod
def _mpl_buttons():
state = wx.GetMouseState()
# 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 = [
(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}

@staticmethod
def _mpl_modifiers(event=None, *, exclude=None):
mod_table = [
Expand Down Expand Up @@ -794,9 +810,8 @@ def _on_mouse_button(self, event):
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,
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 @@ def _on_motion(self, event):
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