Skip to content

Alternative approach to organizing uniform manager creation #22895

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
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
25 changes: 18 additions & 7 deletions lib/matplotlib/backend_bases.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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):
Expand Down
78 changes: 39 additions & 39 deletions lib/matplotlib/backends/_backend_tk.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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()
Expand Down
1 change: 1 addition & 0 deletions lib/matplotlib/backends/backend_gtk3.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions lib/matplotlib/backends/backend_gtk4.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
1 change: 1 addition & 0 deletions lib/matplotlib/backends/backend_macosx.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
31 changes: 15 additions & 16 deletions lib/matplotlib/backends/backend_nbagg.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -143,7 +157,7 @@ def remove_comm(self, comm_id):


class FigureCanvasNbAgg(FigureCanvasWebAggCore):
pass
manager_class = FigureManagerNbAgg


class CommSocket:
Expand Down Expand Up @@ -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 ?
Expand Down
1 change: 1 addition & 0 deletions lib/matplotlib/backends/backend_qt.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 [
Expand Down
2 changes: 1 addition & 1 deletion lib/matplotlib/backends/backend_webagg.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def run(self):


class FigureCanvasWebAgg(core.FigureCanvasWebAggCore):
pass
manager_class = core.FigureManagerWebAgg


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 @@ -418,6 +427,7 @@ class _FigureCanvasWxBase(FigureCanvasBase, wx.Panel):

required_interactive_framework = "wx"
_timer_cls = TimerWx
manager_class = FigureManagerWx

keyvald = {
wx.WXK_CONTROL: 'control',
Expand Down Expand Up @@ -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()
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