Skip to content
8 changes: 8 additions & 0 deletions lib/matplotlib/backends/_backend_tk.py
Original file line number Diff line number Diff line change
Expand Up @@ -646,6 +646,14 @@ def full_screen_toggle(self):
is_fullscreen = bool(self.window.attributes('-fullscreen'))
self.window.attributes('-fullscreen', not is_fullscreen)

def context_menu(self, event, labels=None, actions=None):
if labels is None or actions is None:
return
menu = tk.Menu(self.window, tearoff=0)
for label, action in zip(labels, actions):
menu.add_command(label=label, command=action)
menu.tk_popup(event.guiEvent.x_root, event.guiEvent.y_root)
Comment on lines +649 to +655
Copy link
Owner

Choose a reason for hiding this comment

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

Why is tk's context menu so horrible? 😂
Just check once whether you can do something about it, else lite only

image

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

For me it looks fine only
image



class NavigationToolbar2Tk(NavigationToolbar2, tk.Frame):
def __init__(self, canvas, window=None, *, pack_toolbar=True):
Expand Down
11 changes: 11 additions & 0 deletions lib/matplotlib/backends/backend_gtk3.py
Original file line number Diff line number Diff line change
Expand Up @@ -591,6 +591,17 @@ class FigureManagerGTK3(_FigureManagerGTK):
_toolbar2_class = NavigationToolbar2GTK3
_toolmanager_toolbar_class = ToolbarGTK3

def context_menu(self, event, labels=None, actions=None):
if labels is None or actions is None:
return
menu = Gtk.Menu()
for label, action in zip(labels, actions):
item = Gtk.MenuItem(label=label)
menu.append(item)
item.connect('activate', lambda _, a=action: a())
item.show()
menu.popup_at_pointer(event.guiEvent)


@_BackendGTK.export
class _BackendGTK3(_BackendGTK):
Expand Down
26 changes: 26 additions & 0 deletions lib/matplotlib/backends/backend_gtk4.py
Original file line number Diff line number Diff line change
Expand Up @@ -637,6 +637,32 @@ class FigureManagerGTK4(_FigureManagerGTK):
_toolbar2_class = NavigationToolbar2GTK4
_toolmanager_toolbar_class = ToolbarGTK4

def context_menu(self, event, labels=None, actions=None):
if not labels or not actions:
return
menu = Gio.Menu()
action_group = Gio.SimpleActionGroup()
for i, (label, action) in enumerate(zip(labels, actions)):
action_name = f"{label}"
g_action = Gio.SimpleAction.new(action_name, None)
g_action.connect("activate", lambda *_, a=action: a())
action_group.add_action(g_action)
menu.append(label, f"win.{action_name}")
self.canvas.insert_action_group("win", action_group)
popover = Gtk.PopoverMenu.new_from_model(menu)
popover.set_parent(self.canvas)
popover.set_has_arrow(False)
popover.set_halign(Gtk.Align.START)
scale = self.canvas.get_scale_factor()
height = self.canvas.get_height()
rect = Gdk.Rectangle()
rect.x = int(event.x / scale)
rect.y = int(height - (event.y / scale))
rect.width = 1
rect.height = 1
popover.set_pointing_to(rect)
popover.popup()


@_BackendGTK.export
class _BackendGTK4(_BackendGTK):
Expand Down
25 changes: 25 additions & 0 deletions lib/matplotlib/backends/backend_macosx.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
from matplotlib.backend_bases import (
_Backend, FigureCanvasBase, FigureManagerBase, NavigationToolbar2,
ResizeEvent, TimerBase, _allow_interrupt)
from Foundation import NSObject
import AppKit


class TimerMac(_macosx.Timer, TimerBase):
Expand Down Expand Up @@ -145,6 +147,12 @@ def save_figure(self, *args):
return filename


class MenuCallback(NSObject):
def action_(self, sender):
if hasattr(self, 'callback'):
self.callback()


class FigureManagerMac(_macosx.FigureManager, FigureManagerBase):
_toolbar2_class = NavigationToolbar2Mac

Expand All @@ -161,6 +169,23 @@ def __init__(self, canvas, num):
self.show()
self.canvas.draw_idle()

