Skip to content

Commit 8bb2bb1

Browse files
committed
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.
1 parent 4e20f15 commit 8bb2bb1

9 files changed

+109
-81
lines changed

lib/matplotlib/backend_bases.py

+14-7
Original file line numberDiff line numberDiff line change
@@ -1662,6 +1662,16 @@ def _fix_ipython_backend2gui(cls):
16621662
if _is_non_interactive_terminal_ipython(ip):
16631663
ip.enable_gui(backend2gui_rif)
16641664

1665+
@classmethod
1666+
def new_manager(cls, figure, num):
1667+
"""
1668+
Create a new figure manager for *figure*, using this canvas class.
1669+
1670+
Backends should override this method to instantiate the correct figure
1671+
manager subclass, and perform any additional setup that may be needed.
1672+
"""
1673+
return FigureManagerBase(cls(figure), num)
1674+
16651675
@contextmanager
16661676
def _idle_draw_cntx(self):
16671677
self._is_idle_drawing = True
@@ -3225,11 +3235,10 @@ def configure_subplots(self, *args):
32253235
if hasattr(self, "subplot_tool"):
32263236
self.subplot_tool.figure.canvas.manager.show()
32273237
return
3228-
plt = _safe_pyplot_import()
3238+
# This import needs to happen here due to circular imports.
3239+
from matplotlib.figure import Figure
32293240
with mpl.rc_context({"toolbar": "none"}): # No navbar for the toolfig.
3230-
# Use new_figure_manager() instead of figure() so that the figure
3231-
# doesn't get registered with pyplot.
3232-
manager = plt.new_figure_manager(-1, (6, 3))
3241+
manager = type(self.canvas).new_manager(Figure(figsize=(6, 3)), -1)
32333242
manager.set_window_title("Subplot configuration tool")
32343243
tool_fig = manager.canvas.figure
32353244
tool_fig.subplots_adjust(top=0.9)
@@ -3457,9 +3466,7 @@ def new_figure_manager(cls, num, *args, **kwargs):
34573466
@classmethod
34583467
def new_figure_manager_given_figure(cls, num, figure):
34593468
"""Create a new figure manager instance for the given figure."""
3460-
canvas = cls.FigureCanvas(figure)
3461-
manager = cls.FigureManager(canvas, num)
3462-
return manager
3469+
return cls.FigureCanvas.new_manager(figure, num)
34633470

34643471
@classmethod
34653472
def draw_if_interactive(cls):

lib/matplotlib/backends/_backend_tk.py

+37-39
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,43 @@ def _update_device_pixel_ratio(self, event=None):
223223
w, h = self.get_width_height(physical=True)
224224
self._tkcanvas.configure(width=w, height=h)
225225

226+
@classmethod
227+
def new_manager(cls, figure, num):
228+
# docstring inherited
229+
with _restore_foreground_window_at_end():
230+
if cbook._get_running_interactive_framework() is None:
231+
cbook._setup_new_guiapp()
232+
_c_internal_utils.Win32_SetProcessDpiAwareness_max()
233+
window = tk.Tk(className="matplotlib")
234+
window.withdraw()
235+
236+
# Put a Matplotlib icon on the window rather than the default tk
237+
# icon. See https://www.tcl.tk/man/tcl/TkCmd/wm.html#M50
238+
#
239+
# `ImageTk` can be replaced with `tk` whenever the minimum
240+
# supported Tk version is increased to 8.6, as Tk 8.6+ natively
241+
# supports PNG images.
242+
icon_fname = str(cbook._get_data_path(
243+
'images/matplotlib.png'))
244+
icon_img = ImageTk.PhotoImage(file=icon_fname, master=window)
245+
246+
icon_fname_large = str(cbook._get_data_path(
247+
'images/matplotlib_large.png'))
248+
icon_img_large = ImageTk.PhotoImage(
249+
file=icon_fname_large, master=window)
250+
try:
251+
window.iconphoto(False, icon_img_large, icon_img)
252+
except Exception as exc:
253+
# log the failure (due e.g. to Tk version), but carry on
254+
_log.info('Could not load matplotlib icon: %s', exc)
255+
256+
canvas = cls(figure, master=window)
257+
manager = FigureManagerTk(canvas, num, window)
258+
if mpl.is_interactive():
259+
manager.show()
260+
canvas.draw_idle()
261+
return manager
262+
226263
def resize(self, event):
227264
width, height = event.width, event.height
228265

