##// END OF EJS Templates
Simplified the reset of the _svg_warning_displayed flag in RichIPythonWidget...
Ian Murray -
Show More
@@ -1,313 +1,317 b''
1 #-----------------------------------------------------------------------------
1 #-----------------------------------------------------------------------------
2 # Copyright (c) 2010, IPython Development Team.
2 # Copyright (c) 2010, IPython Development Team.
3 #
3 #
4 # Distributed under the terms of the Modified BSD License.
4 # Distributed under the terms of the Modified BSD License.
5 #
5 #
6 # The full license is in the file COPYING.txt, distributed with this software.
6 # The full license is in the file COPYING.txt, distributed with this software.
7 #-----------------------------------------------------------------------------
7 #-----------------------------------------------------------------------------
8
8
9 # Standard libary imports.
9 # Standard libary imports.
10 from base64 import decodestring
10 from base64 import decodestring
11 import os
11 import os
12 import re
12 import re
13
13
14 # System libary imports.
14 # System libary imports.
15 from IPython.external.qt import QtCore, QtGui
15 from IPython.external.qt import QtCore, QtGui
16
16
17 # Local imports
17 # Local imports
18 from IPython.utils.traitlets import Bool
18 from IPython.utils.traitlets import Bool
19 from IPython.frontend.qt.svg import save_svg, svg_to_clipboard, svg_to_image
19 from IPython.frontend.qt.svg import save_svg, svg_to_clipboard, svg_to_image
20 from ipython_widget import IPythonWidget
20 from ipython_widget import IPythonWidget
21
21
22
22
23 class RichIPythonWidget(IPythonWidget):
23 class RichIPythonWidget(IPythonWidget):
24 """ An IPythonWidget that supports rich text, including lists, images, and
24 """ An IPythonWidget that supports rich text, including lists, images, and
25 tables. Note that raw performance will be reduced compared to the plain
25 tables. Note that raw performance will be reduced compared to the plain
26 text version.
26 text version.
27 """
27 """
28
28
29 # RichIPythonWidget protected class variables.
29 # RichIPythonWidget protected class variables.
30 _payload_source_plot = 'IPython.zmq.pylab.backend_payload.add_plot_payload'
30 _payload_source_plot = 'IPython.zmq.pylab.backend_payload.add_plot_payload'
31 _jpg_supported = Bool(False)
31 _jpg_supported = Bool(False)
32 _svg_warning_displayed = False
33
32 #---------------------------------------------------------------------------
34 #---------------------------------------------------------------------------
33 # 'object' interface
35 # 'object' interface
34 #---------------------------------------------------------------------------
36 #---------------------------------------------------------------------------
35
37
36 def __init__(self, *args, **kw):
38 def __init__(self, *args, **kw):
37 """ Create a RichIPythonWidget.
39 """ Create a RichIPythonWidget.
38 """
40 """
39 kw['kind'] = 'rich'
41 kw['kind'] = 'rich'
40 super(RichIPythonWidget, self).__init__(*args, **kw)
42 super(RichIPythonWidget, self).__init__(*args, **kw)
41
43
42 # Configure the ConsoleWidget HTML exporter for our formats.
44 # Configure the ConsoleWidget HTML exporter for our formats.
43 self._html_exporter.image_tag = self._get_image_tag
45 self._html_exporter.image_tag = self._get_image_tag
44
46
45 # Dictionary for resolving document resource names to SVG data.
47 # Dictionary for resolving document resource names to SVG data.
46 self._name_to_svg_map = {}
48 self._name_to_svg_map = {}
47
49
48 # Do we support jpg ?
50 # Do we support jpg ?
49 # it seems that sometime jpg support is a plugin of QT, so try to assume
51 # it seems that sometime jpg support is a plugin of QT, so try to assume
50 # it is not always supported.
52 # it is not always supported.
51 _supported_format = map(str, QtGui.QImageReader.supportedImageFormats())
53 _supported_format = map(str, QtGui.QImageReader.supportedImageFormats())
52 self._jpg_supported = 'jpeg' in _supported_format
54 self._jpg_supported = 'jpeg' in _supported_format
53
55
54
56
55 #---------------------------------------------------------------------------
57 #---------------------------------------------------------------------------
58 # 'ConsoleWidget' public interface overides
59 #---------------------------------------------------------------------------
60
61 def export_html(self):
62 """ Shows a dialog to export HTML/XML in various formats.
63
64 Overridden in order to reset the _svg_warning_displayed flag.
65 """
66 self._svg_warning_displayed = False
67 super(RichIPythonWidget, self).export_html()
68
69
70 #---------------------------------------------------------------------------
56 # 'ConsoleWidget' protected interface
71 # 'ConsoleWidget' protected interface
57 #---------------------------------------------------------------------------
72 #---------------------------------------------------------------------------
58
73
59 def _context_menu_make(self, pos):
74 def _context_menu_make(self, pos):
60 """ Reimplemented to return a custom context menu for images.
75 """ Reimplemented to return a custom context menu for images.
61 """
76 """
62 format = self._control.cursorForPosition(pos).charFormat()
77 format = self._control.cursorForPosition(pos).charFormat()
63 name = format.stringProperty(QtGui.QTextFormat.ImageName)
78 name = format.stringProperty(QtGui.QTextFormat.ImageName)
64 if name:
79 if name:
65 menu = QtGui.QMenu()
80 menu = QtGui.QMenu()
66
81
67 menu.addAction('Copy Image', lambda: self._copy_image(name))
82 menu.addAction('Copy Image', lambda: self._copy_image(name))
68 menu.addAction('Save Image As...', lambda: self._save_image(name))
83 menu.addAction('Save Image As...', lambda: self._save_image(name))
69 menu.addSeparator()
84 menu.addSeparator()
70
85
71 svg = self._name_to_svg_map.get(name, None)
86 svg = self._name_to_svg_map.get(name, None)
72 if svg is not None:
87 if svg is not None:
73 menu.addSeparator()
88 menu.addSeparator()
74 menu.addAction('Copy SVG', lambda: svg_to_clipboard(svg))
89 menu.addAction('Copy SVG', lambda: svg_to_clipboard(svg))
75 menu.addAction('Save SVG As...',
90 menu.addAction('Save SVG As...',
76 lambda: save_svg(svg, self._control))
91 lambda: save_svg(svg, self._control))
77 else:
92 else:
78 menu = super(RichIPythonWidget, self)._context_menu_make(pos)
93 menu = super(RichIPythonWidget, self)._context_menu_make(pos)
79 return menu
94 return menu
80
95
81 #---------------------------------------------------------------------------
96 #---------------------------------------------------------------------------
82 # 'BaseFrontendMixin' abstract interface
97 # 'BaseFrontendMixin' abstract interface
83 #---------------------------------------------------------------------------
98 #---------------------------------------------------------------------------
84 def _pre_image_append(self, msg, prompt_number):
99 def _pre_image_append(self, msg, prompt_number):
85 """ Append the Out[] prompt and make the output nicer
100 """ Append the Out[] prompt and make the output nicer
86
101
87 Shared code for some the following if statement
102 Shared code for some the following if statement
88 """
103 """
89 self.log.debug("pyout: %s", msg.get('content', ''))
104 self.log.debug("pyout: %s", msg.get('content', ''))
90 self._append_plain_text(self.output_sep, True)
105 self._append_plain_text(self.output_sep, True)
91 self._append_html(self._make_out_prompt(prompt_number), True)
106 self._append_html(self._make_out_prompt(prompt_number), True)
92 self._append_plain_text('\n', True)
107 self._append_plain_text('\n', True)
93
108
94 def _handle_pyout(self, msg):
109 def _handle_pyout(self, msg):
95 """ Overridden to handle rich data types, like SVG.
110 """ Overridden to handle rich data types, like SVG.
96 """
111 """
97 if not self._hidden and self._is_from_this_session(msg):
112 if not self._hidden and self._is_from_this_session(msg):
98 content = msg['content']
113 content = msg['content']
99 prompt_number = content.get('execution_count', 0)
114 prompt_number = content.get('execution_count', 0)
100 data = content['data']
115 data = content['data']
101 if data.has_key('image/svg+xml'):
116 if data.has_key('image/svg+xml'):
102 self._pre_image_append(msg, prompt_number)
117 self._pre_image_append(msg, prompt_number)
103 self._append_svg(data['image/svg+xml'], True)
118 self._append_svg(data['image/svg+xml'], True)
104 self._append_html(self.output_sep2, True)
119 self._append_html(self.output_sep2, True)
105 elif data.has_key('image/png'):
120 elif data.has_key('image/png'):
106 self._pre_image_append(msg, prompt_number)
121 self._pre_image_append(msg, prompt_number)
107 self._append_png(decodestring(data['image/png'].encode('ascii')), True)
122 self._append_png(decodestring(data['image/png'].encode('ascii')), True)
108 self._append_html(self.output_sep2, True)
123 self._append_html(self.output_sep2, True)
109 elif data.has_key('image/jpeg') and self._jpg_supported:
124 elif data.has_key('image/jpeg') and self._jpg_supported:
110 self._pre_image_append(msg, prompt_number)
125 self._pre_image_append(msg, prompt_number)
111 self._append_jpg(decodestring(data['image/jpeg'].encode('ascii')), True)
126 self._append_jpg(decodestring(data['image/jpeg'].encode('ascii')), True)
112 self._append_html(self.output_sep2, True)
127 self._append_html(self.output_sep2, True)
113 else:
128 else:
114 # Default back to the plain text representation.
129 # Default back to the plain text representation.
115 return super(RichIPythonWidget, self)._handle_pyout(msg)
130 return super(RichIPythonWidget, self)._handle_pyout(msg)
116
131
117 def _handle_display_data(self, msg):
132 def _handle_display_data(self, msg):
118 """ Overridden to handle rich data types, like SVG.
133 """ Overridden to handle rich data types, like SVG.
119 """
134 """
120 if not self._hidden and self._is_from_this_session(msg):
135 if not self._hidden and self._is_from_this_session(msg):
121 source = msg['content']['source']
136 source = msg['content']['source']
122 data = msg['content']['data']
137 data = msg['content']['data']
123 metadata = msg['content']['metadata']
138 metadata = msg['content']['metadata']
124 # Try to use the svg or html representations.
139 # Try to use the svg or html representations.
125 # FIXME: Is this the right ordering of things to try?
140 # FIXME: Is this the right ordering of things to try?
126 if data.has_key('image/svg+xml'):
141 if data.has_key('image/svg+xml'):
127 self.log.debug("display: %s", msg.get('content', ''))
142 self.log.debug("display: %s", msg.get('content', ''))
128 svg = data['image/svg+xml']
143 svg = data['image/svg+xml']
129 self._append_svg(svg, True)
144 self._append_svg(svg, True)
130 elif data.has_key('image/png'):
145 elif data.has_key('image/png'):
131 self.log.debug("display: %s", msg.get('content', ''))
146 self.log.debug("display: %s", msg.get('content', ''))
132 # PNG data is base64 encoded as it passes over the network
147 # PNG data is base64 encoded as it passes over the network
133 # in a JSON structure so we decode it.
148 # in a JSON structure so we decode it.
134 png = decodestring(data['image/png'].encode('ascii'))
149 png = decodestring(data['image/png'].encode('ascii'))
135 self._append_png(png, True)
150 self._append_png(png, True)
136 elif data.has_key('image/jpeg') and self._jpg_supported:
151 elif data.has_key('image/jpeg') and self._jpg_supported:
137 self.log.debug("display: %s", msg.get('content', ''))
152 self.log.debug("display: %s", msg.get('content', ''))
138 jpg = decodestring(data['image/jpeg'].encode('ascii'))
153 jpg = decodestring(data['image/jpeg'].encode('ascii'))
139 self._append_jpg(jpg, True)
154 self._append_jpg(jpg, True)
140 else:
155 else:
141 # Default back to the plain text representation.
156 # Default back to the plain text representation.
142 return super(RichIPythonWidget, self)._handle_display_data(msg)
157 return super(RichIPythonWidget, self)._handle_display_data(msg)
143
158
144 #---------------------------------------------------------------------------
159 #---------------------------------------------------------------------------
145 # 'RichIPythonWidget' protected interface
160 # 'RichIPythonWidget' protected interface
146 #---------------------------------------------------------------------------
161 #---------------------------------------------------------------------------
147
162
148 def _append_jpg(self, jpg, before_prompt=False):
163 def _append_jpg(self, jpg, before_prompt=False):
149 """ Append raw JPG data to the widget."""
164 """ Append raw JPG data to the widget."""
150 self._append_custom(self._insert_jpg, jpg, before_prompt)
165 self._append_custom(self._insert_jpg, jpg, before_prompt)
151
166
152 def _append_png(self, png, before_prompt=False):
167 def _append_png(self, png, before_prompt=False):
153 """ Append raw PNG data to the widget.
168 """ Append raw PNG data to the widget.
154 """
169 """
155 self._append_custom(self._insert_png, png, before_prompt)
170 self._append_custom(self._insert_png, png, before_prompt)
156
171
157 def _append_svg(self, svg, before_prompt=False):
172 def _append_svg(self, svg, before_prompt=False):
158 """ Append raw SVG data to the widget.
173 """ Append raw SVG data to the widget.
159 """
174 """
160 self._append_custom(self._insert_svg, svg, before_prompt)
175 self._append_custom(self._insert_svg, svg, before_prompt)
161
176
162 def _add_image(self, image):
177 def _add_image(self, image):
163 """ Adds the specified QImage to the document and returns a
178 """ Adds the specified QImage to the document and returns a
164 QTextImageFormat that references it.
179 QTextImageFormat that references it.
165 """
180 """
166 document = self._control.document()
181 document = self._control.document()
167 name = str(image.cacheKey())
182 name = str(image.cacheKey())
168 document.addResource(QtGui.QTextDocument.ImageResource,
183 document.addResource(QtGui.QTextDocument.ImageResource,
169 QtCore.QUrl(name), image)
184 QtCore.QUrl(name), image)
170 format = QtGui.QTextImageFormat()
185 format = QtGui.QTextImageFormat()
171 format.setName(name)
186 format.setName(name)
172 return format
187 return format
173
188
174 def _copy_image(self, name):
189 def _copy_image(self, name):
175 """ Copies the ImageResource with 'name' to the clipboard.
190 """ Copies the ImageResource with 'name' to the clipboard.
176 """
191 """
177 image = self._get_image(name)
192 image = self._get_image(name)
178 QtGui.QApplication.clipboard().setImage(image)
193 QtGui.QApplication.clipboard().setImage(image)
179
194
180 def _get_image(self, name):
195 def _get_image(self, name):
181 """ Returns the QImage stored as the ImageResource with 'name'.
196 """ Returns the QImage stored as the ImageResource with 'name'.
182 """
197 """
183 document = self._control.document()
198 document = self._control.document()
184 image = document.resource(QtGui.QTextDocument.ImageResource,
199 image = document.resource(QtGui.QTextDocument.ImageResource,
185 QtCore.QUrl(name))
200 QtCore.QUrl(name))
186 return image
201 return image
187
202
188 def _get_image_tag(self, match, path = None, format = "png"):
203 def _get_image_tag(self, match, path = None, format = "png"):
189 """ Return (X)HTML mark-up for the image-tag given by match.
204 """ Return (X)HTML mark-up for the image-tag given by match.
190
205
191 Parameters
206 Parameters
192 ----------
207 ----------
193 match : re.SRE_Match
208 match : re.SRE_Match
194 A match to an HTML image tag as exported by Qt, with
209 A match to an HTML image tag as exported by Qt, with
195 match.group("Name") containing the matched image ID.
210 match.group("Name") containing the matched image ID.
196
211
197 path : string|None, optional [default None]
212 path : string|None, optional [default None]
198 If not None, specifies a path to which supporting files may be
213 If not None, specifies a path to which supporting files may be
199 written (e.g., for linked images). If None, all images are to be
214 written (e.g., for linked images). If None, all images are to be
200 included inline.
215 included inline.
201
216
202 format : "png"|"svg"|"jpg", optional [default "png"]
217 format : "png"|"svg"|"jpg", optional [default "png"]
203 Format for returned or referenced images.
218 Format for returned or referenced images.
204 """
219 """
205 if format in ("png","jpg"):
220 if format in ("png","jpg"):
206 try:
221 try:
207 image = self._get_image(match.group("name"))
222 image = self._get_image(match.group("name"))
208 except KeyError:
223 except KeyError:
209 return "<b>Couldn't find image %s</b>" % match.group("name")
224 return "<b>Couldn't find image %s</b>" % match.group("name")
210
225
211 if path is not None:
226 if path is not None:
212 if not os.path.exists(path):
227 if not os.path.exists(path):
213 os.mkdir(path)
228 os.mkdir(path)
214 relpath = os.path.basename(path)
229 relpath = os.path.basename(path)
215 if image.save("%s/qt_img%s.%s" % (path, match.group("name"), format),
230 if image.save("%s/qt_img%s.%s" % (path, match.group("name"), format),
216 "PNG"):
231 "PNG"):
217 return '<img src="%s/qt_img%s.%s">' % (relpath,
232 return '<img src="%s/qt_img%s.%s">' % (relpath,
218 match.group("name"),format)
233 match.group("name"),format)
219 else:
234 else:
220 return "<b>Couldn't save image!</b>"
235 return "<b>Couldn't save image!</b>"
221 else:
236 else:
222 ba = QtCore.QByteArray()
237 ba = QtCore.QByteArray()
223 buffer_ = QtCore.QBuffer(ba)
238 buffer_ = QtCore.QBuffer(ba)
224 buffer_.open(QtCore.QIODevice.WriteOnly)
239 buffer_.open(QtCore.QIODevice.WriteOnly)
225 image.save(buffer_, format.upper())
240 image.save(buffer_, format.upper())
226 buffer_.close()
241 buffer_.close()
227 return '<img src="data:image/%s;base64,\n%s\n" />' % (
242 return '<img src="data:image/%s;base64,\n%s\n" />' % (
228 format,re.sub(r'(.{60})',r'\1\n',str(ba.toBase64())))
243 format,re.sub(r'(.{60})',r'\1\n',str(ba.toBase64())))
229
244
230 elif format == "svg":
245 elif format == "svg":
231 try:
246 try:
232 svg = str(self._name_to_svg_map[match.group("name")])
247 svg = str(self._name_to_svg_map[match.group("name")])
233 except KeyError:
248 except KeyError:
234
249 if not self._svg_warning_displayed:
235 # Determine whether this is a new export by checking whether
236 # we've attempted to export this svg previously. If so, then
237 # reset the flag that determines whether the warning has been
238 # displayed or not.
239 if match.group("name") in getattr(self, '_svgs_processed', set()):
240 self._svg_warning_displayed = False
241
242 # Display this warning exactly once per export attempt.
243 if not getattr(self, '_svg_warning_displayed', False):
244 QtGui.QMessageBox.warning(self, 'Error converting PNG to SVG.',
250 QtGui.QMessageBox.warning(self, 'Error converting PNG to SVG.',
245 'Cannot convert a PNG to SVG. To fix this, add this to your ipython config:\n\n'
251 'Cannot convert a PNG to SVG. To fix this, add this to your ipython config:\n\n'
246 '\tc.InlineBackendConfig.figure_format = \'svg\'\n\n'
252 '\tc.InlineBackendConfig.figure_format = \'svg\'\n\n'
247 'And regenerate the figures.',
253 'And regenerate the figures.',
248 QtGui.QMessageBox.Ok)
254 QtGui.QMessageBox.Ok)
249 self._svg_warning_displayed = True
255 self._svg_warning_displayed = True
250 self._svgs_processed = set()
251 self._svgs_processed.add(match.group("name"))
252 return "<b>Cannot convert a PNG to SVG. </b>To fix this, add this to your config: <span>c.InlineBackendConfig.figure_format = 'svg'</span> and regenerate the figures."
256 return "<b>Cannot convert a PNG to SVG. </b>To fix this, add this to your config: <span>c.InlineBackendConfig.figure_format = 'svg'</span> and regenerate the figures."
253
257
254 # Not currently checking path, because it's tricky to find a
258 # Not currently checking path, because it's tricky to find a
255 # cross-browser way to embed external SVG images (e.g., via
259 # cross-browser way to embed external SVG images (e.g., via
256 # object or embed tags).
260 # object or embed tags).
257
261
258 # Chop stand-alone header from matplotlib SVG
262 # Chop stand-alone header from matplotlib SVG
259 offset = svg.find("<svg")
263 offset = svg.find("<svg")
260 assert(offset > -1)
264 assert(offset > -1)
261
265
262 return svg[offset:]
266 return svg[offset:]
263
267
264 else:
268 else:
265 return '<b>Unrecognized image format</b>'
269 return '<b>Unrecognized image format</b>'
266
270
267 def _insert_jpg(self, cursor, jpg):
271 def _insert_jpg(self, cursor, jpg):
268 """ Insert raw PNG data into the widget."""
272 """ Insert raw PNG data into the widget."""
269 self._insert_img(cursor, jpg, 'jpg')
273 self._insert_img(cursor, jpg, 'jpg')
270
274
271 def _insert_png(self, cursor, png):
275 def _insert_png(self, cursor, png):
272 """ Insert raw PNG data into the widget.
276 """ Insert raw PNG data into the widget.
273 """
277 """
274 self._insert_img(cursor, png, 'png')
278 self._insert_img(cursor, png, 'png')
275
279
276 def _insert_img(self, cursor, img, fmt):
280 def _insert_img(self, cursor, img, fmt):
277 """ insert a raw image, jpg or png """
281 """ insert a raw image, jpg or png """
278 try:
282 try:
279 image = QtGui.QImage()
283 image = QtGui.QImage()
280 image.loadFromData(img, fmt.upper())
284 image.loadFromData(img, fmt.upper())
281 except ValueError:
285 except ValueError:
282 self._insert_plain_text(cursor, 'Received invalid %s data.'%fmt)
286 self._insert_plain_text(cursor, 'Received invalid %s data.'%fmt)
283 else:
287 else:
284 format = self._add_image(image)
288 format = self._add_image(image)
285 cursor.insertBlock()
289 cursor.insertBlock()
286 cursor.insertImage(format)
290 cursor.insertImage(format)
287 cursor.insertBlock()
291 cursor.insertBlock()
288
292
289 def _insert_svg(self, cursor, svg):
293 def _insert_svg(self, cursor, svg):
290 """ Insert raw SVG data into the widet.
294 """ Insert raw SVG data into the widet.
291 """
295 """
292 try:
296 try:
293 image = svg_to_image(svg)
297 image = svg_to_image(svg)
294 except ValueError:
298 except ValueError:
295 self._insert_plain_text(cursor, 'Received invalid SVG data.')
299 self._insert_plain_text(cursor, 'Received invalid SVG data.')
296 else:
300 else:
297 format = self._add_image(image)
301 format = self._add_image(image)
298 self._name_to_svg_map[format.name()] = svg
302 self._name_to_svg_map[format.name()] = svg
299 cursor.insertBlock()
303 cursor.insertBlock()
300 cursor.insertImage(format)
304 cursor.insertImage(format)
301 cursor.insertBlock()
305 cursor.insertBlock()
302
306
303 def _save_image(self, name, format='PNG'):
307 def _save_image(self, name, format='PNG'):
304 """ Shows a save dialog for the ImageResource with 'name'.
308 """ Shows a save dialog for the ImageResource with 'name'.
305 """
309 """
306 dialog = QtGui.QFileDialog(self._control, 'Save Image')
310 dialog = QtGui.QFileDialog(self._control, 'Save Image')
307 dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
311 dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
308 dialog.setDefaultSuffix(format.lower())
312 dialog.setDefaultSuffix(format.lower())
309 dialog.setNameFilter('%s file (*.%s)' % (format, format.lower()))
313 dialog.setNameFilter('%s file (*.%s)' % (format, format.lower()))
310 if dialog.exec_():
314 if dialog.exec_():
311 filename = dialog.selectedFiles()[0]
315 filename = dialog.selectedFiles()[0]
312 image = self._get_image(name)
316 image = self._get_image(name)
313 image.save(filename, format)
317 image.save(filename, format)
General Comments 0
You need to be logged in to leave comments. Login now