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