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