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

Skip to content

Commit 78bece9

Browse files
committed
Check pressed mouse buttons in pan/zoom drag handlers.
Sometimes, the mouse_release_event ending a pan/zoom can be lost, if it occurs while the canvas does not have focus (a typical case is when a context menu is implemented on top of the canvas, see example below); this can result in rather confusing behavior as the pan/zoom continues which no mouse button is pressed. To fix this, always check that the correct button is still pressed in the motion_notify_event handlers. To test, use 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() ``` enter pan/zoom mode, right-click to open the context menu, exit the menu, and continue moving the mouse.
1 parent 6a8211e commit 78bece9

File tree

4 files changed

+42
-15
lines changed

4 files changed

+42
-15
lines changed

lib/matplotlib/backend_bases.py

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3059,6 +3059,11 @@ def press_pan(self, event):
30593059

30603060
def drag_pan(self, event):
30613061
"""Callback for dragging in pan/zoom mode."""
3062+
if event.buttons != {self._pan_info.button}:
3063+
# Zoom ended while canvas not in focus (it did not receive a
3064+
# button_release_event); cancel it.
3065+
self.release_pan(None) # release_pan doesn't actually use event.
3066+
return
30623067
for ax in self._pan_info.axes:
30633068
# Using the recorded button at the press is safer than the current
30643069
# button, as multiple buttons can get pressed during motion.
@@ -3092,7 +3097,7 @@ def zoom(self, *args):
30923097
for a in self.canvas.figure.get_axes():
30933098
a.set_navigate_mode(self.mode._navigate_mode)
30943099

3095-
_ZoomInfo = namedtuple("_ZoomInfo", "direction start_xy axes cid cbar")
3100+
_ZoomInfo = namedtuple("_ZoomInfo", "button start_xy axes cid cbar")
30963101

30973102
def press_zoom(self, event):
30983103
"""Callback for mouse button press in zoom to rect mode."""
@@ -3117,11 +3122,17 @@ def press_zoom(self, event):
31173122
cbar = None
31183123

31193124
self._zoom_info = self._ZoomInfo(
3120-
direction="in" if event.button == 1 else "out",
3121-
start_xy=(event.x, event.y), axes=axes, cid=id_zoom, cbar=cbar)
3125+
button=event.button, start_xy=(event.x, event.y), axes=axes,
3126+
cid=id_zoom, cbar=cbar)
31223127

31233128
def drag_zoom(self, event):
31243129
"""Callback for dragging in zoom mode."""
3130+
if event.buttons != {self._zoom_info.button}:
3131+
# Zoom ended while canvas not in focus (it did not receive a
3132+
# button_release_event); cancel it.
3133+
self._cleanup_post_zoom()
3134+
return
3135+
31253136
start_xy = self._zoom_info.start_xy
31263137
ax = self._zoom_info.axes[0]
31273138
(x1, y1), (x2, y2) = np.clip(
@@ -3150,6 +3161,7 @@ def release_zoom(self, event):
31503161
self.remove_rubberband()
31513162

31523163
start_x, start_y = self._zoom_info.start_xy
3164+
direction = "in" if self._zoom_info.button == 1 else "out"
31533165
key = event.key
31543166
# Force the key on colorbars to ignore the zoom-cancel on the
31553167
# short-axis side
@@ -3161,8 +3173,7 @@ def release_zoom(self, event):
31613173
# "cancel" a zoom action by zooming by less than 5 pixels.
31623174
if ((abs(event.x - start_x) < 5 and key != "y") or
31633175
(abs(event.y - start_y) < 5 and key != "x")):
3164-
self.canvas.draw_idle()
3165-
self._zoom_info = None
3176+
self._cleanup_post_zoom()
31663177
return
31673178

31683179
for i, ax in enumerate(self._zoom_info.axes):
@@ -3174,11 +3185,18 @@ def release_zoom(self, event):
31743185
for prev in self._zoom_info.axes[:i])
31753186
ax._set_view_from_bbox(
31763187
(start_x, start_y, event.x, event.y),
3177-
self._zoom_info.direction, key, twinx, twiny)
3188+
direction, key, twinx, twiny)
3189+
3190+
self._cleanup_post_zoom()
3191+
self.push_current()
31783192

3193+
def _cleanup_post_zoom(self):
3194+
# We don't check the event button here, so that zooms can be cancelled
3195+
# by (pressing and) releasing another mouse button.
3196+
self.canvas.mpl_disconnect(self._zoom_info.cid)
3197+
self.remove_rubberband()
31793198
self.canvas.draw_idle()
31803199
self._zoom_info = None
3181-
self.push_current()
31823200

31833201
def push_current(self):
31843202
"""Push the current view limits and position onto the stack."""

lib/matplotlib/backend_bases.pyi

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -429,7 +429,7 @@ class NavigationToolbar2:
429429
def zoom(self, *args) -> None: ...
430430

431431
class _ZoomInfo(NamedTuple):
432-
direction: Literal["in", "out"]
432+
button: MouseButton
433433
start_xy: tuple[float, float]
434434
axes: list[Axes]
435435
cid: int

lib/matplotlib/tests/test_backend_bases.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -260,19 +260,21 @@ def test_interactive_colorbar(plot_func, orientation, tool, button, expected):
260260
# Set up the mouse movements
261261
start_event = MouseEvent(
262262
"button_press_event", fig.canvas, *s0, button)
263+
drag_event = MouseEvent(
264+
"motion_notify_event", fig.canvas, *s1, button, buttons={button})
263265
stop_event = MouseEvent(
264266
"button_release_event", fig.canvas, *s1, button)
265267

266268
tb = NavigationToolbar2(fig.canvas)
267269
if tool == "zoom":
268270
tb.zoom()
269271
tb.press_zoom(start_event)
270-
tb.drag_zoom(stop_event)
272+
tb.drag_zoom(drag_event)
271273
tb.release_zoom(stop_event)
272274
else:
273275
tb.pan()
274276
tb.press_pan(start_event)
275-
tb.drag_pan(stop_event)
277+
tb.drag_pan(drag_event)
276278
tb.release_pan(stop_event)
277279

278280
# Should be close, but won't be exact due to screen integer resolution
@@ -395,14 +397,17 @@ def test_interactive_pan(key, mouseend, expectedxlim, expectedylim):
395397
start_event = MouseEvent(
396398
"button_press_event", fig.canvas, *sstart, button=MouseButton.LEFT,
397399
key=key)
400+
drag_event = MouseEvent(
401+
"motion_notify_event", fig.canvas, *send, button=MouseButton.LEFT,
402+
buttons={MouseButton.LEFT}, key=key)
398403
stop_event = MouseEvent(
399404
"button_release_event", fig.canvas, *send, button=MouseButton.LEFT,
400405
key=key)
401406

402407
tb = NavigationToolbar2(fig.canvas)
403408
tb.pan()
404409
tb.press_pan(start_event)
405-
tb.drag_pan(stop_event)
410+
tb.drag_pan(drag_event)
406411
tb.release_pan(stop_event)
407412
# Should be close, but won't be exact due to screen integer resolution
408413
assert tuple(ax.get_xlim()) == pytest.approx(expectedxlim, abs=0.02)
@@ -510,6 +515,8 @@ def test_interactive_pan_zoom_events(tool, button, patch_vis, forward_nav, t_s):
510515

511516
# Set up the mouse movements
512517
start_event = MouseEvent("button_press_event", fig.canvas, *s0, button)
518+
drag_event = MouseEvent(
519+
"motion_notify_event", fig.canvas, *s1, button, buttons={button})
513520
stop_event = MouseEvent("button_release_event", fig.canvas, *s1, button)
514521

515522
tb = NavigationToolbar2(fig.canvas)
@@ -534,7 +541,7 @@ def test_interactive_pan_zoom_events(tool, button, patch_vis, forward_nav, t_s):
534541

535542
tb.zoom()
536543
tb.press_zoom(start_event)
537-
tb.drag_zoom(stop_event)
544+
tb.drag_zoom(drag_event)
538545
tb.release_zoom(stop_event)
539546

540547
assert ax_t.get_xlim() == pytest.approx(xlim_t, abs=0.15)
@@ -570,7 +577,7 @@ def test_interactive_pan_zoom_events(tool, button, patch_vis, forward_nav, t_s):
570577

571578
tb.pan()
572579
tb.press_pan(start_event)
573-
tb.drag_pan(stop_event)
580+
tb.drag_pan(drag_event)
574581
tb.release_pan(stop_event)
575582

576583
assert ax_t.get_xlim() == pytest.approx(xlim_t, abs=0.15)

lib/mpl_toolkits/mplot3d/tests/test_axes3d.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2130,19 +2130,21 @@ def test_toolbar_zoom_pan(tool, button, key, expected):
21302130
# Set up the mouse movements
21312131
start_event = MouseEvent(
21322132
"button_press_event", fig.canvas, *s0, button, key=key)
2133+
drag_event = MouseEvent(
2134+
"motion_notify_event", fig.canvas, *s1, button, key=key, buttons={button})
21332135
stop_event = MouseEvent(
21342136
"button_release_event", fig.canvas, *s1, button, key=key)
21352137

21362138
tb = NavigationToolbar2(fig.canvas)
21372139
if tool == "zoom":
21382140
tb.zoom()
21392141
tb.press_zoom(start_event)
2140-
tb.drag_zoom(stop_event)
2142+
tb.drag_zoom(drag_event)
21412143
tb.release_zoom(stop_event)
21422144
else:
21432145
tb.pan()
21442146
tb.press_pan(start_event)
2145-
tb.drag_pan(stop_event)
2147+
tb.drag_pan(drag_event)
21462148
tb.release_pan(stop_event)
21472149

21482150
# Should be close, but won't be exact due to screen integer resolution

0 commit comments

Comments
 (0)