##// END OF EJS Templates
Fixed PySide incompatibility with QVariant....
epatters -
Show More
@@ -1,30 +1,32 b''
1 """ A Qt API selector that can be used to switch between PyQt and PySide.
1 """ A Qt API selector that can be used to switch between PyQt and PySide.
2 """
2 """
3
3
4 import os
4 import os
5
5
6 # Available APIs.
6 # Available APIs.
7 QT_API_PYQT = 'pyqt'
7 QT_API_PYQT = 'pyqt'
8 QT_API_PYSIDE = 'pyside'
8 QT_API_PYSIDE = 'pyside'
9
9
10 # Use PyQt by default until PySide is stable.
10 # Use PyQt by default until PySide is stable.
11 QT_API = os.environ.get('QT_API', QT_API_PYQT)
11 QT_API = os.environ.get('QT_API', QT_API_PYQT)
12
12
13 if QT_API == QT_API_PYQT:
13 if QT_API == QT_API_PYQT:
14 # For PySide compatibility, use the new string API that automatically
14 # For PySide compatibility, use the new-style string API that automatically
15 # converts QStrings to Unicode Python strings.
15 # converts QStrings to Unicode Python strings. Also, automatically unpack
16 # QVariants to their underlying objects.
16 import sip
17 import sip
17 sip.setapi('QString', 2)
18 sip.setapi('QString', 2)
19 sip.setapi('QVariant', 2)
18
20
19 from PyQt4 import QtCore, QtGui, QtSvg
21 from PyQt4 import QtCore, QtGui, QtSvg
20
22
21 # Alias PyQt-specific functions for PySide compatibility.
23 # Alias PyQt-specific functions for PySide compatibility.
22 QtCore.Signal = QtCore.pyqtSignal
24 QtCore.Signal = QtCore.pyqtSignal
23 QtCore.Slot = QtCore.pyqtSlot
25 QtCore.Slot = QtCore.pyqtSlot
24
26
25 elif QT_API == QT_API_PYSIDE:
27 elif QT_API == QT_API_PYSIDE:
26 from PySide import QtCore, QtGui, QtSvg
28 from PySide import QtCore, QtGui, QtSvg
27
29
28 else:
30 else:
29 raise RuntimeError('Invalid Qt API %r, valid values are: %r or %r' %
31 raise RuntimeError('Invalid Qt API %r, valid values are: %r or %r' %
30 (QT_API, QT_API_PYQT, QT_API_PYSIDE))
32 (QT_API, QT_API_PYQT, QT_API_PYSIDE))
@@ -1,272 +1,249 b''
1 # Standard libary imports.
1 # Standard libary imports.
2 from base64 import decodestring
2 from base64 import decodestring
3 import os
3 import os
4 import re
4 import re
5
5
6 # System libary imports.
6 # System libary imports.
7 from IPython.external.qt import QtCore, QtGui
7 from IPython.external.qt import QtCore, QtGui
8
8
9 # Local imports
9 # Local imports
10 from IPython.frontend.qt.svg import save_svg, svg_to_clipboard, svg_to_image
10 from IPython.frontend.qt.svg import save_svg, svg_to_clipboard, svg_to_image
11 from ipython_widget import IPythonWidget
11 from ipython_widget import IPythonWidget
12
12
13
13
14 class RichIPythonWidget(IPythonWidget):
14 class RichIPythonWidget(IPythonWidget):
15 """ An IPythonWidget that supports rich text, including lists, images, and
15 """ An IPythonWidget that supports rich text, including lists, images, and
16 tables. Note that raw performance will be reduced compared to the plain
16 tables. Note that raw performance will be reduced compared to the plain
17 text version.
17 text version.
18 """
18 """
19
19
20 # RichIPythonWidget protected class variables.
20 # RichIPythonWidget protected class variables.
21 _payload_source_plot = 'IPython.zmq.pylab.backend_payload.add_plot_payload'
21 _payload_source_plot = 'IPython.zmq.pylab.backend_payload.add_plot_payload'
22 _svg_text_format_property = 1
23
22
24 #---------------------------------------------------------------------------
23 #---------------------------------------------------------------------------
25 # 'object' interface
24 # 'object' interface
26 #---------------------------------------------------------------------------
25 #---------------------------------------------------------------------------
27
26
28 def __init__(self, *args, **kw):
27 def __init__(self, *args, **kw):
29 """ Create a RichIPythonWidget.
28 """ Create a RichIPythonWidget.
30 """
29 """
31 kw['kind'] = 'rich'
30 kw['kind'] = 'rich'
32 super(RichIPythonWidget, self).__init__(*args, **kw)
31 super(RichIPythonWidget, self).__init__(*args, **kw)
33
32
34 # Configure the ConsoleWidget HTML exporter for our formats.
33 # Configure the ConsoleWidget HTML exporter for our formats.
35 self._html_exporter.image_tag = self._get_image_tag
34 self._html_exporter.image_tag = self._get_image_tag
36
35
37 # Dictionary for resolving Qt names to images when generating XHTML
36 # Dictionary for resolving document resource names to SVG data.
38 # output
37 self._name_to_svg_map = {}
39 self._name_to_svg = {}
40
38
41 #---------------------------------------------------------------------------
39 #---------------------------------------------------------------------------
42 # 'ConsoleWidget' protected interface
40 # 'ConsoleWidget' protected interface
43 #---------------------------------------------------------------------------
41 #---------------------------------------------------------------------------
44
42
45 def _context_menu_make(self, pos):
43 def _context_menu_make(self, pos):
46 """ Reimplemented to return a custom context menu for images.
44 """ Reimplemented to return a custom context menu for images.
47 """
45 """
48 format = self._control.cursorForPosition(pos).charFormat()
46 format = self._control.cursorForPosition(pos).charFormat()
49 name = format.stringProperty(QtGui.QTextFormat.ImageName)
47 name = format.stringProperty(QtGui.QTextFormat.ImageName)
50 if name:
48 if name:
51 menu = QtGui.QMenu()
49 menu = QtGui.QMenu()
52
50
53 menu.addAction('Copy Image', lambda: self._copy_image(name))
51 menu.addAction('Copy Image', lambda: self._copy_image(name))
54 menu.addAction('Save Image As...', lambda: self._save_image(name))
52 menu.addAction('Save Image As...', lambda: self._save_image(name))
55 menu.addSeparator()
53 menu.addSeparator()
56
54
57 svg = format.stringProperty(self._svg_text_format_property)
55 svg = self._name_to_svg_map.get(name, None)
58 if svg:
56 if svg is not None:
59 menu.addSeparator()
57 menu.addSeparator()
60 menu.addAction('Copy SVG', lambda: svg_to_clipboard(svg))
58 menu.addAction('Copy SVG', lambda: svg_to_clipboard(svg))
61 menu.addAction('Save SVG As...',
59 menu.addAction('Save SVG As...',
62 lambda: save_svg(svg, self._control))
60 lambda: save_svg(svg, self._control))
63 else:
61 else:
64 menu = super(RichIPythonWidget, self)._context_menu_make(pos)
62 menu = super(RichIPythonWidget, self)._context_menu_make(pos)
65 return menu
63 return menu
66
64
67 #---------------------------------------------------------------------------
65 #---------------------------------------------------------------------------
68 # 'BaseFrontendMixin' abstract interface
66 # 'BaseFrontendMixin' abstract interface
69 #---------------------------------------------------------------------------
67 #---------------------------------------------------------------------------
70
68
71 def _handle_pyout(self, msg):
69 def _handle_pyout(self, msg):
72 """ Overridden to handle rich data types, like SVG.
70 """ Overridden to handle rich data types, like SVG.
73 """
71 """
74 if not self._hidden and self._is_from_this_session(msg):
72 if not self._hidden and self._is_from_this_session(msg):
75 content = msg['content']
73 content = msg['content']
76 prompt_number = content['execution_count']
74 prompt_number = content['execution_count']
77 data = content['data']
75 data = content['data']
78 if data.has_key('image/svg+xml'):
76 if data.has_key('image/svg+xml'):
79 self._append_plain_text(self.output_sep)
77 self._append_plain_text(self.output_sep)
80 self._append_html(self._make_out_prompt(prompt_number))
78 self._append_html(self._make_out_prompt(prompt_number))
81 # TODO: try/except this call.
79 # TODO: try/except this call.
82 self._append_svg(data['image/svg+xml'])
80 self._append_svg(data['image/svg+xml'])
83 self._append_html(self.output_sep2)
81 self._append_html(self.output_sep2)
84 elif data.has_key('image/png'):
82 elif data.has_key('image/png'):
85 self._append_plain_text(self.output_sep)
83 self._append_plain_text(self.output_sep)
86 self._append_html(self._make_out_prompt(prompt_number))
84 self._append_html(self._make_out_prompt(prompt_number))
87 # This helps the output to look nice.
85 # This helps the output to look nice.
88 self._append_plain_text('\n')
86 self._append_plain_text('\n')
89 # TODO: try/except these calls
87 # TODO: try/except these calls
90 png = decodestring(data['image/png'])
88 png = decodestring(data['image/png'])
91 self._append_png(png)
89 self._append_png(png)
92 self._append_html(self.output_sep2)
90 self._append_html(self.output_sep2)
93 else:
91 else:
94 # Default back to the plain text representation.
92 # Default back to the plain text representation.
95 return super(RichIPythonWidget, self)._handle_pyout(msg)
93 return super(RichIPythonWidget, self)._handle_pyout(msg)
96
94
97 def _handle_display_data(self, msg):
95 def _handle_display_data(self, msg):
98 """ Overridden to handle rich data types, like SVG.
96 """ Overridden to handle rich data types, like SVG.
99 """
97 """
100 if not self._hidden and self._is_from_this_session(msg):
98 if not self._hidden and self._is_from_this_session(msg):
101 source = msg['content']['source']
99 source = msg['content']['source']
102 data = msg['content']['data']
100 data = msg['content']['data']
103 metadata = msg['content']['metadata']
101 metadata = msg['content']['metadata']
104 # Try to use the svg or html representations.
102 # Try to use the svg or html representations.
105 # FIXME: Is this the right ordering of things to try?
103 # FIXME: Is this the right ordering of things to try?
106 if data.has_key('image/svg+xml'):
104 if data.has_key('image/svg+xml'):
107 svg = data['image/svg+xml']
105 svg = data['image/svg+xml']
108 # TODO: try/except this call.
106 # TODO: try/except this call.
109 self._append_svg(svg)
107 self._append_svg(svg)
110 elif data.has_key('image/png'):
108 elif data.has_key('image/png'):
111 # TODO: try/except these calls
109 # TODO: try/except these calls
112 # PNG data is base64 encoded as it passes over the network
110 # PNG data is base64 encoded as it passes over the network
113 # in a JSON structure so we decode it.
111 # in a JSON structure so we decode it.
114 png = decodestring(data['image/png'])
112 png = decodestring(data['image/png'])
115 self._append_png(png)
113 self._append_png(png)
116 else:
114 else:
117 # Default back to the plain text representation.
115 # Default back to the plain text representation.
118 return super(RichIPythonWidget, self)._handle_display_data(msg)
116 return super(RichIPythonWidget, self)._handle_display_data(msg)
119
117
120 #---------------------------------------------------------------------------
118 #---------------------------------------------------------------------------
121 # 'FrontendWidget' protected interface
122 #---------------------------------------------------------------------------
123
124 def _process_execute_payload(self, item):
125 """ Reimplemented to handle matplotlib plot payloads.
126 """
127 # TODO: remove this as all plot data is coming back through the
128 # display_data message type.
129 if item['source'] == self._payload_source_plot:
130 if item['format'] == 'svg':
131 svg = item['data']
132 self._append_svg(svg)
133 return True
134 else:
135 # Add other plot formats here!
136 return False
137 else:
138 return super(RichIPythonWidget, self)._process_execute_payload(item)
139
140 #---------------------------------------------------------------------------
141 # 'RichIPythonWidget' protected interface
119 # 'RichIPythonWidget' protected interface
142 #---------------------------------------------------------------------------
120 #---------------------------------------------------------------------------
143
121
144 def _append_svg(self, svg):
122 def _append_svg(self, svg):
145 """ Append raw svg data to the widget.
123 """ Append raw svg data to the widget.
146 """
124 """
147 try:
125 try:
148 image = svg_to_image(svg)
126 image = svg_to_image(svg)
149 except ValueError:
127 except ValueError:
150 self._append_plain_text('Received invalid plot data.')
128 self._append_plain_text('Received invalid plot data.')
151 else:
129 else:
152 format = self._add_image(image)
130 format = self._add_image(image)
153 self._name_to_svg[str(format.name())] = svg
131 self._name_to_svg_map[format.name()] = svg
154 format.setProperty(self._svg_text_format_property, svg)
155 cursor = self._get_end_cursor()
132 cursor = self._get_end_cursor()
156 cursor.insertBlock()
133 cursor.insertBlock()
157 cursor.insertImage(format)
134 cursor.insertImage(format)
158 cursor.insertBlock()
135 cursor.insertBlock()
159
136
160 def _append_png(self, png):
137 def _append_png(self, png):
161 """ Append raw svg data to the widget.
138 """ Append raw svg data to the widget.
162 """
139 """
163 try:
140 try:
164 image = QtGui.QImage()
141 image = QtGui.QImage()
165 image.loadFromData(png, 'PNG')
142 image.loadFromData(png, 'PNG')
166 except ValueError:
143 except ValueError:
167 self._append_plain_text('Received invalid plot data.')
144 self._append_plain_text('Received invalid plot data.')
168 else:
145 else:
169 format = self._add_image(image)
146 format = self._add_image(image)
170 cursor = self._get_end_cursor()
147 cursor = self._get_end_cursor()
171 cursor.insertBlock()
148 cursor.insertBlock()
172 cursor.insertImage(format)
149 cursor.insertImage(format)
173 cursor.insertBlock()
150 cursor.insertBlock()
174
151
175 def _add_image(self, image):
152 def _add_image(self, image):
176 """ Adds the specified QImage to the document and returns a
153 """ Adds the specified QImage to the document and returns a
177 QTextImageFormat that references it.
154 QTextImageFormat that references it.
178 """
155 """
179 document = self._control.document()
156 document = self._control.document()
180 name = str(image.cacheKey())
157 name = str(image.cacheKey())
181 document.addResource(QtGui.QTextDocument.ImageResource,
158 document.addResource(QtGui.QTextDocument.ImageResource,
182 QtCore.QUrl(name), image)
159 QtCore.QUrl(name), image)
183 format = QtGui.QTextImageFormat()
160 format = QtGui.QTextImageFormat()
184 format.setName(name)
161 format.setName(name)
185 return format
162 return format
186
163
187 def _copy_image(self, name):
164 def _copy_image(self, name):
188 """ Copies the ImageResource with 'name' to the clipboard.
165 """ Copies the ImageResource with 'name' to the clipboard.
189 """
166 """
190 image = self._get_image(name)
167 image = self._get_image(name)
191 QtGui.QApplication.clipboard().setImage(image)
168 QtGui.QApplication.clipboard().setImage(image)
192
169
193 def _get_image(self, name):
170 def _get_image(self, name):
194 """ Returns the QImage stored as the ImageResource with 'name'.
171 """ Returns the QImage stored as the ImageResource with 'name'.
195 """
172 """
196 document = self._control.document()
173 document = self._control.document()
197 variant = document.resource(QtGui.QTextDocument.ImageResource,
174 image = document.resource(QtGui.QTextDocument.ImageResource,
198 QtCore.QUrl(name))
175 QtCore.QUrl(name))
199 return variant.toPyObject()
176 return image
200
177
201 def _get_image_tag(self, match, path = None, format = "png"):
178 def _get_image_tag(self, match, path = None, format = "png"):
202 """ Return (X)HTML mark-up for the image-tag given by match.
179 """ Return (X)HTML mark-up for the image-tag given by match.
203
180
204 Parameters
181 Parameters
205 ----------
182 ----------
206 match : re.SRE_Match
183 match : re.SRE_Match
207 A match to an HTML image tag as exported by Qt, with
184 A match to an HTML image tag as exported by Qt, with
208 match.group("Name") containing the matched image ID.
185 match.group("Name") containing the matched image ID.
209
186
210 path : string|None, optional [default None]
187 path : string|None, optional [default None]
211 If not None, specifies a path to which supporting files may be
188 If not None, specifies a path to which supporting files may be
212 written (e.g., for linked images). If None, all images are to be
189 written (e.g., for linked images). If None, all images are to be
213 included inline.
190 included inline.
214
191
215 format : "png"|"svg", optional [default "png"]
192 format : "png"|"svg", optional [default "png"]
216 Format for returned or referenced images.
193 Format for returned or referenced images.
217 """
194 """
218 if format == "png":
195 if format == "png":
219 try:
196 try:
220 image = self._get_image(match.group("name"))
197 image = self._get_image(match.group("name"))
221 except KeyError:
198 except KeyError:
222 return "<b>Couldn't find image %s</b>" % match.group("name")
199 return "<b>Couldn't find image %s</b>" % match.group("name")
223
200
224 if path is not None:
201 if path is not None:
225 if not os.path.exists(path):
202 if not os.path.exists(path):
226 os.mkdir(path)
203 os.mkdir(path)
227 relpath = os.path.basename(path)
204 relpath = os.path.basename(path)
228 if image.save("%s/qt_img%s.png" % (path,match.group("name")),
205 if image.save("%s/qt_img%s.png" % (path,match.group("name")),
229 "PNG"):
206 "PNG"):
230 return '<img src="%s/qt_img%s.png">' % (relpath,
207 return '<img src="%s/qt_img%s.png">' % (relpath,
231 match.group("name"))
208 match.group("name"))
232 else:
209 else:
233 return "<b>Couldn't save image!</b>"
210 return "<b>Couldn't save image!</b>"
234 else:
211 else:
235 ba = QtCore.QByteArray()
212 ba = QtCore.QByteArray()
236 buffer_ = QtCore.QBuffer(ba)
213 buffer_ = QtCore.QBuffer(ba)
237 buffer_.open(QtCore.QIODevice.WriteOnly)
214 buffer_.open(QtCore.QIODevice.WriteOnly)
238 image.save(buffer_, "PNG")
215 image.save(buffer_, "PNG")
239 buffer_.close()
216 buffer_.close()
240 return '<img src="data:image/png;base64,\n%s\n" />' % (
217 return '<img src="data:image/png;base64,\n%s\n" />' % (
241 re.sub(r'(.{60})',r'\1\n',str(ba.toBase64())))
218 re.sub(r'(.{60})',r'\1\n',str(ba.toBase64())))
242
219
243 elif format == "svg":
220 elif format == "svg":
244 try:
221 try:
245 svg = str(self._name_to_svg[match.group("name")])
222 svg = str(self._name_to_svg_map[match.group("name")])
246 except KeyError:
223 except KeyError:
247 return "<b>Couldn't find image %s</b>" % match.group("name")
224 return "<b>Couldn't find image %s</b>" % match.group("name")
248
225
249 # Not currently checking path, because it's tricky to find a
226 # Not currently checking path, because it's tricky to find a
250 # cross-browser way to embed external SVG images (e.g., via
227 # cross-browser way to embed external SVG images (e.g., via
251 # object or embed tags).
228 # object or embed tags).
252
229
253 # Chop stand-alone header from matplotlib SVG
230 # Chop stand-alone header from matplotlib SVG
254 offset = svg.find("<svg")
231 offset = svg.find("<svg")
255 assert(offset > -1)
232 assert(offset > -1)
256
233
257 return svg[offset:]
234 return svg[offset:]
258
235
259 else:
236 else:
260 return '<b>Unrecognized image format</b>'
237 return '<b>Unrecognized image format</b>'
261
238
262 def _save_image(self, name, format='PNG'):
239 def _save_image(self, name, format='PNG'):
263 """ Shows a save dialog for the ImageResource with 'name'.
240 """ Shows a save dialog for the ImageResource with 'name'.
264 """
241 """
265 dialog = QtGui.QFileDialog(self._control, 'Save Image')
242 dialog = QtGui.QFileDialog(self._control, 'Save Image')
266 dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
243 dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
267 dialog.setDefaultSuffix(format.lower())
244 dialog.setDefaultSuffix(format.lower())
268 dialog.setNameFilter('%s file (*.%s)' % (format, format.lower()))
245 dialog.setNameFilter('%s file (*.%s)' % (format, format.lower()))
269 if dialog.exec_():
246 if dialog.exec_():
270 filename = dialog.selectedFiles()[0]
247 filename = dialog.selectedFiles()[0]
271 image = self._get_image(name)
248 image = self._get_image(name)
272 image.save(filename, format)
249 image.save(filename, format)
General Comments 0
You need to be logged in to leave comments. Login now