Skip to content

Standardize creation of FigureManager from a given FigureCanvas class. #18854

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 14 additions & 7 deletions lib/matplotlib/backend_bases.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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):
Expand Down
76 changes: 37 additions & 39 deletions lib/matplotlib/backends/_backend_tk.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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()
Expand Down
5 changes: 5 additions & 0 deletions lib/matplotlib/backends/backend_gtk3.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link
Member

@timhoffm timhoffm Apr 19, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find it a bit odd that we code the relation between the canvas and the matching manager into a class method on the canvas. Also the hierarchy feels unclear.

Two alternative suggestions:

  1. How about storing this information either in a canvas class attribute, or in global dict? That way, we could have a standalone function new_manager(canvas_class, figure, num), which feels a bit better than having a method on the canvas that creates a manager.

  2. Or one goes the alternative route and considers the manager to be an optional part of the canvas.

    canvas = [make the canvas instance]
    canvas.attach_manager()
    

    Possibly there is a better name than attach. But the point here is that you amend an existing canvas instance. The original new_manager proposal has the canvas class as entry point, but OTOH you get the manager, that holds a reference to the canvas. In my view this mixes the priority/hierarchy. In the first part, the canvas seems to be the leading object, whereas in the second the manager seems to take over.


def destroy(self):
self.close_event()

Expand Down
5 changes: 5 additions & 0 deletions lib/matplotlib/backends/backend_gtk4.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
5 changes: 5 additions & 0 deletions lib/matplotlib/backends/backend_macosx.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
30 changes: 14 additions & 16 deletions lib/matplotlib/backends/backend_nbagg.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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 ?
Expand Down
5 changes: 5 additions & 0 deletions lib/matplotlib/backends/backend_qt.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 4 additions & 1 deletion lib/matplotlib/backends/backend_webagg.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
38 changes: 20 additions & 18 deletions lib/matplotlib/backends/backend_wx.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""

Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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():
Expand Down