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