diff --git a/.travis.yml b/.travis.yml
index 9342444f8fca..3ad28f824cb2 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -27,6 +27,9 @@ addons:
- graphviz
- libgeos-dev
- otf-freefont
+ - libpangocairo-1.0-0
+ - libgirepository-1.0-1
+ - gir1.2-gtk-3.0
# - fonts-humor-sans
# sources:
# - debian-sid
@@ -112,7 +115,7 @@ install:
pip install --upgrade setuptools
- |
# Install dependencies from pypi
- pip install $PRE python-dateutil $NUMPY pyparsing!=2.1.6 $PANDAS pep8 cycler coveralls coverage
+ pip install $PRE python-dateutil $NUMPY pyparsing!=2.1.6 $PANDAS pep8 cycler coveralls coverage pgi cairocffi
pip install $PRE -r doc-requirements.txt
# Install nose from a build which has partial
@@ -160,7 +163,7 @@ script:
if [[ $TRAVIS_OS_NAME == 'osx' ]]; then
python tests.py $NOSE_ARGS $TEST_ARGS
else
- gdb -return-child-result -batch -ex r -ex bt --args python $PYTHON_ARGS tests.py $NOSE_ARGS $TEST_ARGS
+ xvfb-run gdb -return-child-result -batch -ex r -ex bt --args python $PYTHON_ARGS tests.py $NOSE_ARGS $TEST_ARGS
fi
else
echo The following args are passed to pytest $PYTEST_ARGS
diff --git a/INSTALL b/INSTALL
index aade12aab122..0fabeb3c5ccf 100644
--- a/INSTALL
+++ b/INSTALL
@@ -235,6 +235,10 @@ backends and the capabilities they provide.
:term:`pyqt` 4.4 or later
The Qt4 widgets library python wrappers for the Qt4Agg backend
+:term:`PyGObject` or `pgi`
+ For Gtk3, MPL requires the installation of a GObject introspection library
+ for python, either `PyGObject` (also known as gi) or `pgi`.
+
:term:`pygtk` 2.4 or later
The python wrappers for the GTK widgets library for use with the
GTK or GTKAgg backend
diff --git a/doc/glossary/index.rst b/doc/glossary/index.rst
index 5f0b683f14cf..ac77926b4952 100644
--- a/doc/glossary/index.rst
+++ b/doc/glossary/index.rst
@@ -64,6 +64,17 @@ Glossary
channel. PDF was designed in part as a next-generation document
format to replace postscript
+ pgi
+ `pgi ` exists as a relatively new
+ python wrapper to GTK3 and acts as a pure python alternative to PyGObject.
+ pgi still exists in its infancy, currently missing many features of
+ PyGObject. However matplotlib does not use any of these missing features.
+
+ PyGObject
+ Like :term:`pygtk`, `PyGObject ` provides
+ python wrappers for the :term:`GTK` widgets library; unlike pygtk,
+ PyGObject wraps GTK3 instead of the now obsolete GTK2.
+
pygtk
`pygtk `_ provides python wrappers for
the :term:`GTK` widgets library for use with the GTK or GTKAgg
diff --git a/doc/users/whats_new/2015-07-30_pgi.rst b/doc/users/whats_new/2015-07-30_pgi.rst
new file mode 100644
index 000000000000..08d303d84280
--- /dev/null
+++ b/doc/users/whats_new/2015-07-30_pgi.rst
@@ -0,0 +1,9 @@
+PGI - Pure Python GObject Introspection Bindings
+------------------------------------------------
+
+For the GTK3 backend, matplotlib now supports PGI bindings as an alternative
+to PyGObject. By default matplotlib will still use PyGObject, otherwise it
+will look for pgi. You can change this behaviour through the rcParam
+backend.gi_preference which takes either a string, or a list of strings in
+order of preference.
+
diff --git a/lib/matplotlib/__init__.py b/lib/matplotlib/__init__.py
index 16969e5545dd..a52616331a51 100644
--- a/lib/matplotlib/__init__.py
+++ b/lib/matplotlib/__init__.py
@@ -1496,6 +1496,7 @@ def _jupyter_nbextension_paths():
'matplotlib.tests.test_backend_ps',
'matplotlib.tests.test_backend_qt4',
'matplotlib.tests.test_backend_qt5',
+ 'matplotlib.tests.test_backend_gtk3',
'matplotlib.tests.test_backend_svg',
'matplotlib.tests.test_basic',
'matplotlib.tests.test_bbox_tight',
diff --git a/lib/matplotlib/backends/backend_cairo.py b/lib/matplotlib/backends/backend_cairo.py
index c567a5a2c34c..3e5ef9611cd4 100644
--- a/lib/matplotlib/backends/backend_cairo.py
+++ b/lib/matplotlib/backends/backend_cairo.py
@@ -29,25 +29,7 @@
def _fn_name(): return sys._getframe(1).f_code.co_name
-try:
- import cairocffi as cairo
-except ImportError:
- try:
- import cairo
- except ImportError:
- raise ImportError("Cairo backend requires that cairocffi or pycairo is installed.")
- else:
- HAS_CAIRO_CFFI = False
-else:
- HAS_CAIRO_CFFI = True
-
-_version_required = (1,2,0)
-if cairo.version_info < _version_required:
- raise ImportError ("Pycairo %d.%d.%d is installed\n"
- "Pycairo %d.%d.%d or later is required"
- % (cairo.version_info + _version_required))
-backend_version = cairo.version
-del _version_required
+from .cairo_compat import cairo, HAS_CAIRO_CFFI
from matplotlib.backend_bases import RendererBase, GraphicsContextBase,\
FigureManagerBase, FigureCanvasBase
diff --git a/lib/matplotlib/backends/backend_gtk3.py b/lib/matplotlib/backends/backend_gtk3.py
index 1aee5c75f590..870b239278a5 100644
--- a/lib/matplotlib/backends/backend_gtk3.py
+++ b/lib/matplotlib/backends/backend_gtk3.py
@@ -6,25 +6,7 @@
import os, sys
def fn_name(): return sys._getframe(1).f_code.co_name
-try:
- import gi
-except ImportError:
- raise ImportError("Gtk3 backend requires pygobject to be installed.")
-
-try:
- gi.require_version("Gtk", "3.0")
-except AttributeError:
- raise ImportError(
- "pygobject version too old -- it must have require_version")
-except ValueError:
- raise ImportError(
- "Gtk3 backend requires the GObject introspection bindings for Gtk 3 "
- "to be installed.")
-
-try:
- from gi.repository import Gtk, Gdk, GObject, GLib
-except ImportError:
- raise ImportError("Gtk3 backend requires pygobject to be installed.")
+from .gtk3_compat import Gtk, Gdk, GObject, GLib
import matplotlib
from matplotlib._pylab_helpers import Gcf
@@ -167,6 +149,12 @@ class FigureCanvasGTK3 (Gtk.DrawingArea, FigureCanvasBase):
65421 : 'enter',
}
+ modifier_keys = [
+ (Gdk.ModifierType.MOD4_MASK, 'super'),
+ (Gdk.ModifierType.MOD1_MASK, 'alt'),
+ (Gdk.ModifierType.CONTROL_MASK, 'ctrl'),
+ ]
+
# Setting this as a static constant prevents
# this resulting expression from leaking
event_mask = (Gdk.EventMask.BUTTON_PRESS_MASK |
@@ -293,12 +281,7 @@ def _get_key(self, event):
else:
key = None
- modifiers = [
- (Gdk.ModifierType.MOD4_MASK, 'super'),
- (Gdk.ModifierType.MOD1_MASK, 'alt'),
- (Gdk.ModifierType.CONTROL_MASK, 'ctrl'),
- ]
- for key_mask, prefix in modifiers:
+ for key_mask, prefix in self.modifier_keys:
if event.state & key_mask:
key = '{0}+{1}'.format(prefix, key)
diff --git a/lib/matplotlib/backends/backend_gtk3agg.py b/lib/matplotlib/backends/backend_gtk3agg.py
index c3eb1da68be3..e8cbb5373257 100644
--- a/lib/matplotlib/backends/backend_gtk3agg.py
+++ b/lib/matplotlib/backends/backend_gtk3agg.py
@@ -9,7 +9,7 @@
from . import backend_agg
from . import backend_gtk3
-from .backend_cairo import cairo, HAS_CAIRO_CFFI
+from .cairo_compat import cairo, HAS_CAIRO_CFFI
from matplotlib.figure import Figure
from matplotlib import transforms
@@ -46,7 +46,7 @@ def on_draw_event(self, widget, ctx):
else:
bbox_queue = self._bbox_queue
- if HAS_CAIRO_CFFI:
+ if HAS_CAIRO_CFFI and not isinstance(ctx, cairo.Context):
ctx = cairo.Context._from_pointer(
cairo.ffi.cast('cairo_t **',
id(ctx) + object.__basicsize__)[0],
diff --git a/lib/matplotlib/backends/backend_gtk3cairo.py b/lib/matplotlib/backends/backend_gtk3cairo.py
index da8f099be7f6..fe5d2d6192bf 100644
--- a/lib/matplotlib/backends/backend_gtk3cairo.py
+++ b/lib/matplotlib/backends/backend_gtk3cairo.py
@@ -5,12 +5,12 @@
from . import backend_gtk3
from . import backend_cairo
-from .backend_cairo import cairo, HAS_CAIRO_CFFI
+from .cairo_compat import cairo, HAS_CAIRO_CFFI
from matplotlib.figure import Figure
class RendererGTK3Cairo(backend_cairo.RendererCairo):
def set_context(self, ctx):
- if HAS_CAIRO_CFFI:
+ if HAS_CAIRO_CFFI and not isinstance(ctx, cairo.Context):
ctx = cairo.Context._from_pointer(
cairo.ffi.cast(
'cairo_t **',
diff --git a/lib/matplotlib/backends/cairo_compat.py b/lib/matplotlib/backends/cairo_compat.py
new file mode 100644
index 000000000000..0d6215beaaf7
--- /dev/null
+++ b/lib/matplotlib/backends/cairo_compat.py
@@ -0,0 +1,20 @@
+try:
+ import cairocffi as cairo
+except ImportError:
+ try:
+ import cairo
+ except ImportError:
+ raise ImportError(
+ "Cairo backend requires that cairocffi or pycairo is installed.")
+ else:
+ HAS_CAIRO_CFFI = False
+else:
+ HAS_CAIRO_CFFI = True
+
+_version_required = (1, 2, 0)
+if cairo.version_info < _version_required:
+ raise ImportError("Pycairo %d.%d.%d is installed\n"
+ "Pycairo %d.%d.%d or later is required"
+ % (cairo.version_info + _version_required))
+backend_version = cairo.version
+del _version_required
diff --git a/lib/matplotlib/backends/gtk3_compat.py b/lib/matplotlib/backends/gtk3_compat.py
new file mode 100644
index 000000000000..c3847041d4f6
--- /dev/null
+++ b/lib/matplotlib/backends/gtk3_compat.py
@@ -0,0 +1,34 @@
+import matplotlib
+
+error_msg_gtk3 = "Gtk3 backend requires the installation of pygobject or pgi."
+
+# Import the first library that works from the rcParam list
+# throw ImportError if none works
+for lib in matplotlib.rcParams['backend.gi_preference']:
+ try:
+ gi = __import__(lib, globals(), locals(), [], 0)
+ break
+ except ImportError:
+ pass
+else:
+ raise ImportError(error_msg_gtk3)
+
+# Check version
+try:
+ gi.require_version("Gtk", "3.0")
+except AttributeError:
+ raise ImportError(
+ "pygobject version too old -- it must have require_version")
+except ValueError:
+ raise ImportError(
+ "Gtk3 backend requires the installation of GObject introspection "
+ "bindings for Gtk 3")
+
+# cleanly import pkgs to global scope
+try:
+ pkgs = ['Gtk', 'Gdk', 'GObject', 'GLib']
+ name = gi.__name__ + '.repository'
+ _temp = __import__(name, globals(), locals(), pkgs, 0)
+ globals().update(dict((k, getattr(_temp, k)) for k in pkgs))
+except (ImportError, AttributeError):
+ raise ImportError(error_msg_gtk3)
diff --git a/lib/matplotlib/rcsetup.py b/lib/matplotlib/rcsetup.py
index fcbd7adfe313..8a0c81eb406a 100644
--- a/lib/matplotlib/rcsetup.py
+++ b/lib/matplotlib/rcsetup.py
@@ -410,6 +410,9 @@ def deprecate_axes_colorcycle(value):
validate_stringlist = _listify_validator(six.text_type)
validate_stringlist.__doc__ = 'return a list'
+validate_gi_preference = _listify_validator(ValidateInStrings(
+ 'backend.gi_preference', ['gi', 'pgi']))
+
validate_orientation = ValidateInStrings(
'orientation', ['landscape', 'portrait'])
@@ -888,6 +891,7 @@ def validate_animation_writer_path(p):
'backend_fallback': [True, validate_bool], # agg is certainly present
'backend.qt4': ['PyQt4', validate_qt4],
'backend.qt5': ['PyQt5', validate_qt5],
+ 'backend.gi_preference': [['gi', 'pgi'], validate_gi_preference],
'webagg.port': [8988, validate_int],
'webagg.open_in_browser': [True, validate_bool],
'webagg.port_retries': [50, validate_int],
diff --git a/lib/matplotlib/tests/test_backend_gtk3.py b/lib/matplotlib/tests/test_backend_gtk3.py
new file mode 100644
index 000000000000..47953432f7e6
--- /dev/null
+++ b/lib/matplotlib/tests/test_backend_gtk3.py
@@ -0,0 +1,60 @@
+from __future__ import (absolute_import, division, print_function,
+ unicode_literals)
+
+from matplotlib.externals import six
+from matplotlib.externals.six import unichr
+from matplotlib import pyplot as plt
+from matplotlib.testing.decorators import cleanup, switch_backend
+from matplotlib.testing.decorators import knownfailureif
+from matplotlib._pylab_helpers import Gcf
+import copy
+
+try:
+ # mock in python 3.3+
+ from unittest import mock
+except ImportError:
+ import mock
+
+try:
+ from matplotlib.backends.gtk3_compat import Gtk, Gdk, GObject, GLib
+ HAS_GTK3 = True
+except ImportError:
+ HAS_GTK3 = False
+
+
+def simulate_key_press(canvas, key, modifiers=[]):
+ event = mock.Mock()
+
+ keyval = [k for k, v in six.iteritems(canvas.keyvald) if v == key]
+ if keyval:
+ keyval = keyval[0]
+ else:
+ keyval = ord(key)
+ event.keyval = keyval
+
+ event.state = 0
+ for key_mask, prefix in canvas.modifier_keys:
+ if prefix in modifiers:
+ event.state |= key_mask
+
+ canvas.key_press_event(None, event)
+
+
+@cleanup
+#@knownfailureif(not HAS_GTK3)
+@switch_backend('GTK3Agg')
+def test_fig_close():
+ #save the state of Gcf.figs
+ init_figs = copy.copy(Gcf.figs)
+
+ # make a figure using pyplot interface
+ fig = plt.figure()
+
+ # simulate user pressing the close shortcut
+ #simulate_key_press(fig.canvas, 'w', ['ctrl'])
+
+ plt.show()
+
+ # assert that we have removed the reference to the FigureManager
+ # that got added by plt.figure()
+ assert(init_figs == Gcf.figs)