diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 0e7ee91a0a8c..27fc40f65f7d 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -1662,6 +1662,16 @@ 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. + + Backends should override this method to instantiate the correct figure + manager subclass, and perform any additional setup that may be needed. + """ + return FigureManagerBase(cls(figure), num) + @contextmanager def _idle_draw_cntx(self): self._is_idle_drawing = True @@ -3225,11 +3235,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 +3466,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..b7c88c90342e 100644 --- a/lib/matplotlib/backends/_backend_tk.py +++ b/lib/matplotlib/backends/_backend_tk.py @@ -223,6 +223,43 @@ def _update_device_pixel_ratio(self, event=None): w, h = self.get_width_height(physical=True) self._tkcanvas.configure(width=w, height=h) + @classmethod + def new_manager(cls, 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 = cls(figure, master=window) + manager = FigureManagerTk(canvas, num, window) + if mpl.is_interactive(): + manager.show() + canvas.draw_idle() + return manager + def resize(self, event): width, height = event.width, event.height @@ -958,45 +995,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..b8ceef4c98ce 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -115,6 +115,11 @@ def __init__(self, figure=None): style_ctx.add_provider(css, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) style_ctx.add_class("matplotlib-canvas") + @classmethod + def new_manager(cls, figure, num): + # docstring inherited + return FigureManagerGTK3(cls(figure), num) + def destroy(self): self.close_event() diff --git a/lib/matplotlib/backends/backend_gtk4.py b/lib/matplotlib/backends/backend_gtk4.py index 63663834ec00..4e3a40883f89 100644 --- a/lib/matplotlib/backends/backend_gtk4.py +++ b/lib/matplotlib/backends/backend_gtk4.py @@ -78,6 +78,11 @@ def __init__(self, figure=None): style_ctx.add_provider(css, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) style_ctx.add_class("matplotlib-canvas") + @classmethod + def new_manager(cls, figure, num): + # docstring inherited + return FigureManagerGTK4(cls(figure), num) + def pick(self, mouseevent): # GtkWidget defines pick in GTK4, so we need to override here to work # with the base implementation we want. diff --git a/lib/matplotlib/backends/backend_macosx.py b/lib/matplotlib/backends/backend_macosx.py index 4700e28e48cc..17c222eb4956 100644 --- a/lib/matplotlib/backends/backend_macosx.py +++ b/lib/matplotlib/backends/backend_macosx.py @@ -33,6 +33,11 @@ def __init__(self, figure): self._draw_pending = False self._is_drawing = False + @classmethod + def new_manager(cls, figure, num): + # docstring inherited + return FigureManagerMac(cls(figure), num) + def set_cursor(self, cursor): # docstring inherited _macosx.set_cursor(cursor) diff --git a/lib/matplotlib/backends/backend_nbagg.py b/lib/matplotlib/backends/backend_nbagg.py index ec1430e60d1b..02921ef3239c 100644 --- a/lib/matplotlib/backends/backend_nbagg.py +++ b/lib/matplotlib/backends/backend_nbagg.py @@ -143,7 +143,20 @@ def remove_comm(self, comm_id): class FigureCanvasNbAgg(FigureCanvasWebAggCore): - pass + @classmethod + def new_manager(cls, figure, num): + canvas = cls(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 class CommSocket: @@ -228,21 +241,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..c59ae9e8f5b7 100644 --- a/lib/matplotlib/backends/backend_qt.py +++ b/lib/matplotlib/backends/backend_qt.py @@ -260,6 +260,11 @@ def __init__(self, figure=None): palette = QtGui.QPalette(QtGui.QColor("white")) self.setPalette(palette) + @classmethod + def new_manager(cls, figure, num): + # docstring inherited + return FigureManagerQT(cls(figure), num) + def _update_pixel_ratio(self): if self._set_device_pixel_ratio(_devicePixelRatioF(self)): # The easiest way to resize the canvas is to emit a resizeEvent diff --git a/lib/matplotlib/backends/backend_webagg.py b/lib/matplotlib/backends/backend_webagg.py index a78e86c25af8..bce64118f2c4 100644 --- a/lib/matplotlib/backends/backend_webagg.py +++ b/lib/matplotlib/backends/backend_webagg.py @@ -49,7 +49,10 @@ def run(self): class FigureCanvasWebAgg(core.FigureCanvasWebAggCore): - pass + @classmethod + def new_manager(cls, figure, num): + # docstring inherited + return core.FigureManagerWebAgg(cls(figure), num) class FigureManagerWebAgg(core.FigureManagerWebAgg): diff --git a/lib/matplotlib/backends/backend_wx.py b/lib/matplotlib/backends/backend_wx.py index 37177cf6f60e..260190cbccd0 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.""" @@ -528,6 +537,17 @@ def __init__(self, parent, id, figure=None): self.SetBackgroundStyle(wx.BG_STYLE_PAINT) # Reduce flicker. self.SetBackgroundColour(wx.WHITE) + @classmethod + def new_manager(cls, figure, num): + # docstring inherited + wxapp = wx.GetApp() or _create_wxapp() + frame = FigureFrameWx(num, figure, canvas_class=cls) + figmgr = frame.get_figure_manager() + if mpl.is_interactive(): + figmgr.frame.Show() + figure.canvas.draw_idle() + return figmgr + def Copy_to_Clipboard(self, event=None): """Copy bitmap of canvas to system clipboard.""" bmp_obj = wx.BitmapDataObject() @@ -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():