8000 Merge pull request #22925 from anntzer/uniform-manager-creation-2 · matplotlib/matplotlib@1ff14f1 · GitHub
[go: up one dir, main page]

Skip to content

Commit 1ff14f1

Browse files
authored
Merge pull request #22925 from anntzer/uniform-manager-creation-2
Standardize creation of FigureManager from a given FigureCanvas class.
2 parents 7c19d85 + f29c3e0 commit 1ff14f1

10 files changed

+128
-93
lines changed

lib/matplotlib/backend_bases.py

Lines changed: 34 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,19 @@ 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+
Notes
1678+
-----
1679+
This method should not be reimplemented in subclasses. If
1680+
custom manager creation logic is needed, please reimplement
1681+
``FigureManager.create_with_canvas``.
1682+
"""
1683+
return cls.manager_class.create_with_canvas(cls, figure, num)
1684+
16651685
@contextmanager
16661686
def _idle_draw_cntx(self):
16671687
self._is_idle_drawing = True
@@ -2759,6 +2779,16 @@ def notify_axes_change(fig):
27592779
if self.toolmanager is None and self.toolbar is not None:
27602780
self.toolbar.update()
27612781

2782+
@classmethod
2783+
def create_with_canvas(cls, canvas_class, figure, num):
2784+
"""
2785+
Create a manager for a given *figure* using a specific *canvas_class*.
2786+
2787+
Backends should override this method if they have specific needs for
2788+
setting up the canvas or the manager.
2789+
"""
2790+
return cls(canvas_class(figure), num)
2791+
27622792
def show(self):
27632793
"""
27642794
For GUI backends, show the figure window and redraw.
@@ -3225,11 +3255,10 @@ def configure_subplots(self, *args):
32253255
if hasattr(self, "subplot_tool"):
32263256
self.subplot_tool.figure.canvas.manager.show()
32273257
return
3228-
plt = _safe_pyplot_import()
3258+
# This import needs to happen here due to circular imports.
3259+
from matplotlib.figure import Figure
32293260
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))
3261+
manager = type(self.canvas).new_manager(Figure(figsize=(6, 3)), -1)
32333262
manager.set_window_title("Subplot configuration tool")
32343263
tool_fig = manager.canvas.figure
32353264
tool_fig.subplots_adjust(top=0.9)
@@ -3457,9 +3486,7 @@ def new_figure_manager(cls, num, *args, **kwargs):
34573486
@classmethod
34583487
def new_figure_manager_given_figure(cls, num, figure):
34593488
"""Create a new figure manager instance for the given figure."""
3460-
canvas = cls.FigureCanvas(figure)
3461-
manager = cls.FigureManager(canvas, num)
3462-
return manager
3489+
return cls.FigureCanvas.new_manager(figure, num)
34633490

34643491
@classmethod
34653492
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)
@@ -433,6 +434,43 @@ def __init__(self, canvas, num, window):
433434

434435
self._shown = False
435436

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

969-
@classmethod
970-
def new_figure_manager_given_figure(cls, num, figure):
971-
"""
972-
Create a new figure manager instance for the given figure.
973-
"""
974-
with _restore_foreground_window_at_end():
975-
if cbook._get_running_interactive_framework() is None:
976-
cbook._setup_new_guiapp()
977-
_c_internal_utils.Win32_SetProcessDpiAwareness_max()
978-
window = tk.Tk(className="matplotlib")
979-
window.withdraw()
980-
981-
# Put a Matplotlib icon on the window rather than the default tk
982-
# icon. See https://www.tcl.tk/man/tcl/TkCmd/wm.html#M50
983-
#
984-
# `ImageTk` can be replaced with `tk` whenever the minimum
985-
# supported Tk version is increased to 8.6, as Tk 8.6+ natively
986-
# supports PNG images.
987-
icon_fname = str(cbook._get_data_path(
988-
'images/matplotlib.png'))
989-
icon_img = ImageTk.PhotoImage(file=icon_fname, master=window)
990-
991-
icon_fname_large = str(cbook._get_data_path(
992-
'images/matplotlib_large.png'))
993-
icon_img_large = ImageTk.PhotoImage(
994-
file=icon_fname_large, master=window)
995-
try:
996-
window.iconphoto(False, icon_img_large, icon_img)
997-
except Exception as exc:
998-
# log the failure (due e.g. to Tk version), but carry on
999-
_log.info('Could not load matplotlib icon: %s', exc)
1000-
1001-
canvas = cls.FigureCanvas(figure, master=window)
1002-
manager = cls.FigureManager(canvas, num, window)
1003-
if mpl.is_interactive():
1004-
manager.show()
1005-
canvas.draw_idle()
1006-
return manager
1007-
10081007
@staticmethod
10091008
def mainloop():
10101009
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
@@ -68,6 +68,21 @@ def __init__(self, canvas, num):
6868
self._shown = False
6969
super().__init__(canvas, num)
7070

71+
@classmethod
72+
def create_with_canvas(cls, canvas_class, figure, num):
73+
canvas = canvas_class(figure)
74+
manager = cls(canvas, num)
75+
if is_interactive():
76+
manager.show()
77+
canvas.draw_idle()
78+
79+
def destroy(event):
80+
canvas.mpl_disconnect(cid)
81+
Gcf.destroy(manager)
82+
83+
cid = canvas.mpl_connect('close_event', destroy)
84+
return manager
85+
7186
def display_js(self):
7287
# XXX How to do this just once? It has to deal with multiple
7388
# browser instances using the same kernel (require.js - but the
@@ -133,7 +148,7 @@ def remove_comm(self, comm_id):
133148

134149

135150
class FigureCanvasNbAgg(FigureCanvasWebAggCore):
136-
pass
151+
manager_class = FigureManagerNbAgg
137152

138153

139154
class CommSocket:
@@ -218,21 +233,6 @@ class _BackendNbAgg(_Backend):
218233
FigureCanvas = FigureCanvasNbAgg
219234
FigureManager = FigureManagerNbAgg
220235

221-
@staticmethod
222-
def new_figure_manager_given_figure(num, figure):
223-
canvas = FigureCanvasNbAgg(figure)
224-
manager = FigureManagerNbAgg(canvas, num)
225-
if is_interactive():
226-
manager.show()
227-
figure.canvas.draw_idle()
228-
229-
def destroy(event):
230-
canvas.mpl_disconnect(cid)
231-
Gcf.destroy(manager)
232-
233-
cid = canvas.mpl_connect('close_event', destroy)
234-
return manager
235-
236236
@staticmethod
237237
def show(block=None):
238238
## 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