From a8108accd7ff047267fb6e556db98d01c990d82b Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Fri, 17 Dec 2021 12:14:56 +0100 Subject: [PATCH] Unify toolbar init across backends. ... by instantiating classes specified by _toolbar2_class or _toolmanager_toolbar_class. (Third-parties can still manually instantiate their toolbars the old-fashioned way.) Note that this would read a bit nicer if the toolbar classes were defined before the manager classes, as this would allow _toolbar2_class and _toolmanager_toolbar_class to be set using normal class attributes, but moving large chunks of code around did not seem worth it. I only did so for the macosx backend, where the moved code is very little. This means that all toolbars default to having the standard tools added, not just the interactive ones. See e.g. changes in test_toolbar_zoompan: it is no longer necessary to explicitly register all the tools (this actually now emits a duplicate tool warning). This also moves the canonical storage of the toolbar on wx from the frame to the manager (keeping a proxy for backcompat), for consistency with the other backends. --- lib/matplotlib/backend_bases.py | 17 ++++++- lib/matplotlib/backends/_backend_tk.py | 17 +------ lib/matplotlib/backends/backend_gtk3.py | 9 +--- lib/matplotlib/backends/backend_gtk4.py | 20 +------- lib/matplotlib/backends/backend_macosx.py | 42 +++++++---------- lib/matplotlib/backends/backend_nbagg.py | 2 +- lib/matplotlib/backends/backend_qt.py | 22 ++------- .../backends/backend_webagg_core.py | 7 +-- lib/matplotlib/backends/backend_wx.py | 47 +++++-------------- lib/matplotlib/tests/test_backend_bases.py | 10 ---- 10 files changed, 58 insertions(+), 135 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 173558b534ac..a666f131475b 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -2717,6 +2717,9 @@ class FigureManagerBase: figure.canvas.manager.button_press_handler_id) """ + _toolbar2_class = None + _toolmanager_toolbar_class = None + def __init__(self, canvas, num): self.canvas = canvas canvas.manager = self # store a pointer to parent @@ -2734,7 +2737,19 @@ def __init__(self, canvas, num): self.toolmanager = (ToolManager(canvas.figure) if mpl.rcParams['toolbar'] == 'toolmanager' else None) - self.toolbar = None + if (mpl.rcParams["toolbar"] == "toolbar2" + and self._toolbar2_class): + self.toolbar = self._toolbar2_class(self.canvas) + elif (mpl.rcParams["toolbar"] == "toolmanager" + and self._toolmanager_toolbar_class): + self.toolbar = self._toolmanager_toolbar_class(self.toolmanager) + else: + self.toolbar = None + + if self.toolmanager: + tools.add_tools_to_manager(self.toolmanager) + if self.toolbar: + tools.add_tools_to_container(self.toolbar) @self.canvas.figure.add_axobserver def notify_axes_change(fig): diff --git a/lib/matplotlib/backends/_backend_tk.py b/lib/matplotlib/backends/_backend_tk.py index cf2083e33534..0e5f2e8be7c2 100644 --- a/lib/matplotlib/backends/_backend_tk.py +++ b/lib/matplotlib/backends/_backend_tk.py @@ -410,14 +410,8 @@ def __init__(self, canvas, num, window): self.window.withdraw() # packing toolbar first, because if space is getting low, last packed # widget is getting shrunk first (-> the canvas) - self.toolbar = self._get_toolbar() self.canvas._tkcanvas.pack(side=tk.TOP, fill=tk.BOTH, expand=1) - if self.toolmanager: - backend_tools.add_tools_to_manager(self.toolmanager) - if self.toolbar: - backend_tools.add_tools_to_container(self.toolbar) - # If the window has per-monitor DPI awareness, then setup a Tk variable # to store the DPI, which will be updated by the C code, and the trace # will handle it on the Python side. @@ -430,15 +424,6 @@ def __init__(self, canvas, num, window): self._shown = False - def _get_toolbar(self): - if mpl.rcParams['toolbar'] == 'toolbar2': - toolbar = NavigationToolbar2Tk(self.canvas) - elif mpl.rcParams['toolbar'] == 'toolmanager': - toolbar = ToolbarTk(self.toolmanager) - else: - toolbar = None - return toolbar - def _update_window_dpi(self, *args): newdpi = self._window_dpi.get() self.window.call('tk', 'scaling', newdpi / 72) @@ -898,6 +883,8 @@ def trigger(self, *args): Toolbar = ToolbarTk +FigureManagerTk._toolbar2_class = NavigationToolbar2Tk +FigureManagerTk._toolmanager_toolbar_class = ToolbarTk @_Backend.export diff --git a/lib/matplotlib/backends/backend_gtk3.py b/lib/matplotlib/backends/backend_gtk3.py index 6f48fcf633ba..60073a2d8b1e 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -331,13 +331,6 @@ def __init__(self, canvas, num): # calculate size for window w, h = self.canvas.get_width_height() - self.toolbar = self._get_toolbar() - - if self.toolmanager: - backend_tools.add_tools_to_manager(self.toolmanager) - if self.toolbar: - backend_tools.add_tools_to_container(self.toolbar) - if self.toolbar is not None: self.toolbar.show() self.vbox.pack_end(self.toolbar, False, False, 0) @@ -737,6 +730,8 @@ def error_msg_gtk(msg, parent=None): FigureCanvasGTK3, _backend_gtk.ConfigureSubplotsGTK) backend_tools._register_tool_class( FigureCanvasGTK3, _backend_gtk.RubberbandGTK) +FigureManagerGTK3._toolbar2_class = NavigationToolbar2GTK3 +FigureManagerGTK3._toolmanager_toolbar_class = ToolbarGTK3 @_BackendGTK.export diff --git a/lib/matplotlib/backends/backend_gtk4.py b/lib/matplotlib/backends/backend_gtk4.py index 1c5f4d16f0a5..aeb4a7a0dffa 100644 --- a/lib/matplotlib/backends/backend_gtk4.py +++ b/lib/matplotlib/backends/backend_gtk4.py @@ -279,13 +279,6 @@ def __init__(self, canvas, num): # calculate size for window w, h = self.canvas.get_width_height() - self.toolbar = self._get_toolbar() - - if self.toolmanager: - backend_tools.add_tools_to_manager(self.toolmanager) - if self.toolbar: - backend_tools.add_tools_to_container(self.toolbar) - if self.toolbar is not None: sw = Gtk.ScrolledWindow(vscrollbar_policy=Gtk.PolicyType.NEVER) sw.set_child(self.toolbar) @@ -335,17 +328,6 @@ def full_screen_toggle(self): else: self.window.unfullscreen() - def _get_toolbar(self): - # must be inited after the window, drawingArea and figure - # attrs are set - if mpl.rcParams['toolbar'] == 'toolbar2': - toolbar = NavigationToolbar2GTK4(self.canvas) - elif mpl.rcParams['toolbar'] == 'toolmanager': - toolbar = ToolbarGTK4(self.toolmanager) - else: - toolbar = None - return toolbar - def get_window_title(self): return self.window.get_title() @@ -673,6 +655,8 @@ def trigger(self, *args, **kwargs): backend_tools._register_tool_class( FigureCanvasGTK4, _backend_gtk.RubberbandGTK) Toolbar = ToolbarGTK4 +FigureManagerGTK4._toolbar2_class = NavigationToolbar2GTK4 +FigureManagerGTK4._toolmanager_toolbar_class = ToolbarGTK4 @_BackendGTK.export diff --git a/lib/matplotlib/backends/backend_macosx.py b/lib/matplotlib/backends/backend_macosx.py index 69ef4dae3737..7f8185284fca 100644 --- a/lib/matplotlib/backends/backend_macosx.py +++ b/lib/matplotlib/backends/backend_macosx.py @@ -62,30 +62,6 @@ def resize(self, width, height): self.draw_idle() -class FigureManagerMac(_macosx.FigureManager, FigureManagerBase): - """ - Wrap everything up into a window for the pylab interface - """ - def __init__(self, canvas, num): - _macosx.FigureManager.__init__(self, canvas) - icon_path = str(cbook._get_data_path('images/matplotlib.pdf')) - _macosx.FigureManager.set_icon(icon_path) - FigureManagerBase.__init__(self, canvas, num) - if mpl.rcParams['toolbar'] == 'toolbar2': - self.toolbar = NavigationToolbar2Mac(canvas) - else: - self.toolbar = None - if self.toolbar is not None: - self.toolbar.update() - - if mpl.is_interactive(): - self.show() - self.canvas.draw_idle() - - def close(self): - Gcf.destroy(self) - - class NavigationToolbar2Mac(_macosx.NavigationToolbar2, NavigationToolbar2): def __init__(self, canvas): @@ -123,6 +99,24 @@ def set_message(self, message): _macosx.NavigationToolbar2.set_message(self, message.encode('utf-8')) +class FigureManagerMac(_macosx.FigureManager, FigureManagerBase): + _toolbar2_class = NavigationToolbar2Mac + + def __init__(self, canvas, num): + _macosx.FigureManager.__init__(self, canvas) + icon_path = str(cbook._get_data_path('images/matplotlib.pdf')) + _macosx.FigureManager.set_icon(icon_path) + FigureManagerBase.__init__(self, canvas, num) + if self.toolbar is not None: + self.toolbar.update() + if mpl.is_interactive(): + self.show() + self.canvas.draw_idle() + + def close(self): + Gcf.destroy(self) + + @_Backend.export class _BackendMac(_Backend): FigureCanvas = FigureCanvasMac diff --git a/lib/matplotlib/backends/backend_nbagg.py b/lib/matplotlib/backends/backend_nbagg.py index c9df2e8dbad7..abd2206e27d2 100644 --- a/lib/matplotlib/backends/backend_nbagg.py +++ b/lib/matplotlib/backends/backend_nbagg.py @@ -72,7 +72,7 @@ class NavigationIPy(NavigationToolbar2WebAgg): class FigureManagerNbAgg(FigureManagerWebAgg): - ToolbarCls = NavigationIPy + _toolbar2_class = ToolbarCls = NavigationIPy def __init__(self, canvas, num): self._shown = False diff --git a/lib/matplotlib/backends/backend_qt.py b/lib/matplotlib/backends/backend_qt.py index a8a12c76c80a..66b6698affbf 100644 --- a/lib/matplotlib/backends/backend_qt.py +++ b/lib/matplotlib/backends/backend_qt.py @@ -529,13 +529,6 @@ def __init__(self, canvas, num): self.window._destroying = False - self.toolbar = self._get_toolbar(self.canvas, self.window) - - if self.toolmanager: - backend_tools.add_tools_to_manager(self.toolmanager) - if self.toolbar: - backend_tools.add_tools_to_container(self.toolbar) - if self.toolbar: self.window.addToolBar(self.toolbar) tbs_height = self.toolbar.sizeHint().height() @@ -582,17 +575,6 @@ def _widgetclosed(self): # Gcf can get destroyed before the Gcf.destroy # line is run, leading to a useless AttributeError. - def _get_toolbar(self, canvas, parent): - # must be inited after the window, drawingArea and figure - # attrs are set - if mpl.rcParams['toolbar'] == 'toolbar2': - toolbar = NavigationToolbar2QT(canvas) - elif mpl.rcParams['toolbar'] == 'toolmanager': - toolbar = ToolbarQt(self.toolmanager) - else: - toolbar = None - return toolbar - def resize(self, width, height): # The Qt methods return sizes in 'virtual' pixels so we do need to # rescale from physical to logical pixels. @@ -1021,6 +1003,10 @@ def trigger(self, *args, **kwargs): qApp.clipboard().setPixmap(pixmap) +FigureManagerQT._toolbar2_class = NavigationToolbar2QT +FigureManagerQT._toolmanager_toolbar_class = ToolbarQt + + @_Backend.export class _BackendQT(_Backend): FigureCanvas = FigureCanvasQT diff --git a/lib/matplotlib/backends/backend_webagg_core.py b/lib/matplotlib/backends/backend_webagg_core.py index 8e1997887912..8b5f1ba479d0 100644 --- a/lib/matplotlib/backends/backend_webagg_core.py +++ b/lib/matplotlib/backends/backend_webagg_core.py @@ -453,20 +453,15 @@ def set_history_buttons(self): class FigureManagerWebAgg(backend_bases.FigureManagerBase): - ToolbarCls = NavigationToolbar2WebAgg + _toolbar2_class = ToolbarCls = NavigationToolbar2WebAgg def __init__(self, canvas, num): self.web_sockets = set() super().__init__(canvas, num) - self.toolbar = self._get_toolbar(canvas) def show(self): pass - def _get_toolbar(self, canvas): - toolbar = self.ToolbarCls(canvas) - return toolbar - def resize(self, w, h, forward=True): self._send_event( 'resize', diff --git a/lib/matplotlib/backends/backend_wx.py b/lib/matplotlib/backends/backend_wx.py index 7153887c80fa..b37af1809b05 100644 --- a/lib/matplotlib/backends/backend_wx.py +++ b/lib/matplotlib/backends/backend_wx.py @@ -901,13 +901,9 @@ def __init__(self, num, fig, *, canvas_class=None): self.figmgr = FigureManagerWx(self.canvas, num, self) - self.toolbar = self._get_toolbar() - if self.figmgr.toolmanager: - backend_tools.add_tools_to_manager(self.figmgr.toolmanager) - if self.toolbar: - backend_tools.add_tools_to_container(self.toolbar) - if self.toolbar is not None: - self.SetToolBar(self.toolbar) + toolbar = self.canvas.manager.toolbar + if toolbar is not None: + self.SetToolBar(toolbar) # On Windows, canvas sizing must occur after toolbar addition; # otherwise the toolbar further resizes the canvas. @@ -920,18 +916,8 @@ def __init__(self, num, fig, *, canvas_class=None): self.Bind(wx.EVT_CLOSE, self._on_close) - @property - def toolmanager(self): - return self.figmgr.toolmanager - - def _get_toolbar(self): - if mpl.rcParams['toolbar'] == 'toolbar2': - toolbar = NavigationToolbar2Wx(self.canvas) - elif mpl.rcParams['toolbar'] == 'toolmanager': - toolbar = ToolbarWx(self.toolmanager) - else: - toolbar = None - return toolbar + toolmanager = property(lambda self: self.figmgr.toolmanager) + toolbar = property(lambda self: self.GetToolBar()) # backcompat @_api.deprecated( "3.6", alternative="the canvas_class constructor parameter") @@ -956,7 +942,7 @@ def _on_close(self, event): def Destroy(self, *args, **kwargs): try: - self.canvas.mpl_disconnect(self.toolbar._id_drag) + self.canvas.mpl_disconnect(self.canvas.manager.toolbar._id_drag) # Rationale for line above: see issue 2941338. except AttributeError: pass # classic toolbar lacks the attribute @@ -965,8 +951,8 @@ def Destroy(self, *args, **kwargs): # MPLBACKEND=wxagg python -c 'from pylab import *; plot()'. if self and not self.IsBeingDeleted(): super().Destroy(*args, **kwargs) - # self.toolbar.Destroy() should not be necessary if the close event - # is allowed to propagate. + # toolbar.Destroy() should not be necessary if the close event is + # allowed to propagate. return True @@ -988,20 +974,7 @@ class FigureManagerWx(FigureManagerBase): def __init__(self, canvas, num, frame): _log.debug("%s - __init__()", type(self)) self.frame = self.window = frame - self._initializing = True super().__init__(canvas, num) - self._initializing = False - - @property - def toolbar(self): - return self.frame.GetToolBar() - - @toolbar.setter - def toolbar(self, value): - # Never allow this, except that base class inits this to None before - # the frame is set up. - if not self._initializing: - raise AttributeError("can't set attribute") def show(self): # docstring inherited @@ -1373,6 +1346,10 @@ def trigger(self, *args, **kwargs): wx.TheClipboard.Close() +FigureManagerWx._toolbar2_class = NavigationToolbar2Wx +FigureManagerWx._toolmanager_toolbar_class = ToolbarWx + + @_Backend.export class _BackendWx(_Backend): FigureCanvas = FigureCanvasWx diff --git a/lib/matplotlib/tests/test_backend_bases.py b/lib/matplotlib/tests/test_backend_bases.py index 4abaf1a78ed5..78833a8a7e29 100644 --- a/lib/matplotlib/tests/test_backend_bases.py +++ b/lib/matplotlib/tests/test_backend_bases.py @@ -4,8 +4,6 @@ from matplotlib.backend_bases import ( FigureCanvasBase, LocationEvent, MouseButton, MouseEvent, NavigationToolbar2, RendererBase) -from matplotlib.backend_tools import (ToolZoom, ToolPan, RubberbandBase, - ToolViewsPositions, _views_positions) from matplotlib.figure import Figure import matplotlib.pyplot as plt import matplotlib.transforms as transforms @@ -250,14 +248,6 @@ def test_toolbar_zoompan(): plt.rcParams['toolbar'] = 'toolmanager' ax = plt.gca() assert ax.get_navigate_mode() is None - ax.figure.canvas.manager.toolmanager.add_tool(name="zoom", - tool=ToolZoom) - ax.figure.canvas.manager.toolmanager.add_tool(name="pan", - tool=ToolPan) - ax.figure.canvas.manager.toolmanager.add_tool(name=_views_positions, - tool=ToolViewsPositions) - ax.figure.canvas.manager.toolmanager.add_tool(name='rubberband', - tool=RubberbandBase) ax.figure.canvas.manager.toolmanager.trigger_tool('zoom') assert ax.get_navigate_mode() == "ZOOM" ax.figure.canvas.manager.toolmanager.trigger_tool('pan')