8000 Standardize creation of FigureManager from a given FigureCanvas class. · matplotlib/matplotlib@ad1ded4 · GitHub
[go: up one dir, main page]

Skip to content

Commit ad1ded4

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. Still, put the *actual* logic in FigureManager.create_with_canvas and access it via some indirection, as suggested by timhoffm.
1 parent 4e20f15 commit ad1ded4

10 files changed

+122
-93
lines changed

lib/matplotlib/backend_bases.py

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1580,6 +1580,13 @@ class FigureCanvasBase:
15801580
# interactive framework is required, or None otherwise.
15811581
required_interactive_framework = None
15821582

1583+
# The manager class instantiated by new_manager.
1584+
# (This is defined as a classproperty because the manager class is
1585+
# currently defined *after* the canvas class, but one could also assign
1586+
# ``FigureCanvasBase.manager_class = FigureManagerBase``
1587+
# after defining both classes.)
1588+
manager_class = _api.classproperty(lambda cls: FigureManagerBase)
1589+
15831590
events = [
15841591
'resize_event',
15851592
'draw_event',
@@ -1662,6 +1669,13 @@ def _fix_ipython_backend2gui(cls):
16621669
if _is_non_interactive_terminal_ipython(ip):
16631670
ip.enable_gui(backend2gui_rif)
16641671

1672+
@classmethod
1673+
def new_manager(cls, figure, num):
1674+
"""
1675+
Create a new figure manager for *figure*, using this canvas class.
1676+
"""
1677+
return cls.manager_class.create_with_canvas(cls, figure, num)
1678+
16651679
@contextmanager
16661680
def _idle_draw_cntx(self):
16671681
self._is_idle_drawing = True
@@ -2759,6 +2773,16 @@ def notify_axes_change(fig):
27592773
if self.toolmanager is None and self.toolbar is not None:
27602774
self.toolbar.update()
27612775

2776+
@classmethod
2777+
def create_with_canvas(cls, canvas_class, figure, num):
2778+
"""
2779+
Create a manager for a given *figure* using a specific *canvas_class*.
2780+
2781+
Backends should override this method if they have specific needs for
2782+
setting up the canvas or the manager.
2783+
"""
2784+
return cls(canvas_class(figure), num)
2785+
27622786
def show(self):
27632787
"""
27642788
For GUI backends, show the figure window and redraw.
@@ -3225,11 +3249,10 @@ def configure_subplots(self, *args):
32253249
if hasattr(self, "subplot_tool"):
32263250
self.subplot_tool.figure.canvas.manager.show()
32273251
return
3228-
plt = _safe_pyplot_import()
3252+
# This import needs to happen here due to circular imports.
3253+
from matplotlib.figure import Figure
32293254
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))
3255+
manager = type(self.canvas).new_manager(Figure(figsize=(6, 3)), -1)
32333256
manager.set_window_title("Subplot configuration tool")
32343257
tool_fig = manager.canvas.figure
32353258
tool_fig.subplots_adjust(top=0.9)
@@ -3457,9 +3480,7 @@ def new_figure_man F438 ager(cls, num, *args, **kwargs):
34573480
@classmethod
34583481
def new_figure_manager_given_figure(cls, num, figure):
34593482
"""Create a new figure manager instance for the given figure."""
3460-
canvas = cls.FigureCanvas(figure)
3461-
manager = cls.FigureManager(canvas, num)
3462-
return manager
3483+
return cls.FigureCanvas.new_manager(figure, num)
34633484

34643485
@classmethod
34653486
def draw_if_interactive(cls):

lib/matplotlib/backends/_backend_tk.py

Lines changed: 38 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ def _on_timer(self):
162162

163163
class FigureCanvasTk(FigureCanvasBase):
164164
required_interactive_framework = "tk"
165+
manager_class = _api.classproperty(lambda cls: FigureManagerTk)
165166

166167
def __init__(self, figure=None, master=None):
167168
super().__init__(figure)
@@ -431,6 +432,43 @@ def __init__(self, canvas, num, window):
431432

432433
self._shown = False
433434

435+
@classmethod
436+
def create_with_canvas(cls, canvas_class, figure, num):
437+
# docstring inherited
438+
with _restore_foreground_window_at_end():
439+
if cbook._get_running_interactive_framework() is None:
440+
cbook._setup_new_guiapp()
441+
_c_internal_utils.Win32_SetProcessDpiAwareness_max()
442+
window = tk.Tk(className="matplotlib")
443+
window.withdraw()
444+
445+
# Put a Matplotlib icon on the window rather than the default tk
446+
# icon. See https://www.tcl.tk/man/tcl/TkCmd/wm.html#M50
447+
#
448+
# `ImageTk` can be replaced with `tk` whenever the minimum
449+
# supported Tk version is increased to 8.6, as Tk 8.6+ natively
450+
# supports PNG images.
451+
icon_fname = str(cbook._get_data_path(
452+
'images/matplotlib.png'))
453+
icon_img = ImageTk.PhotoImage(file=icon_fname, master=window)
454+
455+
icon_fname_large = str(cbook._get_data_path(
456+
'images/matplotlib_large.png'))
457+
icon_img_large = ImageTk.PhotoImage(
458+
file=icon_fname_large, master=window)
459+
try:
460+
window.iconphoto(False, icon_img_large, icon_img)
461+
except Exception as exc:
462+
# log the failure (due e.g. to Tk version), but carry on
463+
_log.info('Could not load matplotlib icon: %s', exc)
464+
465+
canvas = canvas_class(figure, master=window)
466+
manager = cls(canvas, num, window)
467+
if mpl.is_interactive():
468+
manager.show()
469+
canvas.draw_idle()
470+
return manager
471+
434472
def _update_window_dpi(self, *args):
435473
newdpi = self._window_dpi.get()
436474
self.window.call('tk', 'scaling', newdpi / 72)
@@ -958,45 +996,6 @@ def trigger(self, *args):
958996
class _BackendTk(_Backend):
959997
FigureManager = FigureManagerTk
960998

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-
1000999
@staticmethod
10011000
def mainloop():
10021001
managers = Gcf.get_all_fig_managers()

lib/matplotlib/backends/backend_gtk3.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ def _mpl_to_gtk_cursor(mpl_cursor):
7171
class FigureCanvasGTK3(Gtk.DrawingArea, FigureCanvasBase):
7272
required_interactive_framework = "gtk3"
7373
_timer_cls = TimerGTK3
74+
manager_class = _api.classproperty(lambda cls: FigureManagerGTK3)
7475
# Setting this as a static constant prevents
7576
# this resulting expression from leaking
7677
event_mask = (Gdk.EventMask.BUTTON_PRESS_MASK

lib/matplotlib/backends/backend_gtk4.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ class FigureCanvasGTK4(Gtk.DrawingArea, FigureCanvasBase):
3333
required_interactive_framework = "gtk4"
3434
supports_blit = False
3535
_timer_cls = TimerGTK4
36+
manager_class = _api.classproperty(lambda cls: FigureManagerGTK4)
3637
_context_is_scaled = False
3738

3839
def __init__(self, figure=None):

lib/matplotlib/backends/backend_macosx.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import matplotlib as mpl
2-
from matplotlib import cbook
2+
from matplotlib import _api, cbook
33
from matplotlib._pylab_helpers import Gcf
44
from . import _macosx
55
from .backend_agg import FigureCanvasAgg
@@ -25,6 +25,7 @@ class FigureCanvasMac(_macosx.FigureCanvas, FigureCanvasAgg):
2525

2626
required_interactive_framework = "macosx"
2727
_timer_cls = TimerMac
28+
manager_class = _api.classproperty(lambda cls: FigureManagerMac)
2829

2930
def __init__(self, figure):
3031
FigureCanvasBase.__init__(self, figure)

lib/matplotlib/backends/backend_nbagg.py

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,21 @@ def __init__(self, canvas, num):
7878
self._shown = False
7979
super().__init__(canvas, num)
8080

81+
@classmethod
82+
def create_with_canvas(cls, canvas_class, figure, num):
83+
canvas = canvas_class(figure)
84+
manager = cls(canvas, num)
85+
if is_interactive():
86+
manager.show()
87+
canvas.draw_idle()
88+
89+
def destroy(event):
90+
canvas.mpl_disconnect(cid)
91+
Gcf.destroy(manager)
92+
93+
cid = canvas.mpl_connect('close_event', destroy)
94+
return manager
95+
8196
def display_js(self):
8297
# XXX How to do this just once? It has to deal with multiple
8398
# browser instances using the same kernel (require.js - but the
@@ -143,7 +158,7 @@ def remove_comm(self, comm_id):
143158

144159

145160
class FigureCanvasNbAgg(FigureCanvasWebAggCore):
146-
pass
161+
manager_class = FigureManagerNbAgg
147162

148163

149164
class CommSocket:
@@ -228,21 +243,6 @@ class _BackendNbAgg(_Backend):
228243
FigureCanvas = FigureCanvasNbAgg
229244
FigureManager = FigureManagerNbAgg
230245

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-
246246
@staticmethod
247247
def show(block=None):
248248
## TODO: something to do when keyword block==False ?

lib/matplotlib/backends/backend_qt.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,7 @@ def _timer_stop(self):
232232
class FigureCanvasQT(QtWidgets.QWidget, FigureCanvasBase):
233233
required_interactive_framework = "qt"
234234
_timer_cls = TimerQT
235+
manager_class = _api.classproperty(lambda cls: FigureManagerQT)
235236

236237
buttond = {
237238
getattr(_enum("QtCore.Qt.MouseButton"), k): v for k, v in [

lib/matplotlib/backends/backend_template.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,14 @@ def new_figure_manager_given_figure(num, figure):
174174
return manager
175175

176176

177+
class FigureManagerTemplate(FigureManagerBase):
178+
"""
179+
Helper class for pyplot mode, wraps everything up into a neat bundle.
180+
181+
For non-interactive backends, the base class is sufficient.
182+
"""
183+
184+
177185
class FigureCanvasTemplate(FigureCanvasBase):
178186
"""
179187
The canvas the figure renders into. Calls the draw and print fig
@@ -191,6 +199,8 @@ class methods button_press_event, button_release_event,
191199
A high-level Figure instance
192200
"""
193201

202+
manager_class = FigureManagerTemplate
203+
194204
def draw(self):
195205
"""
196206
Draw the figure using the renderer.
@@ -227,14 +237,6 @@ def get_default_filetype(self):
227237
return 'foo'
228238

229239

230-
class FigureManagerTemplate(FigureManagerBase):
231-
"""
232-
Helper class for pyplot mode, wraps everything up into a neat bundle.
233-
234-
For non-interactive backends, the base class is sufficient.
235-
"""
236-
237-
238240
########################################################################
239241
#
240242
# Now just provide the standard names that backend.__init__ is expecting

lib/matplotlib/backends/backend_webagg.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,14 +48,14 @@ def run(self):
4848
webagg_server_thread = ServerThread()
4949

5050

51-
class FigureCanvasWebAgg(core.FigureCanvasWebAggCore):
52-
pass
53-
54-
5551
class FigureManagerWebAgg(core.FigureManagerWebAgg):
5652
_toolbar2_class = core.NavigationToolbar2WebAgg
5753

5854

55+
class FigureCanvasWebAgg(core.FigureCanvasWebAggCore):
56+
manager_class = FigureManagerWebAgg
57+
58+
5959
class WebAggApplication(tornado.web.Application):
6060
initialized = False
6161
started = False

lib/matplotlib/backends/backend_wx.py

Lines changed: 21 additions & 18 deletions
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

@@ -418,6 +427,7 @@ class _FigureCanvasWxBase(FigureCanvasBase, wx.Panel):
418427

419428
required_interactive_framework = "wx"
420429
_timer_cls = TimerWx
430+
manager_class = _api.classproperty(lambda cls: FigureManagerWx)
421431

422432
keyvald = {
423433
wx.WXK_CONTROL: 'control',
@@ -970,6 +980,17 @@ def __init__(self, canvas, num, frame):
970980
self.frame = self.window = frame
971981
super().__init__(canvas, num)
972982

983+
@classmethod
984+
def create_with_canvas(cls, canvas_class, figure, num):
985+
# docstring inherited
986+
wxapp = wx.GetApp() or _create_wxapp()
987+
frame = FigureFrameWx(num, figure, canvas_class=canvas_class)
988+
manager = figure.canvas.manager
989+
if mpl.is_interactive():
990+
manager.frame.Show()
991+
figure.canvas.draw_idle()
992+
return manager
993+
973994
def show(self):
974995
# docstring inherited
975996
self.frame.Show()
@@ -1344,24 +1365,6 @@ class _BackendWx(_Backend):
13441365
FigureCanvas = FigureCanvasWx
13451366
FigureManager = FigureManagerWx
13461367

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-
13651368
@staticmethod
13661369
def mainloop():
13671370
if not wx.App.IsMainLoopRunning():

0 commit comments

Comments
 (0)
0