Context Menu for snapping view to primary axis planes in 3D plots#30976
Context Menu for snapping view to primary axis planes in 3D plots#30976AdwaithBatchu wants to merge 3 commits intomatplotlib:mainfrom
Conversation
0ae13f4 to
5417d95
Compare
There was a problem hiding this comment.
This is a lot more cleanly implemented than I thought it would require!
I can see context menus being useful in quite a few use cases, we should talk on the dev team about designing these with some thought to it. It may also be worth implementing a base class for this, but I think this is fine for now as a starting point.
@timhoffm in the original issue mentioned compatibility with our event architecture, which I don't know enough to comment on.
I played around with this locally on a qtagg backend, and things work largely as expected:
- The menu works and each option correctly snaps to the expected view
- Long presses on a touchscreen work the same as a mouse right-click to bring up the menu
- With multiple subplots, the action is only applied to the subplot that was clicked on
- Right clicking in a 2D subplot has no effect
- Rotating, panning, and zooming still all work as expected with either the mouse or toolbar buttons
One minor issue that would be nice to fix but isn't blocking, is that if you bring up the menu and then drag the figure window to a new position, the popup does not follow the window.
For documentation, this behavior should get a doc/release/next_whats_new entry, as well as explanation with a screenshot in https://matplotlib.org/stable/users/explain/figure/interactive.html
We should also add tests for this. Ideally for selecting each option, but at the least for opening the menu on each backend.
lib/mpl_toolkits/mplot3d/axes3d.py
Outdated
| self.view_init(elev=elev, azim=azim) | ||
| canvas.draw_idle() | ||
|
|
||
| canvas.manager.context_menu( |
There was a problem hiding this comment.
This should check for the existence of the context_menu for the backend to avoid errors:
if hasattr(canvas.manager, 'context_menu'):
canvas.manager.context_menu(event, labels=..., actions=...)| rect.width = 1 | ||
| rect.height = 1 | ||
| popover.set_pointing_to(rect) | ||
| popover.popup() |
There was a problem hiding this comment.
My GTK is rusty, but I believe this should be marked for cleanup when closed:
...
popover.connect('closed', lambda p: p.unparent())
popover.popup()| menu.append(item) | ||
| item.connect('activate', lambda _, a=action: a()) | ||
| item.show() | ||
| menu.popup_at_pointer(event.guiEvent) |
There was a problem hiding this comment.
Similarly for gtk3:
...
menu.connect('selection-done', lambda m: m.destroy())
menu.popup_at_pointer(event.guiEvent)|
If matplotlib itself starts to use context menus, then the design needs to be careful to allow developers that embed matplotlib widgets into their own GUIs to add their own context actions (something that I do fairly regularly, and which I would have to redesign if it started to conflict with builtin menus). There's likely many ways to design the API, but based on my own usage it could e.g. look like canvas.add_context_menu_entry("group", "name", callback, condition=lambda e: True)where "name" is what gets displayed in the menu, "group" allows grouping entries (with menu separators? or with submenus?), callback is the callback (taking the button_press_event as parameter) and condition is a function taking the button_press_event as argument and returning whether this entry should be displayed (e.g., for the use case here it would allow displaying the menu entries only when over a 3D axes). |
|
@anntzer how do you feel about getting that API fleshed out in this PR versus marking everything here as a private interface that we can revisit? |
|
Even if the implementation here is all hidden behind private APIs, behaviorally this will still break third-parties that rely on their own context menus. If anything I think I would prefer (wearing my hat of such a third-party) that Matplotlib provides a documented API for this purpose and explicitly marks it as provisional, so that I can at least provide keep adding my own context menu entries by version-gating on Matplotlib's version. |
|
That sounds reasonable to me. @AdwaithBatchu you have a start on the backends - is an API something that you'd be interested in sketching out? I think it's a bit of a different task that will require some thought and care. |
|
Aren't we going one step to far here with adding a full framework for defining context menus? That sounds very much like a second version of toolmanager, and we haven't even managed to migrate to that. Therefore I'm very hesitant on us providing a way for third parties "adding my own context menu entries". I believe a much smaller step is sufficient: Third parties may very well provide their own context menus, but we don't mix between them and us.
|
|
Sure, providing a way to disable the builtin menu seems reasonable too. |
|
Hi @AdwaithBatchu, we talked about this in the weekly dev call today and decided that a context menu is not the right way to solve the original problem (rationale here: #23544 (comment)). Thank you for the work on this and apologies for the change in direction after you had already spent time on it! I'm going to close this PR as not-planned, but please feel free to give the toolbar menu approach a shot if you'd like. |
|
Ok I mentioned this over in the issue but to reiterate here, we agree with your point about choosing which subplot to interact with, and that the context menu is the way to go. The only update is that we should not overwrite any existing menus that a user has already defined. @anntzer do you have an example of what that looks like in one of your implementations that we can test against? |
c1d2762 to
69da89d
Compare
|
Sure, I'll pull out a minimal example from my programs. |
|
This is a minimal example extracted from my own packages, which allows triggering the qt figureoptions menu for the single axes under the mouse, rather than having to go through the axes selection dropdown menu, which can be unwieldy when there are dozens of subpanels on a figure: import functools
from matplotlib import pyplot as plt
def register_context_action(fig, gen_callbacks=None):
"""
Register actions for a right-click menu on a Figure.
On a right-click, *gen_callbacks* is called with the mouse event as single argument;
it should return a mapping of menu entry names to menu action callables. The menu
action callables also take the mouse event as single argument.
Parameters
----------
fig : Figure instance
gen_callbacks : Callable[[MouseEvent], dict[str, Callable[[MouseEvent], ...]]]
"""
if gen_callbacks is None: # Allow use as decorator.
return functools.partial(register_context_action, fig)
def on_button_press(event):
tb = fig.canvas.toolbar
if tb and str(tb.mode) or event.button.name != "RIGHT":
return
callbacks = gen_callbacks(event)
if not callbacks:
return
gui_event = event.guiEvent
pkg = type(gui_event).__module__.split(".")[0]
if pkg.startswith(("PyQt", "PySide")):
from matplotlib.backends.qt_compat import QtWidgets
menu = QtWidgets.QMenu()
for name, func in callbacks.items():
menu.addAction(name, functools.partial(func, event))
menu.exec(
event.guiEvent.globalPosition().toPoint() # Qt6.
if hasattr(event.guiEvent, "globalPosition")
else event.guiEvent.globalPos()) # Qt5.
elif ...: # Other backends (elided here, as the example below is qt only).
pass
fig.canvas.mpl_connect("button_press_event", on_button_press)
def context_axes_edit(fig):
# Allow triggering the Qt menu option for a single axes.
from matplotlib.backends.backend_qt import figureoptions
register_context_action(fig, lambda e: (
{"Edit Axes": lambda e: figureoptions.figure_edit(e.inaxes, e.canvas.toolbar)}
if e.inaxes else {}))
if __name__ == "__main__":
fig, axs = plt.subplots(2, 2)
axs[0, 0].plot([0, 1])
context_axes_edit(fig)
plt.show()In fact, I guess it would be nice to have something like that in matplotlib itself, if we decide to start providing builtin context menus... Another fairly general action I have is one that allows exporting the data artists of an axes to a npz file (essentially something that iterates over the axes children and switches over the child type, exporting line.get_data() for Line2D, image.get_array() for AxesImage, etc.) I also have a few actions that are much more domain-specific, which I essentially put in the context menu because I already have some machinery available to do so, rather than e.g. making a real menu bar. |
PR summary
closes #23544
This PR introduces a feature that adds context menu on 3d Axes triggered by right-click of the mouse.
Added
context_menu()in Figure Manager that takes arguments, a list of labels and a list of corresponding functions to execute upon selection.Modified
_button_release()to callcanvas.manager.context_menu()with functions for setting orthographic views when the right-click is released on the mouse without moving it significantly. Mouse movement is handled in_on_move()using a small threshold because previously I observed trackpad (on macOS) reported micro-movements during a static click, which falsely flagged the action as "drag" and blocked the menu in specific backends.Backends
tk.Menuimplementation.QtWidgets.QMenuimplementation.wx.MenuandPopupMenu.Gtk.Menu.Gtk.PopoverMenuandGio.Menu.PR checklist