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

PySide backend support #80

Merged
merged 14 commits into from Jun 16, 2011
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
55 changes: 18 additions & 37 deletions lib/matplotlib/backends/backend_qt4.py
Expand Up @@ -21,26 +21,18 @@
figureoptions = None

try:
from PyQt4 import QtCore, QtGui
from qt import QtCore, QtGui, _getSaveFileName, QT_API, QT_API_PYSIDE
except ImportError:
raise ImportError("Qt4 backend requires that PyQt4 is installed.")

import sip

try :
if sip.getapi("QString") > 1 :
# Use new getSaveFileNameAndFilter()
_getSaveFileName = lambda self, msg, start, filters, selectedFilter : \
QtGui.QFileDialog.getSaveFileNameAndFilter(self, \
msg, start, filters, selectedFilter)[0]
else :
# Use old getSaveFileName()
_getSaveFileName = QtGui.QFileDialog.getSaveFileName
except (AttributeError, KeyError) :
# call to getapi() can fail in older versions of sip
# Use the old getSaveFileName()
_getSaveFileName = QtGui.QFileDialog.getSaveFileName

raise ImportError("Qt4 backend requires that PyQt4 or PySide is installed.")

if QT_API == QT_API_PYSIDE:
class FigureCanvasBase( FigureCanvasBase, object ):
pass
class NavigationToolbar2( NavigationToolbar2, object ):
pass
class SubplotTool( SubplotTool, object ):
pass

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I gather that PySide requires that its class bases are new-style classes? Why don't we just make these bases new-style classes (i.e. make the canonical FigureCanvasBase in backends.py inherit from object)? That doesn't seem to break anything for any of the other GUI backends and removes the need for this kludge.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All correct. If it doesn't break anything I'll make the change on Monday.

backend_version = "0.9.1"
def fn_name(): return sys._getframe(1).f_code.co_name

Expand Down Expand Up @@ -292,22 +284,6 @@ def idle_draw(*args):
self._idle = True
if d: QtCore.QTimer.singleShot(0, idle_draw)


# XXX Hackish fix: There's a bug in PyQt. See this thread for details:
# http://old.nabble.com/Qt4-backend:-critical-bug-with-PyQt4-v4.6%2B-td26205716.html
# Once a release of Qt/PyQt is available without the bug, the version check
# below can be tightened further to only be applied in the necessary versions.
if QtCore.PYQT_VERSION_STR.startswith('4.6'):
class FigureWindow(QtGui.QMainWindow):
def __init__(self):
super(FigureWindow, self).__init__()
def closeEvent(self, event):
super(FigureWindow, self).closeEvent(event)
self.emit(QtCore.SIGNAL('destroyed()'))
else:
FigureWindow = QtGui.QMainWindow
# /end pyqt hackish bugfix

