Skip to content
Closed
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
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 not labels or not actions:
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)


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

def context_menu(self, event, labels=None, actions=None):
if not labels or not actions:
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.connect('selection-done', lambda m: m.destroy())
menu.popup_at_pointer(event.guiEvent)


@_BackendGTK.export
class _BackendGTK3(_BackendGTK):
Expand Down
28 changes: 28 additions & 0 deletions lib/matplotlib/backends/backend_gtk4.py
Original file line number Diff line number Diff line change
Expand Up @@ -637,6 +637,34 @@ 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 label, action in zip(labels, actions):
action_name = label.replace(" ", "_")
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
11 changes: 11 additions & 0 deletions lib/matplotlib/backends/backend_qt.py
Original file line number Diff line number Diff line change
Expand Up @@ -627,6 +627,17 @@ def full_screen_toggle(self):
else:
self.window.showFullScreen()

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

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 not labels or not actions:
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
35 changes: 30 additions & 5 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,24 @@ 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()

if hasattr(canvas.manager, "context_menu"):
canvas.manager.context_menu(
event,
labels=["Go to X-Y view", "Go to Y-Z view", "Go to X-Z view"],
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 @@ -1546,11 +1568,6 @@ def _on_move(self, event):
if not self.button_pressed:
return

if self.get_navigate_mode() is not None:
# we don't want to rotate if we are zooming/panning
# from the toolbar
return

if self.M is None:
return

Expand All @@ -1563,6 +1580,14 @@ def _on_move(self, event):
w = self._pseudo_w
h = self._pseudo_h

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

if self.get_navigate_mode() is not None:
# we don't want to rotate if we are zooming/panning
# from the toolbar
return

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