diff --git a/.travis.yml b/.travis.yml index 42d78cabdec..8faea410a30 100644 --- a/.travis.yml +++ b/.travis.yml @@ -118,21 +118,25 @@ install: pyparsing!=2.1.6 \ python-dateutil \ sphinx - # GUI toolkits are pip-installable only for some versions of Python so - # don't fail if we can't install them. Make it easier to check whether the - # install was successful by trying to import the toolkit (sometimes, the - # install appears to be successful but shared libraries cannot be loaded at - # runtime, so an actual import is a better check). - pip install pyqt5 && - python -c 'import PyQt5.QtCore' && - echo 'PyQt5 is available' || - echo 'PyQt5 is not available' - pip install -U --pre \ - -f https://wxpython.org/Phoenix/release-extras/linux/gtk3/ubuntu-14.04 \ - wxPython && - python -c 'import wx' && - echo 'wxPython is available' || - echo 'wxPython is not available' + # cairocffi and GUI toolkits are successfully installable for some versions + # of Python only, and will sometimes also fail (possibly with an OSError) + # at import time (e.g. due to missing shared libraries), so try to install + # them and check whether they are actually importable. If they don't, + # uninstall them, as an OSError confuses setupext.py. + _try_install () { + if pip install "$1" && python -c "import $2"; then + echo "$1 is available" + else + pip uninstall --yes "$1" || true + echo "$1 is not available" + fi + } + _try_install cairocffi cairocffi + _try_install PyQt5 PyQt5.QtCore + PIP_INSTALL_UPGRADE=true \ + PIP_INSTALL_PRE=true \ + PIP_INSTALL_FIND_LINKS=https://wxpython.org/Phoenix/release-extras/linux/gtk3/ubuntu-14.04 \ + _try_install wxPython wx # pytest-cov>=2.3.1 due to https://github.com/pytest-dev/pytest-cov/issues/124 pip install $PRE \ diff --git a/doc-requirements.txt b/doc-requirements.txt index e0e378a264d..56e1722ce93 100644 --- a/doc-requirements.txt +++ b/doc-requirements.txt @@ -9,6 +9,7 @@ sphinx>=1.3,!=1.5.0 colorspacious ipython +ipywidgets mock numpydoc pillow diff --git a/doc/api/backend_agg_api.rst b/doc/api/backend_agg_api.rst new file mode 100644 index 00000000000..40c8cd4bce6 --- /dev/null +++ b/doc/api/backend_agg_api.rst @@ -0,0 +1,8 @@ + +:mod:`matplotlib.backends.backend_agg` +====================================== + +.. automodule:: matplotlib.backends.backend_agg + :members: + :undoc-members: + :show-inheritance: diff --git a/doc/api/backend_cairo_api.rst b/doc/api/backend_cairo_api.rst new file mode 100644 index 00000000000..2623270c678 --- /dev/null +++ b/doc/api/backend_cairo_api.rst @@ -0,0 +1,8 @@ + +:mod:`matplotlib.backends.backend_cairo` +======================================== + +.. automodule:: matplotlib.backends.backend_cairo + :members: + :undoc-members: + :show-inheritance: diff --git a/doc/api/backend_gtk3agg_api.rst b/doc/api/backend_gtk3agg_api.rst new file mode 100644 index 00000000000..b05498dee7d --- /dev/null +++ b/doc/api/backend_gtk3agg_api.rst @@ -0,0 +1,11 @@ + +:mod:`matplotlib.backends.backend_gtk3agg` +========================================== + +**TODO** We'll add this later, importing the gtk3 backends requires an active +X-session, which is not compatible with cron jobs. + +.. .. automodule:: matplotlib.backends.backend_gtk3agg +.. :members: +.. :undoc-members: +.. :show-inheritance: diff --git a/doc/api/backend_gtk3cairo_api.rst b/doc/api/backend_gtk3cairo_api.rst new file mode 100644 index 00000000000..b805af75e75 --- /dev/null +++ b/doc/api/backend_gtk3cairo_api.rst @@ -0,0 +1,11 @@ + +:mod:`matplotlib.backends.backend_gtk3cairo` +============================================ + +**TODO** We'll add this later, importing the gtk3 backends requires an active +X-session, which is not compatible with cron jobs. + +.. .. automodule:: matplotlib.backends.backend_gtk3cairo +.. :members: +.. :undoc-members: +.. :show-inheritance: diff --git a/doc/api/backend_gtkcairo_api.rst b/doc/api/backend_gtkcairo_api.rst new file mode 100644 index 00000000000..562f8ea6e7c --- /dev/null +++ b/doc/api/backend_gtkcairo_api.rst @@ -0,0 +1,11 @@ + +:mod:`matplotlib.backends.backend_gtkcairo` +=========================================== + +**TODO** We'll add this later, importing the gtk backends requires an active +X-session, which is not compatible with cron jobs. + +.. .. automodule:: matplotlib.backends.backend_gtkcairo +.. :members: +.. :undoc-members: +.. :show-inheritance: diff --git a/doc/api/backend_managers_api.rst b/doc/api/backend_managers_api.rst index 86d1c383b96..faf4eda18de 100644 --- a/doc/api/backend_managers_api.rst +++ b/doc/api/backend_managers_api.rst @@ -1,6 +1,6 @@ :mod:`matplotlib.backend_managers` -=================================== +================================== .. automodule:: matplotlib.backend_managers :members: diff --git a/doc/api/backend_mixed_api.rst b/doc/api/backend_mixed_api.rst index 9c55e4abaa7..7457f6684f9 100644 --- a/doc/api/backend_mixed_api.rst +++ b/doc/api/backend_mixed_api.rst @@ -4,4 +4,5 @@ .. automodule:: matplotlib.backends.backend_mixed :members: + :undoc-members: :show-inheritance: diff --git a/doc/api/backend_nbagg_api.rst b/doc/api/backend_nbagg_api.rst new file mode 100644 index 00000000000..977eabce8db --- /dev/null +++ b/doc/api/backend_nbagg_api.rst @@ -0,0 +1,8 @@ + +:mod:`matplotlib.backends.backend_nbagg` +======================================== + +.. automodule:: matplotlib.backends.backend_nbagg + :members: + :undoc-members: + :show-inheritance: diff --git a/doc/api/backend_pdf_api.rst b/doc/api/backend_pdf_api.rst index 115863d6187..ded143ddcf8 100644 --- a/doc/api/backend_pdf_api.rst +++ b/doc/api/backend_pdf_api.rst @@ -4,4 +4,5 @@ .. automodule:: matplotlib.backends.backend_pdf :members: + :undoc-members: :show-inheritance: diff --git a/doc/api/backend_pgf_api.rst b/doc/api/backend_pgf_api.rst new file mode 100644 index 00000000000..ec7440080eb --- /dev/null +++ b/doc/api/backend_pgf_api.rst @@ -0,0 +1,8 @@ + +:mod:`matplotlib.backends.backend_pgf` +====================================== + +.. automodule:: matplotlib.backends.backend_pgf + :members: + :undoc-members: + :show-inheritance: diff --git a/doc/api/backend_ps_api.rst b/doc/api/backend_ps_api.rst new file mode 100644 index 00000000000..9d585be7a0a --- /dev/null +++ b/doc/api/backend_ps_api.rst @@ -0,0 +1,8 @@ + +:mod:`matplotlib.backends.backend_ps` +===================================== + +.. automodule:: matplotlib.backends.backend_ps + :members: + :undoc-members: + :show-inheritance: diff --git a/doc/api/backend_qt4agg_api.rst b/doc/api/backend_qt4agg_api.rst index 2e2e852612c..8bf490aa8cb 100644 --- a/doc/api/backend_qt4agg_api.rst +++ b/doc/api/backend_qt4agg_api.rst @@ -6,4 +6,3 @@ :members: :undoc-members: :show-inheritance: - diff --git a/doc/api/backend_qt4cairo_api.rst b/doc/api/backend_qt4cairo_api.rst new file mode 100644 index 00000000000..590465d7fbc --- /dev/null +++ b/doc/api/backend_qt4cairo_api.rst @@ -0,0 +1,8 @@ + +:mod:`matplotlib.backends.backend_qt4cairo` +=========================================== + +.. automodule:: matplotlib.backends.backend_qt4cairo + :members: + :undoc-members: + :show-inheritance: diff --git a/doc/api/backend_qt5agg_api.rst b/doc/api/backend_qt5agg_api.rst index 58e5353a32a..8d1ad2aba0f 100644 --- a/doc/api/backend_qt5agg_api.rst +++ b/doc/api/backend_qt5agg_api.rst @@ -6,4 +6,3 @@ :members: :undoc-members: :show-inheritance: - diff --git a/doc/api/backend_qt5cairo_api.rst b/doc/api/backend_qt5cairo_api.rst new file mode 100644 index 00000000000..73df7ac128a --- /dev/null +++ b/doc/api/backend_qt5cairo_api.rst @@ -0,0 +1,8 @@ + +:mod:`matplotlib.backends.backend_qt5cairo` +=========================================== + +.. automodule:: matplotlib.backends.backend_qt5cairo + :members: + :undoc-members: + :show-inheritance: diff --git a/doc/api/backend_svg_api.rst b/doc/api/backend_svg_api.rst index 399042482ea..0b26d11e881 100644 --- a/doc/api/backend_svg_api.rst +++ b/doc/api/backend_svg_api.rst @@ -4,4 +4,5 @@ .. automodule:: matplotlib.backends.backend_svg :members: + :undoc-members: :show-inheritance: diff --git a/doc/api/backend_tkagg_api.rst b/doc/api/backend_tkagg_api.rst new file mode 100644 index 00000000000..2a55bfe5c69 --- /dev/null +++ b/doc/api/backend_tkagg_api.rst @@ -0,0 +1,8 @@ + +:mod:`matplotlib.backends.backend_tkagg` +======================================== + +.. automodule:: matplotlib.backends.backend_tkagg + :members: + :undoc-members: + :show-inheritance: diff --git a/doc/api/backend_tools_api.rst b/doc/api/backend_tools_api.rst index 32babd5844b..7e3d5619cc3 100644 --- a/doc/api/backend_tools_api.rst +++ b/doc/api/backend_tools_api.rst @@ -1,6 +1,6 @@ :mod:`matplotlib.backend_tools` -================================ +=============================== .. automodule:: matplotlib.backend_tools :members: diff --git a/doc/api/backend_webagg_api.rst b/doc/api/backend_webagg_api.rst new file mode 100644 index 00000000000..50070c3fcb6 --- /dev/null +++ b/doc/api/backend_webagg_api.rst @@ -0,0 +1,12 @@ + +:mod:`matplotlib.backends.backend_webagg` +========================================= + +.. note:: + The WebAgg backend is not documented here, in order to avoid adding Tornado + to the doc build requirements. + +.. .. automodule:: matplotlib.backends.backend_webagg +.. :members: +.. :undoc-members: +.. :show-inheritance: diff --git a/doc/api/index_backend_api.rst b/doc/api/index_backend_api.rst index 8588628c7c0..813c3770214 100644 --- a/doc/api/index_backend_api.rst +++ b/doc/api/index_backend_api.rst @@ -8,12 +8,21 @@ backends backend_managers_api.rst backend_mixed_api.rst backend_tools_api.rst + backend_agg_api.rst + backend_cairo_api.rst backend_gtkagg_api.rst + backend_gtkcairo_api.rst + backend_gtk3agg_api.rst + backend_gtk3cairo_api.rst + backend_nbagg_api.rst + backend_pdf_api.rst + backend_pgf_api.rst + backend_ps_api.rst backend_qt4agg_api.rst + backend_qt4cairo_api.rst backend_qt5agg_api.rst - backend_wxagg_api.rst - backend_pdf_api.rst + backend_qt5cairo_api.rst backend_svg_api.rst -.. backend_webagg.rst - dviread.rst - type1font.rst + backend_tkagg_api.rst + backend_webagg_api.rst + backend_wxagg_api.rst diff --git a/doc/users/whats_new/qtcairo.rst b/doc/users/whats_new/qtcairo.rst new file mode 100644 index 00000000000..8f6f62f630a --- /dev/null +++ b/doc/users/whats_new/qtcairo.rst @@ -0,0 +1,5 @@ +Cairo rendering for Qt canvases +------------------------------- + +The new ``Qt4Cairo`` and ``Qt5Cairo`` backends allow Qt canvases to use Cairo +rendering instead of Agg. diff --git a/examples/showcase/mandelbrot.py b/examples/showcase/mandelbrot.py index ed43a087b18..e8a4366afac 100644 --- a/examples/showcase/mandelbrot.py +++ b/examples/showcase/mandelbrot.py @@ -67,10 +67,9 @@ def mandelbrot_set(xmin, xmax, ymin, ymax, xn, yn, maxiter, horizon=2.0): # Some advertisement for matplotlib year = time.strftime("%Y") - major, minor, micro = matplotlib.__version__.split('.', 2) text = ("The Mandelbrot fractal set\n" - "Rendered with matplotlib %s.%s, %s - http://matplotlib.org" - % (major, minor, year)) + "Rendered with matplotlib %s, %s - http://matplotlib.org" + % (matplotlib.__version__, year)) ax.text(xmin+.025, ymin+.025, text, color="white", fontsize=12, alpha=0.5) plt.show() diff --git a/lib/matplotlib/backends/backend_agg.py b/lib/matplotlib/backends/backend_agg.py index a811401380c..a398b58d5a5 100644 --- a/lib/matplotlib/backends/backend_agg.py +++ b/lib/matplotlib/backends/backend_agg.py @@ -415,17 +415,14 @@ def restore_region(self, region, bbox=None, xy=None): return renderer.restore_region(region, bbox, xy) def draw(self): - """ - Draw the figure using the renderer + """Draw the figure using the renderer. """ self.renderer = self.get_renderer(cleared=True) - # acquire a lock on the shared font cache - RendererAgg.lock.acquire() - - try: + with RendererAgg.lock: self.figure.draw(self.renderer) - finally: - RendererAgg.lock.release() + # A GUI class may be need to update a window using this draw, so don't + # forget to call the superclass. + super(FigureCanvasAgg, self).draw() def get_renderer(self, cleared=False): l, b, w, h = self.figure.bbox.bounds diff --git a/lib/matplotlib/backends/backend_cairo.py b/lib/matplotlib/backends/backend_cairo.py index c513872a588..48b23991250 100644 --- a/lib/matplotlib/backends/backend_cairo.py +++ b/lib/matplotlib/backends/backend_cairo.py @@ -1,21 +1,10 @@ """ A Cairo backend for matplotlib -Author: Steve Chaplin - -Cairo is a vector graphics library with cross-device output support. -Features of Cairo: - * anti-aliasing - * alpha channel - * saves image files as PNG, PostScript, PDF - -http://cairographics.org -Requires (in order, all available from Cairo website): - cairo, pycairo - -Naming Conventions - * classes MixedUpperCase - * varables lowerUpper - * functions underscore_separated +============================== +:Author: Steve Chaplin and others + +This backend depends on `cairo `_, and either on +cairocffi, or (Python 2 only) on pycairo. """ from __future__ import (absolute_import, division, print_function, @@ -30,26 +19,30 @@ import numpy as np -try: - import cairocffi as cairo -except ImportError: +# In order to make it possible to pick the binding, use whichever has already +# been imported, if any. (The intermediate call to iter is just to placate +# Python2.) +cairo = next( + (mod for mod in ( + sys.modules.get(name) for name in ["cairocffi", "cairo"]) if mod), + None) +if cairo is None: try: - import cairo + import cairocffi as 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)) + try: + import cairo + except ImportError: + raise ImportError( + "The cairo backend requires cairocffi or pycairo") +# cairocffi can install itself as cairo (`install_as_pycairo`) -- don't get +# fooled! +HAS_CAIRO_CFFI = cairo.__name__ == "cairocffi" + +if cairo.version_info < (1, 2, 0): + raise ImportError("cairo {} is installed; " + "cairo>=1.2.0 is required".format(cairo.version)) backend_version = cairo.version -del _version_required from matplotlib.backend_bases import ( _Backend, FigureCanvasBase, FigureManagerBase, GraphicsContextBase, @@ -114,14 +107,14 @@ def __init__(self, dpi): def set_ctx_from_surface(self, surface): self.gc.ctx = cairo.Context(surface) + # Although it may appear natural to automatically call + # `self.set_width_height(surface.get_width(), surface.get_height())` + # here (instead of having the caller do so separately), this would fail + # for PDF/PS/SVG surfaces, which have no way to report their extents. def set_width_height(self, width, height): self.width = width self.height = height - self.matrix_flipy = cairo.Matrix(yy=-1, y0=self.height) - # use matrix_flipy for ALL rendering? - # - problem with text? - will need to switch matrix_flipy off, or do a - # font transform? def _fill_and_stroke(self, ctx, fill_c, alpha, alpha_overrides): if fill_c is not None: @@ -317,11 +310,6 @@ def _draw_mathtext(self, gc, x, y, s, prop, angle): ctx.restore() - def flipy(self): - return True - #return False # tried - all draw objects ok except text (and images?) - # which comes out mirrored! - def get_canvas_width_height(self): return self.width, self.height @@ -458,9 +446,9 @@ def print_png(self, fobj, *args, **kwargs): width, height = self.get_width_height() renderer = RendererCairo(self.figure.dpi) - renderer.set_width_height(width, height) surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height) renderer.set_ctx_from_surface(surface) + renderer.set_width_height(width, height) self.figure.draw(renderer) surface.write_to_png(fobj) @@ -516,8 +504,8 @@ def _save(self, fo, fmt, **kwargs): # surface.set_dpi() can be used renderer = RendererCairo(self.figure.dpi) - renderer.set_width_height(width_in_points, height_in_points) renderer.set_ctx_from_surface(surface) + renderer.set_width_height(width_in_points, height_in_points) ctx = renderer.gc.ctx if orientation == 'landscape': diff --git a/lib/matplotlib/backends/backend_gtk3.py b/lib/matplotlib/backends/backend_gtk3.py index a5f223a3875..d73078042d2 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -292,11 +292,8 @@ def on_draw_event(self, widget, ctx): pass def draw(self): - if self.get_visible() and self.get_mapped(): + if self.is_drawable(): self.queue_draw() - # do a synchronous draw (its less efficient than an async draw, - # but is required if/when animation is used) - self.get_property("window").process_updates (False) def draw_idle(self): if self._idle_draw_id != 0: diff --git a/lib/matplotlib/backends/backend_qt4.py b/lib/matplotlib/backends/backend_qt4.py index cac4b2d744e..3f4ac57f9b7 100644 --- a/lib/matplotlib/backends/backend_qt4.py +++ b/lib/matplotlib/backends/backend_qt4.py @@ -1,63 +1,10 @@ from __future__ import (absolute_import, division, print_function, unicode_literals) -import six -from six import unichr -import os -import re -import signal -import sys - -from matplotlib._pylab_helpers import Gcf -from matplotlib.backend_bases import ( - FigureCanvasBase, FigureManagerBase, NavigationToolbar2, TimerBase, - cursors) -from matplotlib.figure import Figure -from matplotlib.widgets import SubplotTool - -from .qt_compat import QtCore, QtWidgets, _getSaveFileName, __version__ - from .backend_qt5 import ( - backend_version, SPECIAL_KEYS, SUPER, ALT, CTRL, SHIFT, MODIFIER_KEYS, - cursord, _create_qApp, _BackendQT5, TimerQT, MainWindow, FigureManagerQT, - NavigationToolbar2QT, SubplotToolQt, error_msg_qt, exception_handler) -from .backend_qt5 import FigureCanvasQT as FigureCanvasQT5 - -DEBUG = False - - -class FigureCanvasQT(FigureCanvasQT5): - - def __init__(self, figure): - if DEBUG: - print('FigureCanvasQt qt4: ', figure) - _create_qApp() - - # Note different super-calling style to backend_qt5 - QtWidgets.QWidget.__init__(self) - FigureCanvasBase.__init__(self, figure) - self.figure = figure - self.setMouseTracking(True) - self._idle = True - w, h = self.get_width_height() - self.resize(w, h) - - # Key auto-repeat enabled by default - self._keyautorepeat = True - - def wheelEvent(self, event): - x = event.x() - # flipy so y=0 is bottom of canvas - y = self.figure.bbox.height - event.y() - # from QWheelEvent::delta doc - steps = event.delta()/120 - if (event.orientation() == QtCore.Qt.Vertical): - FigureCanvasBase.scroll_event(self, x, y, steps) - if DEBUG: - print('scroll event: delta = %i, ' - 'steps = %i ' % (event.delta(), steps)) + _BackendQT5, NavigationToolbar2QT, MODIFIER_KEYS, SUPER, ALT, CTRL, SHIFT) @_BackendQT5.export class _BackendQT4(_BackendQT5): - FigureCanvas = FigureCanvasQT + pass diff --git a/lib/matplotlib/backends/backend_qt4agg.py b/lib/matplotlib/backends/backend_qt4agg.py index b6fd21fc388..dd4bdc08180 100644 --- a/lib/matplotlib/backends/backend_qt4agg.py +++ b/lib/matplotlib/backends/backend_qt4agg.py @@ -1,30 +1,9 @@ -""" -Render to qt from agg -""" from __future__ import (absolute_import, division, print_function, unicode_literals) -import six +from .backend_qt5agg import _BackendQT5Agg, NavigationToolbar2QT -from .backend_agg import FigureCanvasAgg -from .backend_qt4 import ( - QtCore, _BackendQT4, FigureCanvasQT, FigureManagerQT, NavigationToolbar2QT) -from .backend_qt5agg import FigureCanvasQTAggBase - -class FigureCanvasQTAgg(FigureCanvasQTAggBase, FigureCanvasQT): - """ - The canvas the figure renders into. Calls the draw and print fig - methods, creates the renderers, etc... - - Attributes - ---------- - figure : `matplotlib.figure.Figure` - A high-level Figure instance - - """ - - -@_BackendQT4.export -class _BackendQT4Agg(_BackendQT4): - FigureCanvas = FigureCanvasQTAgg +@_BackendQT5Agg.export +class _BackendQT4Agg(_BackendQT5Agg): + pass diff --git a/lib/matplotlib/backends/backend_qt4cairo.py b/lib/matplotlib/backends/backend_qt4cairo.py new file mode 100644 index 00000000000..f94851da382 --- /dev/null +++ b/lib/matplotlib/backends/backend_qt4cairo.py @@ -0,0 +1,6 @@ +from .backend_qt5cairo import _BackendQT5Cairo + + +@_BackendQT5Cairo.export +class _BackendQT4Cairo(_BackendQT5Cairo): + pass diff --git a/lib/matplotlib/backends/backend_qt5.py b/lib/matplotlib/backends/backend_qt5.py index ab8c255680c..c1e300a06ca 100644 --- a/lib/matplotlib/backends/backend_qt5.py +++ b/lib/matplotlib/backends/backend_qt5.py @@ -6,7 +6,7 @@ import re import signal import sys -from six import unichr +import traceback import matplotlib @@ -185,20 +185,50 @@ class FigureCanvasQT(QtWidgets.QWidget, FigureCanvasBase): def __init__(self, figure): _create_qApp() - # NB: Using super for this call to avoid a TypeError: - # __init__() takes exactly 2 arguments (1 given) on QWidget - # PyQt5 - # The need for this change is documented here - # http://pyqt.sourceforge.net/Docs/PyQt5/pyqt4_differences.html#cooperative-multi-inheritance - super(FigureCanvasQT, self).__init__(figure=figure) - self.figure = figure + # Work around lack of cooperative inheritance in PyQt4, PySide, and + # PySide2, we look up in the MRO both + # - the next base class that super() would call, and manually call + # its `__init__` without passing any argument; and + # - the next base class that super() would call AND that supports + # cooperative inheritance (i.e., not defined by the PyQt4, PySide, + # PySide2, sip or Shiboken packages), and manually call its + # `__init__` while passing the `figure` keyword argument to it + # while paying attention not to call the same `__init__` twice (i.e., + # the PyQt5 case). + mro = type(self).__mro__ + super_mro = mro[mro.index(FigureCanvasQT) + 1:] + next_base_init = super_mro[0] + next_coop_init = next( + cls for cls in mro[mro.index(FigureCanvasQT) + 1:] + if cls.__module__.split(".")[0] not in [ + "PyQt4", "sip", "PySide", "PySide2", "Shiboken"]) + if next_base_init == next_coop_init: + super(FigureCanvasQT, self).__init__(figure=figure) + else: + next_base_init.__init__(self) + next_coop_init.__init__(self, figure=figure) + + self.setAttribute(QtCore.Qt.WA_OpaquePaintEvent) self.setMouseTracking(True) - w, h = self.get_width_height() - self.resize(w, h) + self.figure = figure + self._draw_pending = False + self._draw_rect_callback = lambda painter: None # Key auto-repeat enabled by default self._keyautorepeat = True + # In cases with mixed resolution displays, we need to be careful if the + # dpi_ratio changes - in this case we need to resize the canvas + # accordingly. We could watch for screenChanged events from Qt, but + # the issue is that we can't guarantee this will be emitted *before* + # the first paintEvent for the canvas, so instead we keep track of the + # dpi_ratio value here and in paintEvent we resize the canvas if + # needed. + self._dpi_ratio_prev = None + # We don't want to scale up the figure DPI more than once. + self.figure._original_dpi = self.figure.dpi + self._update_dpi() + @property def _dpi_ratio(self): # Not available on Qt4 or some older Qt5. @@ -207,6 +237,24 @@ def _dpi_ratio(self): except AttributeError: return 1 + def _update_dpi(self): + # As described in __init__ above, we need to be careful in cases with + # mixed resolution displays if dpi_ratio is changing between painting + # events. + if self._dpi_ratio != self._dpi_ratio_prev: + # We need to update the figure DPI. + dpi = self._dpi_ratio * self.figure._original_dpi + self.figure._set_dpi(dpi, forward=False) + # The easiest way to resize the canvas is to emit a resizeEvent + # since we implement all the logic for resizing the canvas for + # that event. + event = QtGui.QResizeEvent(self.size(), self.size()) + # We use self.resizeEvent here instead of QApplication.postEvent + # since the latter doesn't guarantee that the event will be emitted + # straight away, and this causes visual delays in the changes. + self.resizeEvent(event) + self._dpi_ratio_prev = self._dpi_ratio + def get_width_height(self): w, h = FigureCanvasBase.get_width_height(self) return int(w / self._dpi_ratio), int(h / self._dpi_ratio) @@ -260,15 +308,26 @@ def mouseReleaseEvent(self, event): FigureCanvasBase.button_release_event(self, x, y, button, guiEvent=event) - def wheelEvent(self, event): - x, y = self.mouseEventCoords(event) - # from QWheelEvent::delta doc - if event.pixelDelta().x() == 0 and event.pixelDelta().y() == 0: - steps = event.angleDelta().y() / 120 - else: - steps = event.pixelDelta().y() - if steps: - FigureCanvasBase.scroll_event(self, x, y, steps, guiEvent=event) + if is_pyqt5(): + def wheelEvent(self, event): + x, y = self.mouseEventCoords(event) + # from QWheelEvent::delta doc + if event.pixelDelta().x() == 0 and event.pixelDelta().y() == 0: + steps = event.angleDelta().y() / 120 + else: + steps = event.pixelDelta().y() + if steps: + FigureCanvasBase.scroll_event( + self, x, y, steps, guiEvent=event) + else: + def wheelEvent(self, event): + x = event.x() + # flipy so y=0 is bottom of canvas + y = self.figure.bbox.height - event.y() + # from QWheelEvent::delta doc + steps = event.delta() / 120 + if event.orientation() == QtCore.Qt.Vertical: + FigureCanvasBase.scroll_event(self, x, y, steps) def keyPressEvent(self, event): key = self._get_key(event) @@ -335,7 +394,7 @@ def _get_key(self, event): if event_key > MAX_UNICODE: return None - key = unichr(event_key) + key = six.unichr(event_key) # qt delivers capitalized letters. fix capitalization # note that capslock is ignored if 'shift' in mods: @@ -381,6 +440,53 @@ def stop_event_loop(self, event=None): if hasattr(self, "_event_loop"): self._event_loop.quit() + def draw(self): + """Render the figure, and queue a request for a Qt draw. + """ + # The Agg draw is done here; delaying causes problems with code that + # uses the result of the draw() to update plot elements. + super(FigureCanvasQT, self).draw() + self.update() + + def draw_idle(self): + """Queue redraw of the Agg buffer and request Qt paintEvent. + """ + # The Agg draw needs to be handled by the same thread matplotlib + # modifies the scene graph from. Post Agg draw request to the + # current event loop in order to ensure thread affinity and to + # accumulate multiple draw requests from event handling. + # TODO: queued signal connection might be safer than singleShot + if not self._draw_pending: + self._draw_pending = True + QtCore.QTimer.singleShot(0, self._draw_idle) + + def _draw_idle(self): + if self.height() < 0 or self.width() < 0: + self._draw_pending = False + return + try: + self.draw() + except Exception: + # Uncaught exceptions are fatal for PyQt5, so catch them instead. + traceback.print_exc() + finally: + self._draw_pending = False + + def drawRectangle(self, rect): + # Draw the zoom rectangle to the QPainter. _draw_rect_callback needs + # to be called at the end of paintEvent. + if rect is not None: + def _draw_rect_callback(painter): + pen = QtGui.QPen(QtCore.Qt.black, 1 / self._dpi_ratio, + QtCore.Qt.DotLine) + painter.setPen(pen) + painter.drawRect(*(pt / self._dpi_ratio for pt in rect)) + else: + def _draw_rect_callback(painter): + return + self._draw_rect_callback = _draw_rect_callback + self.update() + class MainWindow(QtWidgets.QMainWindow): closing = QtCore.Signal() diff --git a/lib/matplotlib/backends/backend_qt5agg.py b/lib/matplotlib/backends/backend_qt5agg.py index b7a90a8f4d6..cb1765a23d8 100644 --- a/lib/matplotlib/backends/backend_qt5agg.py +++ b/lib/matplotlib/backends/backend_qt5agg.py @@ -7,7 +7,6 @@ import six import ctypes -import traceback from matplotlib import cbook from matplotlib.transforms import Bbox @@ -19,40 +18,11 @@ from .qt_compat import QT_API -class FigureCanvasQTAggBase(FigureCanvasAgg): - """ - The canvas the figure renders into. Calls the draw and print fig - methods, creates the renderers, etc... - - Attributes - ---------- - figure : `matplotlib.figure.Figure` - A high-level Figure instance - - """ +class FigureCanvasQTAgg(FigureCanvasAgg, FigureCanvasQT): def __init__(self, figure): - super(FigureCanvasQTAggBase, self).__init__(figure=figure) - self.setAttribute(QtCore.Qt.WA_OpaquePaintEvent) - self._agg_draw_pending = False + super(FigureCanvasQTAgg, self).__init__(figure=figure) self._bbox_queue = [] - self._drawRect = None - - # In cases with mixed resolution displays, we need to be careful if the - # dpi_ratio changes - in this case we need to resize the canvas - # accordingly. We could watch for screenChanged events from Qt, but - # the issue is that we can't guarantee this will be emitted *before* - # the first paintEvent for the canvas, so instead we keep track of the - # dpi_ratio value here and in paintEvent we resize the canvas if - # needed. - self._dpi_ratio_prev = None - - def drawRectangle(self, rect): - if rect is not None: - self._drawRect = [pt / self._dpi_ratio for pt in rect] - else: - self._drawRect = None - self.update() @property @cbook.deprecated("2.1") @@ -71,22 +41,7 @@ def paintEvent(self, e): if not hasattr(self, 'renderer'): return - # As described in __init__ above, we need to be careful in cases with - # mixed resolution displays if dpi_ratio is changing between painting - # events. - if (self._dpi_ratio_prev is None or - self._dpi_ratio != self._dpi_ratio_prev): - # We need to update the figure DPI - self._update_figure_dpi() - # The easiest way to resize the canvas is to emit a resizeEvent - # since we implement all the logic for resizing the canvas for - # that event. - event = QtGui.QResizeEvent(self.size(), self.size()) - # We use self.resizeEvent here instead of QApplication.postEvent - # since the latter doesn't guarantee that the event will be emitted - # straight away, and this causes visual delays in the changes. - self.resizeEvent(event) - self._dpi_ratio_prev = self._dpi_ratio + self._update_dpi() painter = QtGui.QPainter(self) @@ -104,23 +59,17 @@ def paintEvent(self, e): reg = self.copy_from_bbox(bbox) buf = reg.to_string_argb() qimage = QtGui.QImage(buf, w, h, QtGui.QImage.Format_ARGB32) + # Adjust the buf reference count to work around a memory leak bug + # in QImage under PySide on Python 3. + if QT_API == 'PySide' and six.PY3: + ctypes.c_long.from_address(id(buf)).value = 1 if hasattr(qimage, 'setDevicePixelRatio'): # Not available on Qt4 or some older Qt5. qimage.setDevicePixelRatio(self._dpi_ratio) origin = QtCore.QPoint(l, self.renderer.height - t) painter.drawImage(origin / self._dpi_ratio, qimage) - # Adjust the buf reference count to work around a memory - # leak bug in QImage under PySide on Python 3. - if QT_API == 'PySide' and six.PY3: - ctypes.c_long.from_address(id(buf)).value = 1 - # draw the zoom rectangle to the QPainter - if self._drawRect is not None: - pen = QtGui.QPen(QtCore.Qt.black, 1 / self._dpi_ratio, - QtCore.Qt.DotLine) - painter.setPen(pen) - x, y, w, h = self._drawRect - painter.drawRect(x, y, w, h) + self._draw_rect_callback(painter) painter.end() @@ -129,33 +78,9 @@ def draw(self): """ # The Agg draw is done here; delaying causes problems with code that # uses the result of the draw() to update plot elements. - super(FigureCanvasQTAggBase, self).draw() + super(FigureCanvasQTAgg, self).draw() self.update() - def draw_idle(self): - """Queue redraw of the Agg buffer and request Qt paintEvent. - """ - # The Agg draw needs to be handled by the same thread matplotlib - # modifies the scene graph from. Post Agg draw request to the - # current event loop in order to ensure thread affinity and to - # accumulate multiple draw requests from event handling. - # TODO: queued signal connection might be safer than singleShot - if not self._agg_draw_pending: - self._agg_draw_pending = True - QtCore.QTimer.singleShot(0, self.__draw_idle_agg) - - def __draw_idle_agg(self, *args): - if self.height() < 0 or self.width() < 0: - self._agg_draw_pending = False - return - try: - self.draw() - except Exception: - # Uncaught exceptions are fatal for PyQt5, so catch them instead. - traceback.print_exc() - finally: - self._agg_draw_pending = False - def blit(self, bbox=None): """Blit the region in bbox. """ @@ -172,36 +97,10 @@ def blit(self, bbox=None): self.repaint(l, self.renderer.height / self._dpi_ratio - t, w, h) def print_figure(self, *args, **kwargs): - super(FigureCanvasQTAggBase, self).print_figure(*args, **kwargs) + super(FigureCanvasQTAgg, self).print_figure(*args, **kwargs) self.draw() -class FigureCanvasQTAgg(FigureCanvasQTAggBase, FigureCanvasQT): - """ - The canvas the figure renders into. Calls the draw and print fig - methods, creates the renderers, etc. - - Modified to import from Qt5 backend for new-style mouse events. - - Attributes - ---------- - figure : `matplotlib.figure.Figure` - A high-level Figure instance - - """ - - def __init__(self, figure): - super(FigureCanvasQTAgg, self).__init__(figure=figure) - # We don't want to scale up the figure DPI more than once. - # Note, we don't handle a signal for changing DPI yet. - self.figure._original_dpi = self.figure.dpi - self._update_figure_dpi() - - def _update_figure_dpi(self): - dpi = self._dpi_ratio * self.figure._original_dpi - self.figure._set_dpi(dpi, forward=False) - - @_BackendQT5.export class _BackendQT5Agg(_BackendQT5): FigureCanvas = FigureCanvasQTAgg diff --git a/lib/matplotlib/backends/backend_qt5cairo.py b/lib/matplotlib/backends/backend_qt5cairo.py new file mode 100644 index 00000000000..ba3d1603507 --- /dev/null +++ b/lib/matplotlib/backends/backend_qt5cairo.py @@ -0,0 +1,34 @@ +from .backend_cairo import cairo, FigureCanvasCairo, RendererCairo +from .backend_qt5 import QtCore, QtGui, _BackendQT5, FigureCanvasQT +from .qt_compat import QT_API + + +class FigureCanvasQTCairo(FigureCanvasQT, FigureCanvasCairo): + def __init__(self, figure): + super(FigureCanvasQTCairo, self).__init__(figure=figure) + self._renderer = RendererCairo(self.figure.dpi) + + def paintEvent(self, event): + self._update_dpi() + width = self.width() + height = self.height() + surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height) + self._renderer.set_ctx_from_surface(surface) + self._renderer.set_width_height(width, height) + self.figure.draw(self._renderer) + buf = surface.get_data() + qimage = QtGui.QImage(buf, width, height, + QtGui.QImage.Format_ARGB32_Premultiplied) + # Adjust the buf reference count to work around a memory leak bug in + # QImage under PySide on Python 3. + if QT_API == 'PySide' and six.PY3: + ctypes.c_long.from_address(id(buf)).value = 1 + painter = QtGui.QPainter(self) + painter.drawImage(0, 0, qimage) + self._draw_rect_callback(painter) + painter.end() + + +@_BackendQT5.export +class _BackendQT5Cairo(_BackendQT5): + FigureCanvas = FigureCanvasQTCairo diff --git a/lib/matplotlib/rcsetup.py b/lib/matplotlib/rcsetup.py index ae60b108f54..df4d7b9b051 100644 --- a/lib/matplotlib/rcsetup.py +++ b/lib/matplotlib/rcsetup.py @@ -29,20 +29,17 @@ from matplotlib.fontconfig_pattern import parse_fontconfig_pattern from matplotlib.colors import is_color_like - # Don't let the original cycler collide with our validating cycler from cycler import Cycler, cycler as ccycler -# interactive_bk = ['gtk', 'gtkagg', 'gtkcairo', 'qt4agg', -# 'tkagg', 'wx', 'wxagg', 'webagg'] -# The capitalized forms are needed for ipython at present; this may -# change for later versions. - -interactive_bk = ['GTK', 'GTKAgg', 'GTKCairo', 'MacOSX', - 'Qt4Agg', 'Qt5Agg', 'TkAgg', 'WX', 'WXAgg', - 'GTK3Cairo', 'GTK3Agg', 'WebAgg', 'nbAgg'] - +interactive_bk = ['GTK', 'GTKAgg', 'GTKCairo', 'GTK3Agg', 'GTK3Cairo', + 'MacOSX', + 'nbAgg', + 'Qt4Agg', 'Qt4Cairo', 'Qt5Agg', 'Qt5Cairo', + 'TkAgg', + 'WebAgg', + 'WX', 'WXAgg'] non_interactive_bk = ['agg', 'cairo', 'gdk', 'pdf', 'pgf', 'ps', 'svg', 'template'] all_backends = interactive_bk + non_interactive_bk diff --git a/lib/matplotlib/tests/test_backends_interactive.py b/lib/matplotlib/tests/test_backends_interactive.py index 2db2fb5de07..71fe8904207 100644 --- a/lib/matplotlib/tests/test_backends_interactive.py +++ b/lib/matplotlib/tests/test_backends_interactive.py @@ -17,17 +17,24 @@ def _get_testable_interactive_backends(): - return [ - pytest.mark.skipif( - not os.environ.get("DISPLAY") - or sys.version_info < (3,) - or importlib.util.find_spec(module_name) is None, - reason="No $DISPLAY or could not import {!r}".format(module_name))( - backend) - for module_name, backend in [ - ("PyQt5", "qt5agg"), - ("tkinter", "tkagg"), - ("wx", "wxagg")]] + candidates = [(["PyQt5"], "qt5agg"), + (["PyQt5", "cairocffi"], "qt5cairo"), + (["tkinter"], "tkagg"), + (["wx"], "wxagg")] + backends = [] + for mods, backend in candidates: + if sys.version_info < (3,): + backends.append(pytest.mark.skip( + reason="Not testable on Py2.")(backend)) + elif not os.environ.get("DISPLAY"): + backends.append(pytest.mark.skip( + reason="No $DISPLAY.")(backend)) + elif any(importlib.util.find_spec(mod) is None for mod in mods): + backends.append(pytest.mark.skip( + reason="Missing dependency for {}.".format(backend))(backend)) + else: + backends.append(backend) + return backends _test_script = """\ diff --git a/tutorials/01_introductory/usage.py b/tutorials/01_introductory/usage.py index abaa2ba67be..15eaaf65b03 100644 --- a/tutorials/01_introductory/usage.py +++ b/tutorials/01_introductory/usage.py @@ -319,13 +319,18 @@ def my_plotter(ax, data1, data2, param_dict): # backend : WXAgg # use wxpython with antigrain (agg) rendering # # #. Setting the :envvar:`MPLBACKEND` environment -# variable, either for your current shell or for a single script:: +# variable, either for your current shell or for a single script. On Unix:: # -# > export MPLBACKEND="module://my_backend" +# > export MPLBACKEND=module://my_backend # > python simple_plot.py # # > MPLBACKEND="module://my_backend" python simple_plot.py # +# On Windows, only the former is possible:: +# +# > set MPLBACKEND=module://my_backend +# > python simple_plot.py +# # Setting this environment variable will override the ``backend`` parameter # in *any* ``matplotlibrc``, even if there is a ``matplotlibrc`` in your # current working directory. Therefore setting :envvar:`MPLBACKEND` @@ -357,19 +362,18 @@ def my_plotter(ax, data1, data2, param_dict): # methods given above. # # If, however, you want to write graphical user interfaces, or a web -# application server (:ref:`howto-webapp`), or need a better -# understanding of what is going on, read on. To make things a little -# more customizable for graphical user interfaces, matplotlib separates -# the concept of the renderer (the thing that actually does the drawing) -# from the canvas (the place where the drawing goes). The canonical -# renderer for user interfaces is ``Agg`` which uses the `Anti-Grain -# Geometry`_ C++ library to make a raster (pixel) image of the figure. -# All of the user interfaces except ``macosx`` can be used with -# agg rendering, e.g., -# ``WXAgg``, ``GTKAgg``, ``QT4Agg``, ``QT5Agg``, ``TkAgg``. In -# addition, some of the user interfaces support other rendering engines. -# For example, with GTK, you can also select GDK rendering (backend -# ``GTK`` deprecated in 2.0) or Cairo rendering (backend ``GTKCairo``). +# application server (:ref:`howto-webapp`), or need a better understanding of +# what is going on, read on. To make things a little more customizable for +# graphical user interfaces, matplotlib separates the concept of the renderer +# (the thing that actually does the drawing) from the canvas (the place where +# the drawing goes). The canonical renderer for user interfaces is ``Agg`` +# which uses the `Anti-Grain Geometry`_ C++ library to make a raster (pixel) +# image of the figure. All of the user interfaces except ``macosx`` can +# be used with Agg rendering, e.g., ``GTKAgg``, ``GTK3Agg``, ``Qt4Agg``, +# ``Qt5Agg``, ``TkAgg``, ``WxAgg``. In addition, some of the user interfaces +# support other rendering engines. For example, with GTK, GTK3, and QT5, +# you can also select Cairo rendering (backends ``GTKCairo``, ``GTK3Cairo``, +# ``Qt5Cairo``). # # For the rendering engines, one can also distinguish between `vector # `_ or `raster @@ -410,31 +414,28 @@ def my_plotter(ax, data1, data2, param_dict): # and of using appropriate renderers from the table above to write to # a file: # -# ============ ================================================================ -# Backend Description -# ============ ================================================================ -# GTKAgg Agg rendering to a :term:`GTK` 2.x canvas (requires PyGTK_ and -# pycairo_ or cairocffi_; Python2 only) -# GTK3Agg Agg rendering to a :term:`GTK` 3.x canvas (requires PyGObject_ -# and pycairo_ or cairocffi_) -# GTK GDK rendering to a :term:`GTK` 2.x canvas (not recommended and d -# eprecated in 2.0) (requires PyGTK_ and pycairo_ or cairocffi_; -# Python2 only) -# GTKCairo Cairo rendering to a :term:`GTK` 2.x canvas (requires PyGTK_ -# and pycairo_ or cairocffi_; Python2 only) -# GTK3Cairo Cairo rendering to a :term:`GTK` 3.x canvas (requires PyGObject_ -# and pycairo_ or cairocffi_) -# WXAgg Agg rendering to a :term:`wxWidgets` canvas -# (requires wxPython_) -# WX Native :term:`wxWidgets` drawing to a :term:`wxWidgets` Canvas -# (not recommended and deprecated in 2.0) (requires wxPython_) -# TkAgg Agg rendering to a :term:`Tk` canvas (requires TkInter_) -# Qt4Agg Agg rendering to a :term:`Qt4` canvas (requires PyQt4_ or ``pyside``) -# Qt5Agg Agg rendering in a :term:`Qt5` canvas (requires PyQt5_) -# macosx Cocoa rendering in OSX windows -# (presently lacks blocking show() behavior when matplotlib -# is in non-interactive mode) -# ============ ================================================================ +# ========= ================================================================ +# Backend Description +# ========= ================================================================ +# GTKAgg Agg rendering to a :term:`GTK` 2.x canvas (requires PyGTK_, and +# pycairo_ or cairocffi_; Python2 only) +# GTKCairo Cairo rendering to a :term:`GTK` 2.x canvas (requires PyGTK_, and +# pycairo_ or cairocffi_; Python2 only) +# GTK3Agg Agg rendering to a :term:`GTK` 3.x canvas (requires PyGObject_, and +# pycairo_ (Python2 only) or cairocffi_) +# GTK3Cairo Cairo rendering to a :term:`GTK` 3.x canvas (requires PyGObject_, +# and pycairo_ (Python2 only) or cairocffi_) +# TkAgg Agg rendering to a :term:`Tk` canvas (requires TkInter_) +# Qt4Agg Agg rendering to a :term:`Qt4` canvas (requires PyQt4_ or +# ``PySide``) +# Qt5Agg Agg rendering in a :term:`Qt5` canvas (requires PyQt5_) +# Qt5Cairo Cairo rendering in a :term:`Qt5` canvas (requires PyQt5_ and +# pycairo_ or cairocffi_) +# WxAgg Agg rendering to a :term:`wxWidgets` canvas (requires wxPython_) +# macosx Cocoa rendering in OSX windows +# (presently lacks blocking show() behavior when matplotlib is in +# non-interactive mode) +# ========= ================================================================ # # .. _`Anti-Grain Geometry`: http://antigrain.com/ # .. _Postscript: https://en.wikipedia.org/wiki/PostScript @@ -464,11 +465,8 @@ def my_plotter(ax, data1, data2, param_dict): # GTK and Cairo # ============= # -# Both `GTK2` and `GTK3` have implicit dependencies on PyCairo regardless of the -# specific Matplotlib backend used. Unfortunatly the latest release of PyCairo -# for Python3 does not implement the Python wrappers needed for the `GTK3Agg` -# backend. `Cairocffi` can be used as a replacement which implements the correct -# wrapper. +# Both `GTK2` and `GTK3` depend on a Cairo wrapper (PyCairo or cairocffi) even +# if the Agg renderer is used. On Python3, only cairocffi is supported. # # How do I select PyQt4 or PySide? # ========================================