@@ -958,45 +995,6 @@ def trigger(self, *args):
958995
class _BackendTk(_Backend):
959996
FigureManager = FigureManagerTk
960997

961-
@classmethod
962-
def new_figure_manager_given_figure(cls, num, figure):
963-
"""
964-
Create a new figure manager instance for the given figure.
965-
"""
966-
with _restore_foreground_window_at_end():
967-
if cbook._get_running_interactive_framework() is None:
968-
cbook._setup_new_guiapp()
969-
_c_internal_utils.Win32_SetProcessDpiAwareness_max()
970-
window = tk.Tk(className="matplotlib")
971-
window.withdraw()
972-
973-
# Put a Matplotlib icon on the window rather than the default tk
974-
# icon. See https://www.tcl.tk/man/tcl/TkCmd/wm.html#M50
975-
#
976-
# `ImageTk` can be replaced with `tk` whenever the minimum
977-
# supported Tk version is increased to 8.6, as Tk 8.6+ natively
978-
# supports PNG images.
979-
icon_fname = str(cbook._get_data_path(
980-
'images/matplotlib.png'))
981-
icon_img = ImageTk.PhotoImage(file=icon_fname, master=window)
982-
983-
icon_fname_large = str(cbook._get_data_path(
984-
'images/matplotlib_large.png'))
985-
icon_img_large = ImageTk.PhotoImage(
986-
file=icon_fname_large, master=window)
987-
try:
988-
window.iconphoto(False, icon_img_large, icon_img)
989-
except Exception as exc:
990-
# log the failure (due e.g. to Tk version), but carry on
991-
_log.info('Could not load matplotlib icon: %s', exc)
992-
993-
canvas = cls.FigureCanvas(figure, master=window)
994-
manager = cls.FigureManager(canvas, num, window)
995-
if mpl.is_interactive():
996-
manager.show()
997-
canvas.draw_idle()
998-
return manager
999-
1000998
@staticmethod
1001999
def mainloop():
10021000
managers = Gcf.get_all_fig_managers()

lib/matplotlib/backends/backend_gtk3.py

