From 8bb2bb145de6d57d3c9da9a2fbf72c02d91df498 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Sun, 30 Aug 2020 22:25:06 +0200 Subject: [PATCH] Standardize creation of FigureManager from a given FigureCanvas class. The `new_manager` classmethod may appear to belong more naturally to the FigureManager class rather than the FigureCanvas class, but putting it on FigureCanvas has multiple advantages: - One may want to create managers at times where all one has is a FigureCanvas instance, which may not even have a corresponding manager (e.g. `subplot_tool`). - A given FigureManager class can be associated with many different FigureCanvas classes (indeed, FigureManagerQT can manage both FigureCanvasQTAgg and FigureCanvasQTCairo and also the mplcairo Qt canvas classes), whereas we don't have multiple FigureManager classes for a given FigureCanvas class. --- lib/matplotlib/backend_bases.py | 21 ++++--- lib/matplotlib/backends/_backend_tk.py | 76 +++++++++++------------ lib/matplotlib/backends/backend_gtk3.py | 5 ++ lib/matplotlib/backends/backend_gtk4.py | 5 ++ lib/matplotlib/backends/backend_macosx.py | 5 ++ lib/matplotlib/backends/backend_nbagg.py | 30 +++++---- lib/matplotlib/backends/backend_qt.py | 5 ++ lib/matplotlib/backends/backend_webagg.py | 5 +- lib/matplotlib/backends/backend_wx.py | 38 ++++++------ 9 files changed, 109 insertions(+), 81 deletions(-) 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():