class FigureManagerQT( FigureManagerBase ):
"""
Public attributes
Expand All @@ -322,7 +298,7 @@ def __init__( self, canvas, num ):
if DEBUG: print 'FigureManagerQT.%s' % fn_name()
FigureManagerBase.__init__( self, canvas, num )
self.canvas = canvas
self.window = FigureWindow()
self.window = QtGui.QMainWindow()
self.window.setAttribute(QtCore.Qt.WA_DeleteOnClose)

self.window.setWindowTitle("Figure %d" % num)
Expand All @@ -341,7 +317,7 @@ def __init__( self, canvas, num ):
if self.toolbar is not None:
self.window.addToolBar(self.toolbar)
QtCore.QObject.connect(self.toolbar, QtCore.SIGNAL("message"),
self.window.statusBar().showMessage)
self._show_message)
tbs_height = self.toolbar.sizeHint().height()
else:
tbs_height = 0
Expand All @@ -366,6 +342,11 @@ def notify_axes_change( fig ):
self.toolbar.update()
self.canvas.figure.add_axobserver( notify_axes_change )

@QtCore.Slot()
def _show_message(self,s):
# Fixes a PySide segfault.
self.window.statusBar().showMessage(s)

def _widgetclosed( self ):
if self.window._destroying: return
self.window._destroying = True
Expand Down
8 changes: 6 additions & 2 deletions lib/matplotlib/backends/backend_qt4agg.py
Expand Up @@ -11,8 +11,12 @@
from backend_agg import FigureCanvasAgg
from backend_qt4 import QtCore, QtGui, FigureManagerQT, FigureCanvasQT,\
show, draw_if_interactive, backend_version, \
NavigationToolbar2QT
NavigationToolbar2QT, QT_API, QT_API_PYSIDE

if QT_API == QT_API_PYSIDE:
class FigureCanvasAgg( FigureCanvasAgg, object ):
pass

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See my note above re: new-style classes.

DEBUG = False


Expand Down Expand Up @@ -91,7 +95,7 @@ def paintEvent( self, e ):
else:
stringBuffer = self.renderer._renderer.tostring_argb()

qImage = QtGui.QImage(stringBuffer, self.renderer.width,
qImage = QtGui.QImage(stringBuffer, self.renderer.width,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With ubuntu 11.4 (pyside 1.0.1 & qt4 4.7.2), I get a following exception.

TypeError: 'PySide.QtGui.QImage' called with wrong argument types:
PySide.QtGui.QImage(str, int, int, PySide.QtGui.QImage.Format)
Supported signatures:
PySide.QtGui.QImage()
PySide.QtGui.QImage(PySide.QtGui.QImage)
PySide.QtGui.QImage(PySide.QtCore.QSize, PySide.QtGui.QImage.Format)
PySide.QtGui.QImage(QString, str = None)
PySide.QtGui.QImage(int, int, PySide.QtGui.QImage.Format)
PySide.QtGui.QImage(buffer, int, int, PySide.QtGui.QImage.Format)
PySide.QtGui.QImage(buffer, int, int, int, PySide.QtGui.QImage.Format)

Converting stringBuffer to a buffer object explicitly seem to solve the problem (both pyqt and pyside works), but i'm not sure if this behavior will depend on the pyside version.

        qImage = QtGui.QImage(buffer(stringBuffer), int(self.renderer.width), 
                              int(self.renderer.height),

We may need a same change at line 117.
The agg renderer has a "buffer_rgba" method but QImage does not seem to support RGBA format.

-JJ

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a PySide bug fixed in 1.0.2 http://bugs.pyside.org/show_bug.cgi?id=489

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably add a version check against PySide then, since we know 1.0.2 is the minimum workable version.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good to know that this is a pyside bug. However, using explicit buffer call does any harm to matplotib w/ pyside v1.0.2?
I could be wrong but my guess is that if we call buffer explicitly, the code will work for both v1.0.2 and v1.0.1?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. Wrapping with buffer makes it work in both versions. I was under the impression that mpl wasn't in the business of fixing bugs for specific versions of on specific platforms etc. In any case, there's another bug (819, see my opening comment) that wasn't fixed until 1.0.2 (try saving a figure). There is also an outstanding bug that prevents this backend working on mac (809) that won't be fixed until 1.0.3.

I'd suggest that 1.0.3 should probably be the minimum supported version to keep things simple. In which case this can stay as is. Unless there is a good reason to want to wrap with buffer() in any case?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my experience, mpl tries to broadly support as many versions of a library on as many platforms as feasible. I don't know if it's stated explicitly anywhere, but at least in my work environment, it is very important to support rather old and possibly buggy versions of core dependencies (e.g. GUI toolkits) for quite some time. Look at the GTK backend, for example, for a number of version-specific hacks that have been required over time. Eventually the idea is to prune that stuff out -- my own rule of thumb is to support at least as far back as whatever was in the (current release - 2) of RHEL.

In this specific case, however, with PySide being so new and cutting edge to begin with, I think it's ok to require a minimum version and not add hacks for 1.0.x which were fairly ephemeral and not widely-used releases. But the version check is important because user's should understand it's breaking because of an old version of PySide and not some other reason.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was inclined to fix the buffer thing since pyside v1.0.1 is packaged for ubuntu, and this simple fix at least makes the backend usable. Anyhow, I agree with Michael and the current approach is okay.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, that's good then.

As a side note, I was under the impression that Ubuntu was more or less the development platform for PySide. Every version of PySide is packaged for it within a few hours of release isn't it?

self.renderer.height,
QtGui.QImage.Format_ARGB32)
p = QtGui.QPainter(self)
Expand Down
53 changes: 53 additions & 0 deletions lib/matplotlib/backends/qt.py
@@ -0,0 +1,53 @@
""" A Qt API selector that can be used to switch between PyQt and PySide.
"""

import os

# Available APIs.
QT_API_PYQT = 'pyqt'
QT_API_PYSIDE = 'pyside'

# Use PyQt by default until PySide is stable.
QT_API = os.environ.get('QT_API', QT_API_PYQT)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was wondering if we shouldn't do PySide selection by adding a new backend type (Qt4Agg and PySideAgg would share a lot of code, but would be selected with matplotlib.use). However, then I saw that IPython is also using this method, and it would be nice to be compatible with that. Can we add a comment:

Select PyQt4 or PySide using an environment variable, in the same way IPython does.

if QT_API == QT_API_PYQT:
from PyQt4 import QtCore, QtGui

# Alias PyQt-specific functions for PySide compatibility.
try:
QtCore.Slot = QtCore.pyqtSlot
except AttributeError:
QtCore.Slot = pyqtSignature # Not a perfect match but
# works in simple cases
QtCore.Property = QtCore.pyqtProperty

import sip
try :
if sip.getapi("QString") > 1 :
# Use new getSaveFileNameAndFilter()
_getSaveFileName = lambda self, msg, start, filters, \
selectedFilter : \
QtGui.QFileDialog.getSaveFileNameAndFilter( \
self, msg, start, filters, selectedFilter)[0]
else :
# Use old getSaveFileName()
_getSaveFileName = QtGui.QFileDialog.getSaveFileName
except (AttributeError, KeyError) :
# call to getapi() can fail in older versions of sip
# Use the old getSaveFileName()
_getSaveFileName = QtGui.QFileDialog.getSaveFileName

elif QT_API == QT_API_PYSIDE:
from PySide import QtCore, QtGui

# Alias PySide-specific function for PyQt compatibilty
QtCore.pyqtProperty = QtCore.Property
QtCore.pyqtSignature = QtCore.Slot # Not a perfect match but
# works in simple cases

_getSaveFileName = lambda self, msg, start, filters, selectedFilter : \
QtGui.QFileDialog.getSaveFileName(self, \
msg, start, filters, selectedFilter)[0]
else:
raise RuntimeError('Invalid Qt API %r, valid values are: %r or %r' %
(QT_API, QT_API_PYQT, QT_API_PYSIDE))
4 changes: 2 additions & 2 deletions lib/matplotlib/backends/qt4_editor/figureoptions.py
Expand Up @@ -9,12 +9,12 @@
import os.path as osp

import matplotlib.backends.qt4_editor.formlayout as formlayout
from PyQt4.QtGui import QIcon
from matplotlib.backends.qt import QtGui

def get_icon(name):
import matplotlib
basedir = osp.join(matplotlib.rcParams['datapath'], 'images')
return QIcon(osp.join(basedir, name))
return QtGui.QIcon(osp.join(basedir, name))

LINESTYLES = {
'-': 'Solid',
Expand Down
41 changes: 23 additions & 18 deletions lib/matplotlib/backends/qt4_editor/formlayout.py
Expand Up @@ -45,21 +45,28 @@
import sys
STDERR = sys.stderr

try:
from PyQt4.QtGui import QFormLayout
except ImportError:
raise ImportError, "Warning: formlayout requires PyQt4 >v4.3"

from PyQt4.QtGui import (QWidget, QLineEdit, QComboBox, QLabel, QSpinBox, QIcon,
QStyle, QDialogButtonBox, QHBoxLayout, QVBoxLayout,
QDialog, QColor, QPushButton, QCheckBox, QColorDialog,
QPixmap, QTabWidget, QApplication, QStackedWidget,
QDateEdit, QDateTimeEdit, QFont, QFontComboBox,
QFontDatabase, QGridLayout)
from PyQt4.QtCore import (Qt, SIGNAL, SLOT, QObject, QSize,
pyqtSignature, pyqtProperty)
import datetime
from matplotlib.backends.qt import QtGui,QtCore
if not hasattr(QtGui,'QFormLayout'):
raise ImportError, "Warning: formlayout requires PyQt4 >v4.3 or PySide"

(QWidget, QLineEdit, QComboBox, QLabel, QSpinBox, QIcon,QStyle,
QDialogButtonBox, QHBoxLayout, QVBoxLayout, QDialog, QColor, QPushButton,
QCheckBox, QColorDialog, QPixmap, QTabWidget, QApplication, QStackedWidget,
QDateEdit, QDateTimeEdit, QFont, QFontComboBox, QFontDatabase, QGridLayout,
QFormLayout) =\
(QtGui.QWidget, QtGui.QLineEdit, QtGui.QComboBox, QtGui.QLabel,
QtGui.QSpinBox, QtGui.QIcon, QtGui.QStyle, QtGui.QDialogButtonBox,
QtGui.QHBoxLayout, QtGui.QVBoxLayout, QtGui.QDialog, QtGui.QColor,
QtGui.QPushButton, QtGui.QCheckBox, QtGui.QColorDialog, QtGui.QPixmap,
QtGui.QTabWidget, QtGui.QApplication, QtGui.QStackedWidget, QtGui.QDateEdit,
QtGui.QDateTimeEdit, QtGui.QFont, QtGui.QFontComboBox, QtGui.QFontDatabase,
QtGui.QGridLayout, QtGui.QFormLayout)

(Qt, SIGNAL, SLOT, QObject, QSize,pyqtSignature, pyqtProperty) =\
(QtCore.Qt, QtCore.SIGNAL, QtCore.SLOT, QtCore.QObject, QtCore.QSize,
QtCore.pyqtSignature, QtCore.pyqtProperty)

import datetime

class ColorButton(QPushButton):
"""
Expand All @@ -75,10 +82,8 @@ def __init__(self, parent=None):
self._color = QColor()

def choose_color(self):
rgba, valid = QColorDialog.getRgba(self._color.rgba(),
self.parentWidget())
if valid:
color = QColor.fromRgba(rgba)
color = QColorDialog.getColor(self._color,self.parentWidget(),'')
if color.isValid():
self.set_color(color)

def get_color(self):
Expand Down