diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 0e7ee91a0a8c..2a75de525592 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -1580,6 +1580,13 @@ class FigureCanvasBase: # interactive framework is required, or None otherwise. required_interactive_framework = None + # The manager class instantiated by new_manager. + # (This is defined as a classproperty because the manager class is + # currently defined *after* the canvas class, but one could also assign + # ``FigureCanvasBase.manager_class = FigureManagerBase`` + # after defining both classes.) + manager_class = _api.classproperty(lambda cls: FigureManagerBase) + events = [ 'resize_event', 'draw_event', @@ -1662,6 +1669,19 @@ def _fix_ipython_backend2gui(cls): if _is_non_interactive_terminal_ipython(ip): ip.enable_gui(backend2gui_rif) + @classmethod + def new_manager(cls, figure, num): + """ + Create a new figure manager for *figure*, using this canvas class. + + Notes + ----- + This method should not be reimplemented in subclasses. If + custom manager creation logic is needed, please reimplement + ``FigureManager.create_with_canvas``. + """ + return cls.manager_class.create_with_canvas(cls, figure, num) + @contextmanager def _idle_draw_cntx(self): self._is_idle_drawing = True @@ -2759,6 +2779,16 @@ def notify_axes_change(fig): if self.toolmanager is None and self.toolbar is not None: self.toolbar.update() + @classmethod + def create_with_canvas(cls, canvas_class, figure, num): + """ + Create a manager for a given *figure* using a specific *canvas_class*. + + Backends should override this method if they have specific needs for + setting up the canvas or the manager. + """ + return cls(canvas_class(figure), num) + def show(self): """ For GUI backends, show the figure window and redraw. @@ -3225,11 +3255,10 @@ def configure_subplots(self, *args): if hasattr(self, "subplot_tool"): self.subplot_tool.figure.canvas.manager.show() return - plt = _safe_pyplot_import() + # This import needs to happen here due to circular imports. + from matplotlib.figure import Figure with mpl.rc_context({"toolbar": "none"}): # No navbar for the toolfig. - # Use new_figure_manager() instead of figure() so that the figure - # doesn't get registered with pyplot. - manager = plt.new_figure_manager(-1, (6, 3)) + manager = type(self.canvas).new_manager(Figure(figsize=(6, 3)), -1) manager.set_window_title("Subplot configuration tool") tool_fig = manager.canvas.figure tool_fig.subplots_adjust(top=0.9) @@ -3457,9 +3486,7 @@ def new_figure_manager(cls, num, *args, **kwargs): @classmethod def new_figure_manager_given_figure(cls, num, figure): """Create a new figure manager instance for the given figure.""" - canvas = cls.FigureCanvas(figure) - manager = cls.FigureManager(canvas, num) - return manager + return cls.FigureCanvas.new_manager(figure, num) @classmethod def draw_if_interactive(cls): diff --git a/lib/matplotlib/backends/_backend_tk.py b/lib/matplotlib/backends/_backend_tk.py index c759fe4840d6..713081155613 100644 --- a/lib/matplotlib/backends/_backend_tk.py +++ b/lib/matplotlib/backends/_backend_tk.py @@ -162,6 +162,7 @@ def _on_timer(self): class FigureCanvasTk(FigureCanvasBase): required_interactive_framework = "tk" + manager_class = _api.classproperty(lambda cls: FigureManagerTk) def __init__(self, figure=None, master=None): super().__init__(figure) @@ -431,6 +432,43 @@ def __init__(self, canvas, num, window): self._shown = False + @classmethod + def create_with_canvas(cls, canvas_class, figure, num): + # docstring inherited + with _restore_foreground_window_at_end(): + if cbook._get_running_interactive_framework() is None: + cbook._setup_new_guiapp() + _c_internal_utils.Win32_SetProcessDpiAwareness_max() + window = tk.Tk(className="matplotlib") + window.withdraw() + + # Put a Matplotlib icon on the window rather than the default tk + # icon. See https://www.tcl.tk/man/tcl/TkCmd/wm.html#M50 + # + # `ImageTk` can be replaced with `tk` whenever the minimum + # supported Tk version is increased to 8.6, as Tk 8.6+ natively + # supports PNG images. + icon_fname = str(cbook._get_data_path( + 'images/matplotlib.png')) + icon_img = ImageTk.PhotoImage(file=icon_fname, master=window) + + icon_fname_large = str(cbook._get_data_path( + 'images/matplotlib_large.png')) + icon_img_large = ImageTk.PhotoImage( + file=icon_fname_large, master=window) + try: + window.iconphoto(False, icon_img_large, icon_img) + except Exception as exc: + # log the failure (due e.g. to Tk version), but carry on + _log.info('Could not load matplotlib icon: %s', exc) + + canvas = canvas_class(figure, master=window) + manager = cls(canvas, num, window) + if mpl.is_interactive(): + manager.show() + canvas.draw_idle() + return manager + def _update_window_dpi(self, *args): newdpi = self._window_dpi.get() self.window.call('tk', 'scaling', newdpi / 72) @@ -958,45 +996,6 @@ def trigger(self, *args): class _BackendTk(_Backend): FigureManager = FigureManagerTk - @classmethod - def new_figure_manager_given_figure(cls, num, figure): - """ - Create a new figure manager instance for the given figure. - """ - with _restore_foreground_window_at_end(): - if cbook._get_running_interactive_framework() is None: - cbook._setup_new_guiapp() - _c_internal_utils.Win32_SetProcessDpiAwareness_max() - window = tk.Tk(className="matplotlib") - window.withdraw() - - # Put a Matplotlib icon on the window rather than the default tk - # icon. See https://www.tcl.tk/man/tcl/TkCmd/wm.html#M50 - # - # `ImageTk` can be replaced with `tk` whenever the minimum - # supported Tk version is increased to 8.6, as Tk 8.6+ natively - # supports PNG images. - icon_fname = str(cbook._get_data_path( - 'images/matplotlib.png')) - icon_img = ImageTk.PhotoImage(file=icon_fname, master=window) - - icon_fname_large = str(cbook._get_data_path( - 'images/matplotlib_large.png')) - icon_img_large = ImageTk.PhotoImage( - file=icon_fname_large, master=window) - try: - window.iconphoto(False, icon_img_large, icon_img) - except Exception as exc: - # log the failure (due e.g. to Tk version), but carry on - _log.info('Could not load matplotlib icon: %s', exc) - - canvas = cls.FigureCanvas(figure, master=window) - manager = cls.FigureManager(canvas, num, window) - if mpl.is_interactive(): - manager.show() - canvas.draw_idle() - return manager - @staticmethod def mainloop(): managers = Gcf.get_all_fig_managers() diff --git a/lib/matplotlib/backends/backend_gtk3.py b/lib/matplotlib/backends/backend_gtk3.py index 877fd800c821..ea32e9c3cba0 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -71,6 +71,7 @@ def _mpl_to_gtk_cursor(mpl_cursor): class FigureCanvasGTK3(Gtk.DrawingArea, FigureCanvasBase): required_interactive_framework = "gtk3" _timer_cls = TimerGTK3 + manager_class = _api.classproperty(lambda cls: FigureManagerGTK3) # Setting this as a static constant prevents # this resulting expression from leaking event_mask = (Gdk.EventMask.BUTTON_PRESS_MASK diff --git a/lib/matplotlib/backends/backend_gtk4.py b/lib/matplotlib/backends/backend_gtk4.py index 63663834ec00..4cf6c0c8090e 100644 --- a/lib/matplotlib/backends/backend_gtk4.py +++ b/lib/matplotlib/backends/backend_gtk4.py @@ -33,6 +33,7 @@ class FigureCanvasGTK4(Gtk.DrawingArea, FigureCanvasBase): required_interactive_framework = "gtk4" supports_blit = False _timer_cls = TimerGTK4 + manager_class = _api.classproperty(lambda cls: FigureManagerGTK4) _context_is_scaled = False def __init__(self, figure=None): diff --git a/lib/matplotlib/backends/backend_macosx.py b/lib/matplotlib/backends/backend_macosx.py index 4700e28e48cc..1995e5985008 100644 --- a/lib/matplotlib/backends/backend_macosx.py +++ b/lib/matplotlib/backends/backend_macosx.py @@ -1,5 +1,5 @@ import matplotlib as mpl -from matplotlib import cbook +from matplotlib import _api, cbook from matplotlib._pylab_helpers import Gcf from . import _macosx from .backend_agg import FigureCanvasAgg @@ -25,6 +25,7 @@ class FigureCanvasMac(_macosx.FigureCanvas, FigureCanvasAgg): required_interactive_framework = "macosx" _timer_cls = TimerMac + manager_class = _api.classproperty(lambda cls: FigureManagerMac) def __init__(self, figure): FigureCanvasBase.__init__(self, figure) diff --git a/lib/matplotlib/backends/backend_nbagg.py b/lib/matplotlib/backends/backend_nbagg.py index ec1430e60d1b..6c98f0fd9cf5 100644 --- a/lib/matplotlib/backends/backend_nbagg.py +++ b/lib/matplotlib/backends/backend_nbagg.py @@ -78,6 +78,21 @@ def __init__(self, canvas, num): self._shown = False super().__init__(canvas, num) + @classmethod + def create_with_canvas(cls, canvas_class, figure, num): + canvas = canvas_class(figure) + manager = cls(canvas, num) + if is_interactive(): + manager.show() + canvas.draw_idle() + + def destroy(event): + canvas.mpl_disconnect(cid) + Gcf.destroy(manager) + + cid = canvas.mpl_connect('close_event', destroy) + return manager + def display_js(self): # XXX How to do this just once? It has to deal with multiple # browser instances using the same kernel (require.js - but the @@ -143,7 +158,7 @@ def remove_comm(self, comm_id): class FigureCanvasNbAgg(FigureCanvasWebAggCore): - pass + manager_class = FigureManagerNbAgg class CommSocket: @@ -228,21 +243,6 @@ class _BackendNbAgg(_Backend): FigureCanvas = FigureCanvasNbAgg FigureManager = FigureManagerNbAgg - @staticmethod - def new_figure_manager_given_figure(num, figure): - canvas = FigureCanvasNbAgg(figure) - manager = FigureManagerNbAgg(canvas, num) - if is_interactive(): - manager.show() - figure.canvas.draw_idle() - - def destroy(event): - canvas.mpl_disconnect(cid) - Gcf.destroy(manager) - - cid = canvas.mpl_connect('close_event', destroy) - return manager - @staticmethod def show(block=None): ## TODO: something to do when keyword block==False ? diff --git a/lib/matplotlib/backends/backend_qt.py b/lib/matplotlib/backends/backend_qt.py index 27945af8a342..2340cbf66c3d 100644 --- a/lib/matplotlib/backends/backend_qt.py +++ b/lib/matplotlib/backends/backend_qt.py @@ -232,6 +232,7 @@ def _timer_stop(self): class FigureCanvasQT(QtWidgets.QWidget, FigureCanvasBase): required_interactive_framework = "qt" _timer_cls = TimerQT + manager_class = _api.classproperty(lambda cls: FigureManagerQT) buttond = { getattr(_enum("QtCore.Qt.MouseButton"), k): v for k, v in [ diff --git a/lib/matplotlib/backends/backend_template.py b/lib/matplotlib/backends/backend_template.py index 9a25371465af..ebb9b413f5c0 100644 --- a/lib/matplotlib/backends/backend_template.py +++ b/lib/matplotlib/backends/backend_template.py @@ -174,6 +174,14 @@ def new_figure_manager_given_figure(num, figure): return manager +class FigureManagerTemplate(FigureManagerBase): + """ + Helper class for pyplot mode, wraps everything up into a neat bundle. + + For non-interactive backends, the base class is sufficient. + """ + + class FigureCanvasTemplate(FigureCanvasBase): """ The canvas the figure renders into. Calls the draw and print fig @@ -191,6 +199,8 @@ class methods button_press_event, button_release_event, A high-level Figure instance """ + manager_class = FigureManagerTemplate + def draw(self): """ Draw the figure using the renderer. @@ -227,14 +237,6 @@ def get_default_filetype(self): return 'foo' -class FigureManagerTemplate(FigureManagerBase): - """ - Helper class for pyplot mode, wraps everything up into a neat bundle. - - For non-interactive backends, the base class is sufficient. - """ - - ######################################################################## # # Now just provide the standard names that backend.__init__ is expecting diff --git a/lib/matplotlib/backends/backend_webagg.py b/lib/matplotlib/backends/backend_webagg.py index a78e86c25af8..9e1e4925496f 100644 --- a/lib/matplotlib/backends/backend_webagg.py +++ b/lib/matplotlib/backends/backend_webagg.py @@ -48,14 +48,14 @@ def run(self): webagg_server_thread = ServerThread() -class FigureCanvasWebAgg(core.FigureCanvasWebAggCore): - pass - - class FigureManagerWebAgg(core.FigureManagerWebAgg): _toolbar2_class = core.NavigationToolbar2WebAgg +class FigureCanvasWebAgg(core.FigureCanvasWebAggCore): + manager_class = FigureManagerWebAgg + + class WebAggApplication(tornado.web.Application): initialized = False started = False diff --git a/lib/matplotlib/backends/backend_wx.py b/lib/matplotlib/backends/backend_wx.py index 37177cf6f60e..3eaf59e3f502 100644 --- a/lib/matplotlib/backends/backend_wx.py +++ b/lib/matplotlib/backends/backend_wx.py @@ -63,6 +63,15 @@ def error_msg_wx(msg, parent=None): return None +# lru_cache holds a reference to the App and prevents it from being gc'ed. +@functools.lru_cache(1) +def _create_wxapp(): + wxapp = wx.App(False) + wxapp.SetExitOnFrameDelete(True) + cbook._setup_new_guiapp() + return wxapp + + class TimerWx(TimerBase): """Subclass of `.TimerBase` using wx.Timer events.""" @@ -418,6 +427,7 @@ class _FigureCanvasWxBase(FigureCanvasBase, wx.Panel): required_interactive_framework = "wx" _timer_cls = TimerWx + manager_class = _api.classproperty(lambda cls: FigureManagerWx) keyvald = { wx.WXK_CONTROL: 'control', @@ -970,6 +980,17 @@ def __init__(self, canvas, num, frame): self.frame = self.window = frame super().__init__(canvas, num) + @classmethod + def create_with_canvas(cls, canvas_class, figure, num): + # docstring inherited + wxapp = wx.GetApp() or _create_wxapp() + frame = FigureFrameWx(num, figure, canvas_class=canvas_class) + manager = figure.canvas.manager + if mpl.is_interactive(): + manager.frame.Show() + figure.canvas.draw_idle() + return manager + def show(self): # docstring inherited self.frame.Show() @@ -1344,24 +1365,6 @@ class _BackendWx(_Backend): FigureCanvas = FigureCanvasWx FigureManager = FigureManagerWx - @classmethod - def new_figure_manager_given_figure(cls, num, figure): - # Create a wx.App instance if it has not been created so far. - wxapp = wx.GetApp() - if wxapp is None: - wxapp = wx.App() - wxapp.SetExitOnFrameDelete(True) - cbook._setup_new_guiapp() - # Retain a reference to the app object so that it does not get - # garbage collected. - _BackendWx._theWxApp = wxapp - # Attaches figure.canvas, figure.canvas.manager. - frame = FigureFrameWx(num, figure, canvas_class=cls.FigureCanvas) - if mpl.is_interactive(): - frame.Show() - figure.canvas.draw_idle() - return figure.canvas.manager - @staticmethod def mainloop(): if not wx.App.IsMainLoopRunning():