+5
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,11 @@ def __init__(self, figure=None):
115115
style_ctx.add_provider(css, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
116116
style_ctx.add_class("matplotlib-canvas")
117117

118+
@classmethod
119+
def new_manager(cls, figure, num):
120+
# docstring inherited
121+
return FigureManagerGTK3(cls(figure), num)
122+
118123
def destroy(self):
119124
self.close_event()
120125

lib/matplotlib/backends/backend_gtk4.py

+5
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,11 @@ def __init__(self, figure=None):
7878
style_ctx.add_provider(css, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
7979
style_ctx.add_class("matplotlib-canvas")
8080

81+
@classmethod
82+
def new_manager(cls, figure, num):
83+
# docstring inherited
84+
return FigureManagerGTK4(cls(figure), num)
85+
8186
def pick(self, mouseevent):
8287
# GtkWidget defines pick in GTK4, so we need to override here to work
8388
# with the base implementation we want.

lib/matplotlib/backends/backend_macosx.py

+5
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ def __init__(self, figure):
3333
self._draw_pending = False
3434
self._is_drawing = False
3535

36+
@classmethod
37+
def new_manager(cls, figure, num):
38+
# docstring inherited
39+
return FigureManagerMac(cls(figure), num)
40+
3641
def set_cursor(self, cursor):
3742
# docstring inherited
3843
_macosx.set_cursor(cursor)

lib/matplotlib/backends/backend_nbagg.py

+14-16
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,20 @@ def remove_comm(self, comm_id):
143143

144144

145145
class FigureCanvasNbAgg(FigureCanvasWebAggCore):
146-
pass
146+
@classmethod
147+
def new_manager(cls, figure, num):
148+
canvas = cls(figure)
149+
manager = FigureManagerNbAgg(canvas, num)
150+
if is_interactive():
151+
manager.show()
152+
figure.canvas.draw_idle()
153+
154+
def destroy(event):
155+
canvas.mpl_disconnect(cid)
156+
Gcf.destroy(manager)
157+
158+
cid = canvas.mpl_connect('close_event', destroy)
159+
return manager
147160

148161

149162
class CommSocket:
@@ -228,21 +241,6 @@ class _BackendNbAgg(_Backend):
228241
FigureCanvas = FigureCanvasNbAgg
229242
FigureManager = FigureManagerNbAgg
230243

231-
@staticmethod
232-
def new_figure_manager_given_figure(num, figure):
233-
canvas = FigureCanvasNbAgg(figure)
234-
manager = FigureManagerNbAgg(canvas, num)
235-
if is_interactive():
236-
manager.show()
237-
figure.canvas.draw_idle()
238-
239-
def destroy(event):
240-
canvas.mpl_disconnect(cid)
241-
Gcf.destroy(manager)
242-
243-
cid = canvas.mpl_connect('close_event', destroy)
244-
return manager
245-
246244
@staticmethod
247245
def show(block=None):
248246
## TODO: something to do when keyword block==False ?

lib/matplotlib/backends/backend_qt.py

+5
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,11 @@ def __init__(self, figure=None):
260260
palette = QtGui.QPalette(QtGui.QColor("white"))
261261
self.setPalette(palette)
262262

263+
@classmethod
264+
def new_manager(cls, figure, num):
265+
# docstring inherited
266+
return FigureManagerQT(cls(figure), num)
267+
263268
def _update_pixel_ratio(self):
264269
if self._set_device_pixel_ratio(_devicePixelRatioF(self)):
265270
# The easiest way to resize the canvas is to emit a resizeEvent

lib/matplotlib/backends/backend_webagg.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,10 @@ def run(self):
4949

5050

5151
class FigureCanvasWebAgg(core.FigureCanvasWebAggCore):
52-
pass
52+
@classmethod
53+
def new_manager(cls, figure, num):
54+
# docstring inherited
55+
return core.FigureManagerWebAgg(cls(figure), num)
5356

5457

5558
class FigureManagerWebAgg(core.FigureManagerWebAgg):

lib/matplotlib/backends/backend_wx.py

+20-18
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,15 @@ def error_msg_wx(msg, parent=None):
6363
return None
6464

6565

66+
# lru_cache holds a reference to the App and prevents it from being gc'ed.
67+
@functools.lru_cache(1)
68+
def _create_wxapp():
69+
wxapp = wx.App(False)
70+
wxapp.SetExitOnFrameDelete(True)
71+
cbook._setup_new_guiapp()
72+
return wxapp
73+
74+
6675
class TimerWx(TimerBase):
6776
"""Subclass of `.TimerBase` using wx.Timer events."""
6877

@@ -528,6 +537,17 @@ def __init__(self, parent, id, figure=None):
528537
self.SetBackgroundStyle(wx.BG_STYLE_PAINT) # Reduce flicker.
529538
self.SetBackgroundColour(wx.WHITE)
530539

540+
@classmethod
541+
def new_manager(cls, figure, num):
542+
# docstring inherited
543+
wxapp = wx.GetApp() or _create_wxapp()
544+
frame = FigureFrameWx(num, figure, canvas_class=cls)
545+
figmgr = frame.get_figure_manager()
546+
if mpl.is_interactive():
547+
figmgr.frame.Show()
548+
figure.canvas.draw_idle()
549+
return figmgr
550+
531551
def Copy_to_Clipboard(self, event=None):
532552
"""Copy bitmap of canvas to system clipboard."""
533553
bmp_obj = wx.BitmapDataObject()
@@ -1344,24 +1364,6 @@ class _BackendWx(_Backend):
13441364
FigureCanvas = FigureCanvasWx
13451365
FigureManager = FigureManagerWx
13461366

1347-
@classmethod
1348-
def new_figure_manager_given_figure(cls, num, figure):
1349-
# Create a wx.App instance if it has not been created so far.
1350-
wxapp = wx.GetApp()
1351-
if wxapp is None:
1352-
wxapp = wx.App()
1353-
wxapp.SetExitOnFrameDelete(True)
1354-
cbook._setup_new_guiapp()
1355-
# Retain a reference to the app object so that it does not get
1356-
# garbage collected.
1357-
_BackendWx._theWxApp = wxapp
1358-
# Attaches figure.canvas, figure.canvas.manager.
1359-
frame = FigureFrameWx(num, figure, canvas_class=cls.FigureCanvas)
1360-
if mpl.is_interactive():
1361-
frame.Show()
1362-
figure.canvas.draw_idle()
1363-
return figure.canvas.manager
1364-
13651367
@staticmethod
13661368
def mainloop():
13671369
if not wx.App.IsMainLoopRunning():

0 commit comments

Comments
 (0)