Skip to content
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

Backend refactor qt #5

Open
wants to merge 10 commits into
base: backend-refactor
Choose a base branch
from
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