diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 0e7ee91a0a8c..494a3ceeaa4e 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -1576,6 +1576,8 @@ class FigureCanvasBase: A high-level figure instance. """ + manager_class = FigureManagerBase + # Set to one of {"qt", "gtk3", "gtk4", "wx", "tk", "macosx"} if an # interactive framework is required, or None otherwise. required_interactive_framework = None @@ -1662,6 +1664,13 @@ 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. + """ + return cls.manager_class.create_with_canvas(cls, figure, num) + @contextmanager def _idle_draw_cntx(self): self._is_idle_drawing = True @@ -2759,6 +2768,11 @@ 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): + canvas = canvas_class(figure) + return cls(canvas, num) + def show(self): """ For GUI backends, show the figure window and redraw. @@ -3225,11 +3239,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 +3470,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..6a11702a0f45 100644 --- a/lib/matplotlib/backends/_backend_tk.py +++ b/lib/matplotlib/backends/_backend_tk.py @@ -161,6 +161,7 @@ def _on_timer(self): class FigureCanvasTk(FigureCanvasBase): + manager_class = FigureManagerTk required_interactive_framework = "tk" def __init__(self, figure=None, master=None): @@ -431,6 +432,44 @@ def __init__(self, canvas, num, window): self._shown = False + @classmethod + def create_with_canvas(cls, canvas_class, figure, num): + + 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 = FigureManagerTk(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 +997,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..3a260d59341e 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 = 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..a3546d4f65b1 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 = 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..9f53197b326e 100644 --- a/lib/matplotlib/backends/backend_macosx.py +++ b/lib/matplotlib/backends/backend_macosx.py @@ -25,6 +25,7 @@ class FigureCanvasMac(_macosx.FigureCanvas, FigureCanvasAgg): required_interactive_framework = "macosx" _timer_cls = TimerMac + manager_class = 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..b590a8fdd8fa 100644 --- a/lib/matplotlib/backends/backend_nbagg.py +++ b/lib/matplotlib/backends/backend_nbagg.py @@ -78,6 +78,20 @@ def __init__(self, canvas, num): self._shown = False super().__init__(canvas, num) + def create_with_canvas(self, canvas_class, figure, num): + canvas = canvas_class(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 + 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 +157,7 @@ def remove_comm(self, comm_id): class FigureCanvasNbAgg(FigureCanvasWebAggCore): - pass + manager_class = FigureManagerNbAgg class CommSocket: @@ -228,21 +242,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..d1072ed8e3e4 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 = FigureManagerQT buttond = { getattr(_enum("QtCore.Qt.MouseButton"), k): v for k, v in [ diff --git a/lib/matplotlib/backends/backend_webagg.py b/lib/matplotlib/backends/backend_webagg.py index a78e86c25af8..905dabbf8aae 100644 --- a/lib/matplotlib/backends/backend_webagg.py +++ b/lib/matplotlib/backends/backend_webagg.py @@ -49,7 +49,7 @@ def run(self): class FigureCanvasWebAgg(core.FigureCanvasWebAggCore): - pass + manager_class = core.FigureManagerWebAgg class FigureManagerWebAgg(core.FigureManagerWebAgg): diff --git a/lib/matplotlib/backends/backend_wx.py b/lib/matplotlib/backends/backend_wx.py index 37177cf6f60e..0201ec925f48 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 = FigureManagerWx keyvald = { wx.WXK_CONTROL: 'control', @@ -970,6 +980,16 @@ 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): + wxapp = wx.GetApp() or _create_wxapp() + frame = FigureFrameWx(num, figure, canvas_class=canvas_class) + figmgr = frame.get_figure_manager() + if mpl.is_interactive(): + figmgr.frame.Show() + figure.canvas.draw_idle() + return figmgr + def show(self): # docstring inherited self.frame.Show() @@ -1344,24 +1364,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():