Skip to content
59 changes: 58 additions & 1 deletion lib/matplotlib/_pylab_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
import gc
import atexit

from matplotlib import is_interactive


def error_msg(msg):
print(msg, file=sys.stderr)
Expand All @@ -35,6 +37,16 @@ class Gcf(object):
_activeQue = []
figs = {}

@classmethod
def add_figure_manager(cls, manager):
cls.figs[manager.num] = manager
try: # TODO remove once all backends converted to use the new manager.
manager.mpl_connect('window_destroy_event', cls.destroy_cbk)
except:
pass

cls.set_active(manager)

@classmethod
def get_fig_manager(cls, num):
"""
Expand All @@ -46,6 +58,49 @@ def get_fig_manager(cls, num):
cls.set_active(manager)
return manager

@classmethod
def show_all(cls, block=None):
"""
Show all figures. If *block* is not None, then
it is a boolean that overrides all other factors
determining whether show blocks by calling mainloop().
The other factors are:
it does not block if run inside ipython's "%pylab" mode
it does not block in interactive mode.
"""
managers = cls.get_all_fig_managers()
if not managers:
return

for manager in managers:
manager.show()

if block is not None:
if block:
manager.mainloop()
return

from matplotlib import pyplot
try:
ipython_pylab = not pyplot.show._needmain
# IPython versions >= 0.10 tack the _needmain
# attribute onto pyplot.show, and always set
# it to False, when in %pylab mode.
ipython_pylab = ipython_pylab and get_backend() != 'WebAgg'
# TODO: The above is a hack to get the WebAgg backend
# working with ipython's `%pylab` mode until proper
# integration is implemented.
except AttributeError:
ipython_pylab = False

# Leave the following as a separate step in case we
# want to control this behavior with an rcParam.
if ipython_pylab:
block = False

if not is_interactive() or get_backend() == 'WebAgg':
manager.mainloop()

@classmethod
def destroy(cls, num):
"""
Expand Down Expand Up @@ -134,7 +189,9 @@ def set_active(cls, manager):
if m != manager:
cls._activeQue.append(m)
cls._activeQue.append(manager)
cls.figs[manager.num] = manager

@classmethod
def destroy_cbk(cls, event):
cls.destroy(event.figure_manager.num)

atexit.register(Gcf.destroy_all)
107 changes: 105 additions & 2 deletions lib/matplotlib/backend_bases.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import warnings
import time
import io
import weakref

import numpy as np
import matplotlib.cbook as cbook
Expand Down Expand Up @@ -132,6 +133,33 @@ def get_registered_canvas_class(format):
return backend_class


class MainLoopBase(object):
"""This gets used as a key maintaining the event loop.
Backends should only need to override begin and end.
It should not matter if this gets used as a singleton or not due to
clever magic.
"""
_instance_count = {}
def __init__(self):
MainLoopBase._instance_count.setdefault(self.__class__, 0)
MainLoopBase._instance_count[self.__class__] += 1

def begin(self):
pass

def end(self):
pass

def __call__(self):
self.begin()

def __del__(self):
MainLoopBase._instance_count[self.__class__] -= 1
if (MainLoopBase._instance_count[self.__class__] <= 0 and
not is_interactive()):
self.end()


class ShowBase(object):
"""
Simple base class to generate a show() callable in backends.
Expand Down Expand Up @@ -1664,7 +1692,7 @@ class FigureCanvasBase(object):
register_backend('tiff', 'matplotlib.backends.backend_agg',
'Tagged Image File Format')

def __init__(self, figure):
def __init__(self, figure, manager=None):
figure.set_canvas(self)
self.figure = figure
# a dictionary from event name to a dictionary that maps cid->func
Expand All @@ -1678,6 +1706,7 @@ def __init__(self, figure):
self.mouse_grabber = None # the axes currently grabbing mouse
self.toolbar = None # NavigationToolbar2 will set me
self._is_saving = False
self.manager = manager

def is_saving(self):
"""
Expand Down Expand Up @@ -2422,6 +2451,19 @@ def stop_event_loop_default(self):
"""
self._looping = False

def destroy(self):
pass

@property
def manager(self):
if self._manager is not None:
return self._manager()

@manager.setter
def manager(self, manager):
if manager is not None:
self._manager = weakref.ref(manager)


def key_press_handler(event, canvas, toolbar=None):
"""
Expand Down Expand Up @@ -2461,7 +2503,10 @@ def key_press_handler(event, canvas, toolbar=None):

# quit the figure (defaut key 'ctrl+w')
if event.key in quit_keys:
Gcf.destroy_fig(canvas.figure)
if isinstance(canvas.manager.mainloop, MainLoopBase): # If new no Gcf.
canvas.manager._destroy('window_destroy_event')
else:
Gcf.destroy_fig(canvas.figure)

if toolbar is not None:
# home or reset mnemonic (default key 'h', 'home' and 'r')
Expand Down Expand Up @@ -2537,6 +2582,64 @@ class NonGuiException(Exception):
pass


class WindowEvent(object):
def __init__(self, name, window):
self.name = name
self.window = window


