Skip to content

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

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

Merged
merged 1 commit into from
May 1, 2022
Merged
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
41 changes: 34 additions & 7 deletions lib/matplotlib/backend_bases.py
Original file line number Diff line number Diff line change
Expand Up @@ -1580,6 +1580,13 @@ class FigureCanvasBase:
# interactive framework is required, or None otherwise.
required_interactive_framework = None

# The manager class instantiated by new_manager.
# (This is defined as a classproperty because the manager class is
# currently defined *after* the canvas class, but one could also assign
# ``FigureCanvasBase.manager_class = FigureManagerBase``
# after defining both classes.)
manager_class = _api.classproperty(lambda cls: FigureManagerBase)

events = [
'resize_event',
'draw_event',
Expand Down Expand Up @@ -1662,6 +1669,19 @@ 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.

Notes
-----
This method should not be reimplemented in subclasses. If
custom manager creation logic is needed, please reimplement
``FigureManager.create_with_canvas``.
"""
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 +2779,16 @@ 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):
"""
Create a manager for a given *figure* using a specific *canvas_class*.

Backends should override this method if they have specific needs for
setting up the canvas or the manager.
"""
return cls(canvas_class(figure), num)

def show(self):
"""
For GUI backends, show the figure window and redraw.
Expand Down Expand Up @@ -3225,11 +3255,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 +3486,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
77 changes: 38 additions & 39 deletions lib/matplotlib/backends/_backend_tk.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ def _on_timer(self):

class FigureCanvasTk(FigureCanvasBase):
required_interactive_framework = "tk"
manager_class = _api.classproperty(lambda cls: FigureManagerTk)

def __init__(self, figure=None, master=None):
super().__init__(figure)
Expand Down Expand Up @@ -431,6 +432,43 @@ def __init__(self, canvas, num, window):

self._shown = False

@classmethod
def create_with_canvas(cls, canvas_class, 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 = canvas_class(figure, master=window)
manager = cls(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 +996,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 = _api.classproperty(lambda cls: 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 = _api.classproperty(lambda cls: FigureManagerGTK4)
_context_is_scaled = False

def __init__(self, figure=None):
Expand Down
3 changes: 2 additions & 1 deletion lib/matplotlib/backends/backend_macosx.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import matplotlib as mpl
from matplotlib import cbook
from matplotlib import _api, cbook
from matplotlib._pylab_helpers import Gcf
from . import _macosx
from .backend_agg import FigureCanvasAgg
Expand All @@ -25,6 +25,7 @@ class FigureCanvasMac(_macosx.FigureCanvas, FigureCanvasAgg):

required_interactive_framework = "macosx"
_timer_cls = TimerMac
manager_class = _api.classproperty(lambda cls: FigureManagerMac)

def __init__(self, figure):
FigureCanvasBase.__init__(self, figure)
Expand Down
32 changes: 16 additions & 16 deletions lib/matplotlib/backends/backend_nbagg.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,21 @@ def __init__(self, canvas, num):
self._shown = False
super().__init__(canvas, num)

@classmethod
def create_with_canvas(cls, canvas_class, figure, num):
canvas = canvas_class(figure)
manager = cls(canvas, num)
if is_interactive():
manager.show()
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 +158,7 @@ def remove_comm(self, comm_id):


class FigureCanvasNbAgg(FigureCanvasWebAggCore):
pass
manager_class = FigureManagerNbAgg


class CommSocket:
Expand Down Expand Up @@ -228,21 +243,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 = _api.classproperty(lambda cls: FigureManagerQT)

buttond = {
getattr(_enum("QtCore.Qt.MouseButton"), k): v for k, v in [
Expand Down
18 changes: 10 additions & 8 deletions lib/matplotlib/backends/backend_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,14 @@ def new_figure_manager_given_figure(num, figure):
return manager


class FigureManagerTemplate(FigureManagerBase):
"""
Helper class for pyplot mode, wraps everything up into a neat bundle.

For non-interactive backends, the base class is sufficient.
"""


class FigureCanvasTemplate(FigureCanvasBase):
"""
The canvas the figure renders into. Calls the draw and print fig
Expand All @@ -191,6 +199,8 @@ class methods button_press_event, button_release_event,
A high-level Figure instance
"""

manager_class = FigureManagerTemplate

def draw(self):
"""
Draw the figure using the renderer.
Expand Down Expand Up @@ -227,14 +237,6 @@ def get_default_filetype(self):
return 'foo'


class FigureManagerTemplate(FigureManagerBase):
"""
Helper class for pyplot mode, wraps everything up into a neat bundle.

For non-interactive backends, the base class is sufficient.
"""


########################################################################
#
# Now just provide the standard names that backend.__init__ is expecting
Expand Down
8 changes: 4 additions & 4 deletions lib/matplotlib/backends/backend_webagg.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,14 @@ def run(self):
webagg_server_thread = ServerThread()


class FigureCanvasWebAgg(core.FigureCanvasWebAggCore):
pass


class FigureManagerWebAgg(core.FigureManagerWebAgg):
_toolbar2_class = core.NavigationToolbar2WebAgg


class FigureCanvasWebAgg(core.FigureCanvasWebAggCore):
manager_class = FigureManagerWebAgg


class WebAggApplication(tornado.web.Application):
initialized = False
started = False
Expand Down
39 changes: 21 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 = _api.classproperty(lambda cls: FigureManagerWx)

keyvald = {
wx.WXK_CONTROL: 'control',
Expand Down Expand Up @@ -970,6 +980,17 @@ 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):
# docstring inherited
wxapp = wx.GetApp() or _create_wxapp()
frame = FigureFrameWx(num, figure, canvas_class=canvas_class)
manager = figure.canvas.manager
if mpl.is_interactive():
manager.frame.Show()
figure.canvas.draw_idle()
return manager

def show(self):
# docstring inherited
self.frame.Show()
Expand Down Expand Up @@ -1344,24 +1365,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