def context_menu(self, event, labels=None, actions=None):
if labels is None or actions is None:
return
menu = AppKit.NSMenu.alloc().init()
self._menu_callbacks = []
for label, action in zip(labels, actions):
target = MenuCallback.alloc().init()
target.callback = action
self._menu_callbacks.append(target)
item = AppKit.NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(
label, "action:", ""
)
item.setTarget_(target)
menu.addItem_(item)
mouse_loc = AppKit.NSEvent.mouseLocation()
menu.popUpMenuPositioningItem_atLocation_inView_(None, mouse_loc, None)
Comment on lines +186 to +187
Copy link
Owner

Choose a reason for hiding this comment

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

For other backends, the position is extracted from event or event.guiEvent. So try to follow that here as well, instead of using the current mouse location. Check this for other backends as well, for instance Qt and Wx


def _close_button_pressed(self):
Gcf.destroy(self)
self.canvas.flush_events()
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 @@ -627,6 +627,14 @@ def full_screen_toggle(self):
else:
self.window.showFullScreen()

def context_menu(self, event, labels=None, actions=None):
if labels is None or actions is None:
return
menu = QtWidgets.QMenu(self.window)
for label, action in zip(labels, actions):
menu.addAction(label).triggered.connect(action)
menu.exec(event.guiEvent.globalPosition().toPoint())

def _widgetclosed(self):
CloseEvent("close_event", self.canvas)._process()
if self.window._destroying:
Expand Down
10 changes: 10 additions & 0 deletions lib/matplotlib/backends/backend_wx.py
Original file line number Diff line number Diff line change
Expand Up @@ -1018,6 +1018,16 @@ def full_screen_toggle(self):
# docstring inherited
self.frame.ShowFullScreen(not self.frame.IsFullScreen())

def context_menu(self, event, labels=None, actions=None):
if labels is None or actions is None:
return
menu = wx.Menu()
for label, action in zip(labels, actions):
item = menu.Append(wx.NewIdRef(), label)
menu.Bind(wx.EVT_MENU, lambda _, a=action: a(), item)
self.canvas.PopupMenu(menu, event.guiEvent.GetPosition())
menu.Destroy()

def get_window_title(self):
# docstring inherited
return self.window.GetTitle()
Expand Down
24 changes: 24 additions & 0 deletions lib/mpl_toolkits/mplot3d/axes3d.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import textwrap
import warnings

import functools
import numpy as np

import matplotlib as mpl
Expand Down Expand Up @@ -187,6 +188,8 @@ def __init__(
pseudo_bbox = self.transLimits.inverted().transform([(0, 0), (1, 1)])
self._pseudo_w, self._pseudo_h = pseudo_bbox[1] - pseudo_bbox[0]

self._mouse_moved = False

# mplot3d currently manages its own spines and needs these turned off
# for bounding box calculations
self.spines[:].set_visible(False)
Expand Down Expand Up @@ -1357,6 +1360,7 @@ def clear(self):

def _button_press(self, event):
if event.inaxes == self:
self._mouse_moved = False
self.button_pressed = event.button
self._sx, self._sy = event.xdata, event.ydata
toolbar = self.get_figure(root=True).canvas.toolbar
Expand All @@ -1367,6 +1371,23 @@ def _button_press(self, event):

def _button_release(self, event):
self.button_pressed = None

if event.button in self._zoom_btn and event.inaxes == self \
and not self._mouse_moved:
canvas = self.get_figure(root=True).canvas

def draw_lambda(elev, azim):
self.view_init(elev=elev, azim=azim)
canvas.draw_idle()

canvas.manager.context_menu(
event,
labels=["XY", "YZ", "XZ"],
actions=[functools.partial(draw_lambda, elev=90, azim=-90),
functools.partial(draw_lambda, elev=0, azim=0),
functools.partial(draw_lambda, elev=0, azim=-90)],
)

toolbar = self.get_figure(root=True).canvas.toolbar
# backend_bases.release_zoom and backend_bases.release_pan call
# push_current, so check the navigation mode so we don't call it twice
Expand Down Expand Up @@ -1563,6 +1584,9 @@ def _on_move(self, event):
w = self._pseudo_w
h = self._pseudo_h

if (dx**2 + dy**2) > 1e-6:
self._mouse_moved = True

Copy link
Owner

Choose a reason for hiding this comment

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

Isn't this threshold a bit too high?

Copy link
Owner

@r3kste r3kste Jan 5, 2026

Choose a reason for hiding this comment

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

This threshold is now not too high, but is still noticeable. If I move the cursor slightly, the plot is zoomed as well as the context menu is shown.

# Rotation
if self.button_pressed in self._rotate_btn:
# rotate viewing point
Expand Down