class WindowBase(cbook.EventEmitter):
def __init__(self, title):
cbook.EventEmitter.__init__(self)

def show(self):
"""
For GUI backends, show the figure window and redraw.
For non-GUI backends, raise an exception to be caught
by :meth:`~matplotlib.figure.Figure.show`, for an
optional warning.
"""
raise NonGuiException()

def destroy(self):
pass

def set_fullscreen(self, fullscreen):
pass

def set_default_size(self, w, h):
self.resize(w, h)

def resize(self, w, h):
""""For gui backends, resize the window (in pixels)."""
pass

def get_window_title(self):
"""
Get the title text of the window containing the figure.
Return None for non-GUI backends (e.g., a PS backend).
"""
return 'image'

def set_window_title(self, title):
"""
Set the title text of the window containing the figure. Note that
this has no effect for non-GUI backends (e.g., a PS backend).
"""
pass

def add_element_to_window(self, element, expand, fill, pad, side='bottom'):
""" Adds a gui widget to the window.
This has no effect for non-GUI backends
"""
pass

def destroy_event(self, *args):
s = 'window_destroy_event'
event = WindowEvent(s, self)
self._callbacks.process(s, event)


class FigureManagerBase(object):
"""
Helper class for pyplot mode, wraps everything up into a neat bundle
Expand Down
134 changes: 134 additions & 0 deletions lib/matplotlib/backend_managers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
from matplotlib import is_interactive
from matplotlib import rcParams
from matplotlib.figure import Figure
from matplotlib import cbook
from matplotlib.backend_bases import key_press_handler
from matplotlib.backends import get_backends
(FigureCanvas, Window, Toolbar2, MainLoop,
old_new_figure_manager) = get_backends()


class FigureManagerEvent(object):
def __init__(self, s, fm):
self.name = s
self.figure_manager = fm


class FigureManager(cbook.EventEmitter):
def __init__(self, figure, num):
cbook.EventEmitter.__init__(self)
self.num = num

self.mainloop = MainLoop()
self.window = Window('Figure %d' % num)
self.window.mpl_connect('window_destroy_event', self._destroy)

self.canvas = FigureCanvas(figure, manager=self)

self.key_press_handler_id = self.canvas.mpl_connect('key_press_event',
self.key_press)

w = int(self.canvas.figure.bbox.width)
h = int(self.canvas.figure.bbox.height)

self.window.add_element_to_window(self.canvas, True, True, 0, 'top')

self.toolbar = self._get_toolbar()
if self.toolbar is not None:
h += self.window.add_element_to_window(self.toolbar,
False, False, 0, 'bottom')

self.window.set_default_size(w, h)

if is_interactive():
self.window.show()

def notify_axes_change(fig):
'this will be called whenever the current axes is changed'
if self.toolbar is not None:
self.toolbar.update()
self.canvas.figure.add_axobserver(notify_axes_change)

def key_press(self, event):
"""
Implement the default mpl key bindings defined at
:ref:`key-event-handling`
"""
key_press_handler(event, self.canvas, self.canvas.toolbar)

def _destroy(self, event=None):
# Callback from the when the window wants to destroy itself
s = 'window_destroy_event'
event = FigureManagerEvent(s, self)
self._callbacks.process(s, event)

def destroy(self, *args):
self.canvas.destroy()
if self.toolbar:
self.toolbar.destroy()
self.window.destroy()

self.mainloop.__del__()

def show(self):
self.window.show()

def full_screen_toggle(self):
self._full_screen_flag = not self._full_screen_flag
self.window.set_fullscreen(self._full_screen_flag)

def resize(self, w, h):
self.window.resize(w, h)

def get_window_title(self):
"""
Get the title text of the window containing the figure.
Return None for non-GUI backends (e.g., a PS backend).
"""
return self.window.get_window_title()

def set_window_title(self, title):
"""
Set the title text of the window containing the figure. Note that
this has no effect for non-GUI backends (e.g., a PS backend).
"""
self.window.set_window_title(title)

def _get_toolbar(self):
# must be inited after the window, drawingArea and figure
# attrs are set
if rcParams['toolbar'] == 'toolbar2':
# Short term hack until toolbar2 gets removed.
if 'qt' in str(FigureCanvas):
toolbar = Toolbar2(self.canvas, self.window, False)
else:
toolbar = Toolbar2(self.canvas, self.window)
else:
toolbar = None
return toolbar

def show_popup(self, msg):
"""
Display message in a popup -- GUI only
"""
pass


def new_figure_manager(num, *args, **kwargs):
"""
Create a new figure manager instance
"""
show = kwargs.pop('show', None)
if old_new_figure_manager is None: # Test if we can use the new code
FigureClass = kwargs.pop('FigureClass', Figure)
thisFig = FigureClass(*args, **kwargs)
manager = new_figure_manager_given_figure(num, thisFig)
else: # TODO remove once Gcf removed from backends. Default to old code.
manager = old_new_figure_manager(num, *args, **kwargs)
manager.mainloop = MainLoop
return manager


def new_figure_manager_given_figure(num, figure):
manager = FigureManager(figure, num)
return manager
Loading