##// END OF EJS Templates
Use safe appending in RichIPythonWidget.
epatters -
Show More
@@ -1,511 +1,511 b''
1 1 """ A FrontendWidget that emulates the interface of the console IPython and
2 2 supports the additional functionality provided by the IPython kernel.
3 3 """
4 4
5 5 #-----------------------------------------------------------------------------
6 6 # Imports
7 7 #-----------------------------------------------------------------------------
8 8
9 9 # Standard library imports
10 10 from collections import namedtuple
11 11 import os.path
12 12 import re
13 13 from subprocess import Popen
14 14 import sys
15 15 from textwrap import dedent
16 16
17 17 # System library imports
18 18 from IPython.external.qt import QtCore, QtGui
19 19
20 20 # Local imports
21 21 from IPython.core.inputsplitter import IPythonInputSplitter, \
22 22 transform_ipy_prompt
23 23 from IPython.core.usage import default_gui_banner
24 24 from IPython.utils.traitlets import Bool, Str, Unicode
25 25 from frontend_widget import FrontendWidget
26 26 import styles
27 27
28 28 #-----------------------------------------------------------------------------
29 29 # Constants
30 30 #-----------------------------------------------------------------------------
31 31
32 32 # Default strings to build and display input and output prompts (and separators
33 33 # in between)
34 34 default_in_prompt = 'In [<span class="in-prompt-number">%i</span>]: '
35 35 default_out_prompt = 'Out[<span class="out-prompt-number">%i</span>]: '
36 36 default_input_sep = '\n'
37 37 default_output_sep = ''
38 38 default_output_sep2 = ''
39 39
40 40 # Base path for most payload sources.
41 41 zmq_shell_source = 'IPython.zmq.zmqshell.ZMQInteractiveShell'
42 42
43 43 if sys.platform.startswith('win'):
44 44 default_editor = 'notepad'
45 45 else:
46 46 default_editor = ''
47 47
48 48 #-----------------------------------------------------------------------------
49 49 # IPythonWidget class
50 50 #-----------------------------------------------------------------------------
51 51
52 52 class IPythonWidget(FrontendWidget):
53 53 """ A FrontendWidget for an IPython kernel.
54 54 """
55 55
56 56 # If set, the 'custom_edit_requested(str, int)' signal will be emitted when
57 57 # an editor is needed for a file. This overrides 'editor' and 'editor_line'
58 58 # settings.
59 59 custom_edit = Bool(False)
60 60 custom_edit_requested = QtCore.Signal(object, object)
61 61
62 62 editor = Unicode(default_editor, config=True,
63 63 help="""
64 64 A command for invoking a system text editor. If the string contains a
65 65 {filename} format specifier, it will be used. Otherwise, the filename
66 66 will be appended to the end the command.
67 67 """)
68 68
69 69 editor_line = Unicode(config=True,
70 70 help="""
71 71 The editor command to use when a specific line number is requested. The
72 72 string should contain two format specifiers: {line} and {filename}. If
73 73 this parameter is not specified, the line number option to the %edit
74 74 magic will be ignored.
75 75 """)
76 76
77 77 style_sheet = Unicode(config=True,
78 78 help="""
79 79 A CSS stylesheet. The stylesheet can contain classes for:
80 80 1. Qt: QPlainTextEdit, QFrame, QWidget, etc
81 81 2. Pygments: .c, .k, .o, etc. (see PygmentsHighlighter)
82 82 3. IPython: .error, .in-prompt, .out-prompt, etc
83 83 """)
84 84
85 85
86 86 syntax_style = Str(config=True,
87 87 help="""
88 88 If not empty, use this Pygments style for syntax highlighting.
89 89 Otherwise, the style sheet is queried for Pygments style
90 90 information.
91 91 """)
92 92
93 93 # Prompts.
94 94 in_prompt = Str(default_in_prompt, config=True)
95 95 out_prompt = Str(default_out_prompt, config=True)
96 96 input_sep = Str(default_input_sep, config=True)
97 97 output_sep = Str(default_output_sep, config=True)
98 98 output_sep2 = Str(default_output_sep2, config=True)
99 99
100 100 # FrontendWidget protected class variables.
101 101 _input_splitter_class = IPythonInputSplitter
102 102
103 103 # IPythonWidget protected class variables.
104 104 _PromptBlock = namedtuple('_PromptBlock', ['block', 'length', 'number'])
105 105 _payload_source_edit = zmq_shell_source + '.edit_magic'
106 106 _payload_source_exit = zmq_shell_source + '.ask_exit'
107 107 _payload_source_next_input = zmq_shell_source + '.set_next_input'
108 108 _payload_source_page = 'IPython.zmq.page.page'
109 109
110 110 #---------------------------------------------------------------------------
111 111 # 'object' interface
112 112 #---------------------------------------------------------------------------
113 113
114 114 def __init__(self, *args, **kw):
115 115 super(IPythonWidget, self).__init__(*args, **kw)
116 116
117 117 # IPythonWidget protected variables.
118 118 self._payload_handlers = {
119 119 self._payload_source_edit : self._handle_payload_edit,
120 120 self._payload_source_exit : self._handle_payload_exit,
121 121 self._payload_source_page : self._handle_payload_page,
122 122 self._payload_source_next_input : self._handle_payload_next_input }
123 123 self._previous_prompt_obj = None
124 124 self._keep_kernel_on_exit = None
125 125
126 126 # Initialize widget styling.
127 127 if self.style_sheet:
128 128 self._style_sheet_changed()
129 129 self._syntax_style_changed()
130 130 else:
131 131 self.set_default_style()
132 132
133 133 #---------------------------------------------------------------------------
134 134 # 'BaseFrontendMixin' abstract interface
135 135 #---------------------------------------------------------------------------
136 136
137 137 def _handle_complete_reply(self, rep):
138 138 """ Reimplemented to support IPython's improved completion machinery.
139 139 """
140 140 cursor = self._get_cursor()
141 141 info = self._request_info.get('complete')
142 142 if info and info.id == rep['parent_header']['msg_id'] and \
143 143 info.pos == cursor.position():
144 144 matches = rep['content']['matches']
145 145 text = rep['content']['matched_text']
146 146 offset = len(text)
147 147
148 148 # Clean up matches with period and path separators if the matched
149 149 # text has not been transformed. This is done by truncating all
150 150 # but the last component and then suitably decreasing the offset
151 151 # between the current cursor position and the start of completion.
152 152 if len(matches) > 1 and matches[0][:offset] == text:
153 153 parts = re.split(r'[./\\]', text)
154 154 sep_count = len(parts) - 1
155 155 if sep_count:
156 156 chop_length = sum(map(len, parts[:sep_count])) + sep_count
157 157 matches = [ match[chop_length:] for match in matches ]
158 158 offset -= chop_length
159 159
160 160 # Move the cursor to the start of the match and complete.
161 161 cursor.movePosition(QtGui.QTextCursor.Left, n=offset)
162 162 self._complete_with_items(cursor, matches)
163 163
164 164 def _handle_execute_reply(self, msg):
165 165 """ Reimplemented to support prompt requests.
166 166 """
167 167 info = self._request_info.get('execute')
168 168 if info and info.id == msg['parent_header']['msg_id']:
169 169 if info.kind == 'prompt':
170 170 number = msg['content']['execution_count'] + 1
171 171 self._show_interpreter_prompt(number)
172 172 else:
173 173 super(IPythonWidget, self)._handle_execute_reply(msg)
174 174
175 175 def _handle_history_reply(self, msg):
176 176 """ Implemented to handle history tail replies, which are only supported
177 177 by the IPython kernel.
178 178 """
179 179 history_items = msg['content']['history']
180 180 items = [ line.rstrip() for _, _, line in history_items ]
181 181 self._set_history(items)
182 182
183 183 def _handle_pyout(self, msg):
184 184 """ Reimplemented for IPython-style "display hook".
185 185 """
186 186 if not self._hidden and self._is_from_this_session(msg):
187 187 content = msg['content']
188 188 prompt_number = content['execution_count']
189 189 data = content['data']
190 190 if data.has_key('text/html'):
191 191 self._append_plain_text(self.output_sep, True)
192 192 self._append_html(self._make_out_prompt(prompt_number), True)
193 193 html = data['text/html']
194 194 self._append_plain_text('\n', True)
195 195 self._append_html(html + self.output_sep2, True)
196 196 elif data.has_key('text/plain'):
197 197 self._append_plain_text(self.output_sep, True)
198 198 self._append_html(self._make_out_prompt(prompt_number), True)
199 199 text = data['text/plain']
200 200 # If the repr is multiline, make sure we start on a new line,
201 201 # so that its lines are aligned.
202 202 if "\n" in text and not self.output_sep.endswith("\n"):
203 203 self._append_plain_text('\n', True)
204 204 self._append_plain_text(text + self.output_sep2, True)
205 205
206 206 def _handle_display_data(self, msg):
207 207 """ The base handler for the ``display_data`` message.
208 208 """
209 209 # For now, we don't display data from other frontends, but we
210 210 # eventually will as this allows all frontends to monitor the display
211 211 # data. But we need to figure out how to handle this in the GUI.
212 212 if not self._hidden and self._is_from_this_session(msg):
213 213 source = msg['content']['source']
214 214 data = msg['content']['data']
215 215 metadata = msg['content']['metadata']
216 216 # In the regular IPythonWidget, we simply print the plain text
217 217 # representation.
218 218 if data.has_key('text/html'):
219 219 html = data['text/html']
220 self._append_html(html, before_prompt=True)
220 self._append_html(html, True)
221 221 elif data.has_key('text/plain'):
222 222 text = data['text/plain']
223 self._append_plain_text(text, before_prompt=True)
223 self._append_plain_text(text, True)
224 224 # This newline seems to be needed for text and html output.
225 self._append_plain_text(u'\n', before_prompt=True)
225 self._append_plain_text(u'\n', True)
226 226
227 227 def _started_channels(self):
228 228 """ Reimplemented to make a history request.
229 229 """
230 230 super(IPythonWidget, self)._started_channels()
231 231 self.kernel_manager.shell_channel.history(hist_access_type='tail',
232 232 n=1000)
233 233 #---------------------------------------------------------------------------
234 234 # 'ConsoleWidget' public interface
235 235 #---------------------------------------------------------------------------
236 236
237 237 def copy(self):
238 238 """ Copy the currently selected text to the clipboard, removing prompts
239 239 if possible.
240 240 """
241 241 text = self._control.textCursor().selection().toPlainText()
242 242 if text:
243 243 lines = map(transform_ipy_prompt, text.splitlines())
244 244 text = '\n'.join(lines)
245 245 QtGui.QApplication.clipboard().setText(text)
246 246
247 247 #---------------------------------------------------------------------------
248 248 # 'FrontendWidget' public interface
249 249 #---------------------------------------------------------------------------
250 250
251 251 def execute_file(self, path, hidden=False):
252 252 """ Reimplemented to use the 'run' magic.
253 253 """
254 254 # Use forward slashes on Windows to avoid escaping each separator.
255 255 if sys.platform == 'win32':
256 256 path = os.path.normpath(path).replace('\\', '/')
257 257
258 258 self.execute('%%run %s' % path, hidden=hidden)
259 259
260 260 #---------------------------------------------------------------------------
261 261 # 'FrontendWidget' protected interface
262 262 #---------------------------------------------------------------------------
263 263
264 264 def _complete(self):
265 265 """ Reimplemented to support IPython's improved completion machinery.
266 266 """
267 267 # We let the kernel split the input line, so we *always* send an empty
268 268 # text field. Readline-based frontends do get a real text field which
269 269 # they can use.
270 270 text = ''
271 271
272 272 # Send the completion request to the kernel
273 273 msg_id = self.kernel_manager.shell_channel.complete(
274 274 text, # text
275 275 self._get_input_buffer_cursor_line(), # line
276 276 self._get_input_buffer_cursor_column(), # cursor_pos
277 277 self.input_buffer) # block
278 278 pos = self._get_cursor().position()
279 279 info = self._CompletionRequest(msg_id, pos)
280 280 self._request_info['complete'] = info
281 281
282 282 def _get_banner(self):
283 283 """ Reimplemented to return IPython's default banner.
284 284 """
285 285 return default_gui_banner
286 286
287 287 def _process_execute_error(self, msg):
288 288 """ Reimplemented for IPython-style traceback formatting.
289 289 """
290 290 content = msg['content']
291 291 traceback = '\n'.join(content['traceback']) + '\n'
292 292 if False:
293 293 # FIXME: For now, tracebacks come as plain text, so we can't use
294 294 # the html renderer yet. Once we refactor ultratb to produce
295 295 # properly styled tracebacks, this branch should be the default
296 296 traceback = traceback.replace(' ', '&nbsp;')
297 297 traceback = traceback.replace('\n', '<br/>')
298 298
299 299 ename = content['ename']
300 300 ename_styled = '<span class="error">%s</span>' % ename
301 301 traceback = traceback.replace(ename, ename_styled)
302 302
303 303 self._append_html(traceback)
304 304 else:
305 305 # This is the fallback for now, using plain text with ansi escapes
306 306 self._append_plain_text(traceback)
307 307
308 308 def _process_execute_payload(self, item):
309 309 """ Reimplemented to dispatch payloads to handler methods.
310 310 """
311 311 handler = self._payload_handlers.get(item['source'])
312 312 if handler is None:
313 313 # We have no handler for this type of payload, simply ignore it
314 314 return False
315 315 else:
316 316 handler(item)
317 317 return True
318 318
319 319 def _show_interpreter_prompt(self, number=None):
320 320 """ Reimplemented for IPython-style prompts.
321 321 """
322 322 # If a number was not specified, make a prompt number request.
323 323 if number is None:
324 324 msg_id = self.kernel_manager.shell_channel.execute('', silent=True)
325 325 info = self._ExecutionRequest(msg_id, 'prompt')
326 326 self._request_info['execute'] = info
327 327 return
328 328
329 329 # Show a new prompt and save information about it so that it can be
330 330 # updated later if the prompt number turns out to be wrong.
331 331 self._prompt_sep = self.input_sep
332 332 self._show_prompt(self._make_in_prompt(number), html=True)
333 333 block = self._control.document().lastBlock()
334 334 length = len(self._prompt)
335 335 self._previous_prompt_obj = self._PromptBlock(block, length, number)
336 336
337 337 # Update continuation prompt to reflect (possibly) new prompt length.
338 338 self._set_continuation_prompt(
339 339 self._make_continuation_prompt(self._prompt), html=True)
340 340
341 341 def _show_interpreter_prompt_for_reply(self, msg):
342 342 """ Reimplemented for IPython-style prompts.
343 343 """
344 344 # Update the old prompt number if necessary.
345 345 content = msg['content']
346 346 previous_prompt_number = content['execution_count']
347 347 if self._previous_prompt_obj and \
348 348 self._previous_prompt_obj.number != previous_prompt_number:
349 349 block = self._previous_prompt_obj.block
350 350
351 351 # Make sure the prompt block has not been erased.
352 352 if block.isValid() and block.text():
353 353
354 354 # Remove the old prompt and insert a new prompt.
355 355 cursor = QtGui.QTextCursor(block)
356 356 cursor.movePosition(QtGui.QTextCursor.Right,
357 357 QtGui.QTextCursor.KeepAnchor,
358 358 self._previous_prompt_obj.length)
359 359 prompt = self._make_in_prompt(previous_prompt_number)
360 360 self._prompt = self._insert_html_fetching_plain_text(
361 361 cursor, prompt)
362 362
363 363 # When the HTML is inserted, Qt blows away the syntax
364 364 # highlighting for the line, so we need to rehighlight it.
365 365 self._highlighter.rehighlightBlock(cursor.block())
366 366
367 367 self._previous_prompt_obj = None
368 368
369 369 # Show a new prompt with the kernel's estimated prompt number.
370 370 self._show_interpreter_prompt(previous_prompt_number + 1)
371 371
372 372 #---------------------------------------------------------------------------
373 373 # 'IPythonWidget' interface
374 374 #---------------------------------------------------------------------------
375 375
376 376 def set_default_style(self, colors='lightbg'):
377 377 """ Sets the widget style to the class defaults.
378 378
379 379 Parameters:
380 380 -----------
381 381 colors : str, optional (default lightbg)
382 382 Whether to use the default IPython light background or dark
383 383 background or B&W style.
384 384 """
385 385 colors = colors.lower()
386 386 if colors=='lightbg':
387 387 self.style_sheet = styles.default_light_style_sheet
388 388 self.syntax_style = styles.default_light_syntax_style
389 389 elif colors=='linux':
390 390 self.style_sheet = styles.default_dark_style_sheet
391 391 self.syntax_style = styles.default_dark_syntax_style
392 392 elif colors=='nocolor':
393 393 self.style_sheet = styles.default_bw_style_sheet
394 394 self.syntax_style = styles.default_bw_syntax_style
395 395 else:
396 396 raise KeyError("No such color scheme: %s"%colors)
397 397
398 398 #---------------------------------------------------------------------------
399 399 # 'IPythonWidget' protected interface
400 400 #---------------------------------------------------------------------------
401 401
402 402 def _edit(self, filename, line=None):
403 403 """ Opens a Python script for editing.
404 404
405 405 Parameters:
406 406 -----------
407 407 filename : str
408 408 A path to a local system file.
409 409
410 410 line : int, optional
411 411 A line of interest in the file.
412 412 """
413 413 if self.custom_edit:
414 414 self.custom_edit_requested.emit(filename, line)
415 415 elif not self.editor:
416 416 self._append_plain_text('No default editor available.\n'
417 417 'Specify a GUI text editor in the `IPythonWidget.editor` '
418 418 'configurable to enable the %edit magic')
419 419 else:
420 420 try:
421 421 filename = '"%s"' % filename
422 422 if line and self.editor_line:
423 423 command = self.editor_line.format(filename=filename,
424 424 line=line)
425 425 else:
426 426 try:
427 427 command = self.editor.format()
428 428 except KeyError:
429 429 command = self.editor.format(filename=filename)
430 430 else:
431 431 command += ' ' + filename
432 432 except KeyError:
433 433 self._append_plain_text('Invalid editor command.\n')
434 434 else:
435 435 try:
436 436 Popen(command, shell=True)
437 437 except OSError:
438 438 msg = 'Opening editor with command "%s" failed.\n'
439 439 self._append_plain_text(msg % command)
440 440
441 441 def _make_in_prompt(self, number):
442 442 """ Given a prompt number, returns an HTML In prompt.
443 443 """
444 444 body = self.in_prompt % number
445 445 return '<span class="in-prompt">%s</span>' % body
446 446
447 447 def _make_continuation_prompt(self, prompt):
448 448 """ Given a plain text version of an In prompt, returns an HTML
449 449 continuation prompt.
450 450 """
451 451 end_chars = '...: '
452 452 space_count = len(prompt.lstrip('\n')) - len(end_chars)
453 453 body = '&nbsp;' * space_count + end_chars
454 454 return '<span class="in-prompt">%s</span>' % body
455 455
456 456 def _make_out_prompt(self, number):
457 457 """ Given a prompt number, returns an HTML Out prompt.
458 458 """
459 459 body = self.out_prompt % number
460 460 return '<span class="out-prompt">%s</span>' % body
461 461
462 462 #------ Payload handlers --------------------------------------------------
463 463
464 464 # Payload handlers with a generic interface: each takes the opaque payload
465 465 # dict, unpacks it and calls the underlying functions with the necessary
466 466 # arguments.
467 467
468 468 def _handle_payload_edit(self, item):
469 469 self._edit(item['filename'], item['line_number'])
470 470
471 471 def _handle_payload_exit(self, item):
472 472 self._keep_kernel_on_exit = item['keepkernel']
473 473 self.exit_requested.emit()
474 474
475 475 def _handle_payload_next_input(self, item):
476 476 self.input_buffer = dedent(item['text'].rstrip())
477 477
478 478 def _handle_payload_page(self, item):
479 479 # Since the plain text widget supports only a very small subset of HTML
480 480 # and we have no control over the HTML source, we only page HTML
481 481 # payloads in the rich text widget.
482 482 if item['html'] and self.kind == 'rich':
483 483 self._page(item['html'], html=True)
484 484 else:
485 485 self._page(item['text'], html=False)
486 486
487 487 #------ Trait change handlers --------------------------------------------
488 488
489 489 def _style_sheet_changed(self):
490 490 """ Set the style sheets of the underlying widgets.
491 491 """
492 492 self.setStyleSheet(self.style_sheet)
493 493 self._control.document().setDefaultStyleSheet(self.style_sheet)
494 494 if self._page_control:
495 495 self._page_control.document().setDefaultStyleSheet(self.style_sheet)
496 496
497 497 bg_color = self._control.palette().window().color()
498 498 self._ansi_processor.set_background_color(bg_color)
499 499
500 500
501 501 def _syntax_style_changed(self):
502 502 """ Set the style for the syntax highlighter.
503 503 """
504 504 if self._highlighter is None:
505 505 # ignore premature calls
506 506 return
507 507 if self.syntax_style:
508 508 self._highlighter.set_style(self.syntax_style)
509 509 else:
510 510 self._highlighter.set_style_sheet(self.style_sheet)
511 511
@@ -1,249 +1,252 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 22
23 23 #---------------------------------------------------------------------------
24 24 # 'object' interface
25 25 #---------------------------------------------------------------------------
26 26
27 27 def __init__(self, *args, **kw):
28 28 """ Create a RichIPythonWidget.
29 29 """
30 30 kw['kind'] = 'rich'
31 31 super(RichIPythonWidget, self).__init__(*args, **kw)
32 32
33 33 # Configure the ConsoleWidget HTML exporter for our formats.
34 34 self._html_exporter.image_tag = self._get_image_tag
35 35
36 36 # Dictionary for resolving document resource names to SVG data.
37 37 self._name_to_svg_map = {}
38 38
39 39 #---------------------------------------------------------------------------
40 40 # 'ConsoleWidget' protected interface
41 41 #---------------------------------------------------------------------------
42 42
43 43 def _context_menu_make(self, pos):
44 44 """ Reimplemented to return a custom context menu for images.
45 45 """
46 46 format = self._control.cursorForPosition(pos).charFormat()
47 47 name = format.stringProperty(QtGui.QTextFormat.ImageName)
48 48 if name:
49 49 menu = QtGui.QMenu()
50 50
51 51 menu.addAction('Copy Image', lambda: self._copy_image(name))
52 52 menu.addAction('Save Image As...', lambda: self._save_image(name))
53 53 menu.addSeparator()
54 54
55 55 svg = self._name_to_svg_map.get(name, None)
56 56 if svg is not None:
57 57 menu.addSeparator()
58 58 menu.addAction('Copy SVG', lambda: svg_to_clipboard(svg))
59 59 menu.addAction('Save SVG As...',
60 60 lambda: save_svg(svg, self._control))
61 61 else:
62 62 menu = super(RichIPythonWidget, self)._context_menu_make(pos)
63 63 return menu
64 64
65 65 #---------------------------------------------------------------------------
66 66 # 'BaseFrontendMixin' abstract interface
67 67 #---------------------------------------------------------------------------
68 68
69 69 def _handle_pyout(self, msg):
70 70 """ Overridden to handle rich data types, like SVG.
71 71 """
72 72 if not self._hidden and self._is_from_this_session(msg):
73 73 content = msg['content']
74 74 prompt_number = content['execution_count']
75 75 data = content['data']
76 76 if data.has_key('image/svg+xml'):
77 self._append_plain_text(self.output_sep)
78 self._append_html(self._make_out_prompt(prompt_number))
79 # TODO: try/except this call.
80 self._append_svg(data['image/svg+xml'])
81 self._append_html(self.output_sep2)
77 self._append_plain_text(self.output_sep, True)
78 self._append_html(self._make_out_prompt(prompt_number), True)
79 self._append_svg(data['image/svg+xml'], True)
80 self._append_html(self.output_sep2, True)
82 81 elif data.has_key('image/png'):
83 self._append_plain_text(self.output_sep)
84 self._append_html(self._make_out_prompt(prompt_number))
82 self._append_plain_text(self.output_sep, True)
83 self._append_html(self._make_out_prompt(prompt_number), True)
85 84 # This helps the output to look nice.
86 self._append_plain_text('\n')
87 # TODO: try/except these calls
88 png = decodestring(data['image/png'])
89 self._append_png(png)
90 self._append_html(self.output_sep2)
85 self._append_plain_text('\n', True)
86 self._append_png(decodestring(data['image/png']), True)
87 self._append_html(self.output_sep2, True)
91 88 else:
92 89 # Default back to the plain text representation.
93 90 return super(RichIPythonWidget, self)._handle_pyout(msg)
94 91
95 92 def _handle_display_data(self, msg):
96 93 """ Overridden to handle rich data types, like SVG.
97 94 """
98 95 if not self._hidden and self._is_from_this_session(msg):
99 96 source = msg['content']['source']
100 97 data = msg['content']['data']
101 98 metadata = msg['content']['metadata']
102 99 # Try to use the svg or html representations.
103 100 # FIXME: Is this the right ordering of things to try?
104 101 if data.has_key('image/svg+xml'):
105 102 svg = data['image/svg+xml']
106 # TODO: try/except this call.
107 self._append_svg(svg)
103 self._append_svg(svg, True)
108 104 elif data.has_key('image/png'):
109 # TODO: try/except these calls
110 105 # PNG data is base64 encoded as it passes over the network
111 106 # in a JSON structure so we decode it.
112 107 png = decodestring(data['image/png'])
113 self._append_png(png)
108 self._append_png(png, True)
114 109 else:
115 110 # Default back to the plain text representation.
116 111 return super(RichIPythonWidget, self)._handle_display_data(msg)
117 112
118 113 #---------------------------------------------------------------------------
119 114 # 'RichIPythonWidget' protected interface
120 115 #---------------------------------------------------------------------------
121 116
122 def _append_svg(self, svg):
123 """ Append raw svg data to the widget.
117 def _append_png(self, png, before_prompt=False):
118 """ Append raw PNG data to the widget.
124 119 """
125 try:
126 image = svg_to_image(svg)
127 except ValueError:
128 self._append_plain_text('Received invalid plot data.')
129 else:
130 format = self._add_image(image)
131 self._name_to_svg_map[format.name()] = svg
132 cursor = self._get_end_cursor()
133 cursor.insertBlock()
134 cursor.insertImage(format)
135 cursor.insertBlock()
120 self._append_custom(self._insert_png, png, before_prompt)
136 121
137 def _append_png(self, png):
138 """ Append raw svg data to the widget.
122 def _append_svg(self, svg, before_prompt=False):
123 """ Append raw SVG data to the widget.
139 124 """
140 try:
141 image = QtGui.QImage()
142 image.loadFromData(png, 'PNG')
143 except ValueError:
144 self._append_plain_text('Received invalid plot data.')
145 else:
146 format = self._add_image(image)
147 cursor = self._get_end_cursor()
148 cursor.insertBlock()
149 cursor.insertImage(format)
150 cursor.insertBlock()
125 self._append_custom(self._insert_svg, svg, before_prompt)
151 126
152 127 def _add_image(self, image):
153 128 """ Adds the specified QImage to the document and returns a
154 129 QTextImageFormat that references it.
155 130 """
156 131 document = self._control.document()
157 132 name = str(image.cacheKey())
158 133 document.addResource(QtGui.QTextDocument.ImageResource,
159 134 QtCore.QUrl(name), image)
160 135 format = QtGui.QTextImageFormat()
161 136 format.setName(name)
162 137 return format
163 138
164 139 def _copy_image(self, name):
165 140 """ Copies the ImageResource with 'name' to the clipboard.
166 141 """
167 142 image = self._get_image(name)
168 143 QtGui.QApplication.clipboard().setImage(image)
169 144
170 145 def _get_image(self, name):
171 146 """ Returns the QImage stored as the ImageResource with 'name'.
172 147 """
173 148 document = self._control.document()
174 149 image = document.resource(QtGui.QTextDocument.ImageResource,
175 150 QtCore.QUrl(name))
176 151 return image
177 152
178 153 def _get_image_tag(self, match, path = None, format = "png"):
179 154 """ Return (X)HTML mark-up for the image-tag given by match.
180 155
181 156 Parameters
182 157 ----------
183 158 match : re.SRE_Match
184 159 A match to an HTML image tag as exported by Qt, with
185 160 match.group("Name") containing the matched image ID.
186 161
187 162 path : string|None, optional [default None]
188 163 If not None, specifies a path to which supporting files may be
189 164 written (e.g., for linked images). If None, all images are to be
190 165 included inline.
191 166
192 167 format : "png"|"svg", optional [default "png"]
193 168 Format for returned or referenced images.
194 169 """
195 170 if format == "png":
196 171 try:
197 172 image = self._get_image(match.group("name"))
198 173 except KeyError:
199 174 return "<b>Couldn't find image %s</b>" % match.group("name")
200 175
201 176 if path is not None:
202 177 if not os.path.exists(path):
203 178 os.mkdir(path)
204 179 relpath = os.path.basename(path)
205 180 if image.save("%s/qt_img%s.png" % (path,match.group("name")),
206 181 "PNG"):
207 182 return '<img src="%s/qt_img%s.png">' % (relpath,
208 183 match.group("name"))
209 184 else:
210 185 return "<b>Couldn't save image!</b>"
211 186 else:
212 187 ba = QtCore.QByteArray()
213 188 buffer_ = QtCore.QBuffer(ba)
214 189 buffer_.open(QtCore.QIODevice.WriteOnly)
215 190 image.save(buffer_, "PNG")
216 191 buffer_.close()
217 192 return '<img src="data:image/png;base64,\n%s\n" />' % (
218 193 re.sub(r'(.{60})',r'\1\n',str(ba.toBase64())))
219 194
220 195 elif format == "svg":
221 196 try:
222 197 svg = str(self._name_to_svg_map[match.group("name")])
223 198 except KeyError:
224 199 return "<b>Couldn't find image %s</b>" % match.group("name")
225 200
226 201 # Not currently checking path, because it's tricky to find a
227 202 # cross-browser way to embed external SVG images (e.g., via
228 203 # object or embed tags).
229 204
230 205 # Chop stand-alone header from matplotlib SVG
231 206 offset = svg.find("<svg")
232 207 assert(offset > -1)
233 208
234 209 return svg[offset:]
235 210
236 211 else:
237 212 return '<b>Unrecognized image format</b>'
238 213
214 def _insert_png(self, cursor, png):
215 """ Insert raw PNG data into the widget.
216 """
217 try:
218 image = QtGui.QImage()
219 image.loadFromData(png, 'PNG')
220 except ValueError:
221 self._insert_plain_text(cursor, 'Received invalid PNG data.')
222 else:
223 format = self._add_image(image)
224 cursor.insertBlock()
225 cursor.insertImage(format)
226 cursor.insertBlock()
227
228 def _insert_svg(self, cursor, svg):
229 """ Insert raw SVG data into the widet.
230 """
231 try:
232 image = svg_to_image(svg)
233 except ValueError:
234 self._insert_plain_text(cursor, 'Received invalid SVG data.')
235 else:
236 format = self._add_image(image)
237 self._name_to_svg_map[format.name()] = svg
238 cursor.insertBlock()
239 cursor.insertImage(format)
240 cursor.insertBlock()
241
239 242 def _save_image(self, name, format='PNG'):
240 243 """ Shows a save dialog for the ImageResource with 'name'.
241 244 """
242 245 dialog = QtGui.QFileDialog(self._control, 'Save Image')
243 246 dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
244 247 dialog.setDefaultSuffix(format.lower())
245 248 dialog.setNameFilter('%s file (*.%s)' % (format, format.lower()))
246 249 if dialog.exec_():
247 250 filename = dialog.selectedFiles()[0]
248 251 image = self._get_image(name)
249 252 image.save(filename, format)
General Comments 0
You need to be logged in to leave comments. Login now