##// END OF EJS Templates
Merge pull request #5763 from minrk/qt-no-html...
Thomas Kluyver -
r16529:b2c20f7e merge
parent child Browse files
Show More
@@ -1,603 +1,594 b''
1 """A FrontendWidget that emulates the interface of the console IPython.
1 """A FrontendWidget that emulates the interface of the console IPython.
2
2
3 This supports the additional functionality provided by the IPython kernel.
3 This supports the additional functionality provided by the IPython kernel.
4 """
4 """
5
5
6 #-----------------------------------------------------------------------------
6 #-----------------------------------------------------------------------------
7 # Imports
7 # Imports
8 #-----------------------------------------------------------------------------
8 #-----------------------------------------------------------------------------
9
9
10 # Standard library imports
10 # Standard library imports
11 from collections import namedtuple
11 from collections import namedtuple
12 import os.path
12 import os.path
13 import re
13 import re
14 from subprocess import Popen
14 from subprocess import Popen
15 import sys
15 import sys
16 import time
16 import time
17 from textwrap import dedent
17 from textwrap import dedent
18
18
19 # System library imports
19 # System library imports
20 from IPython.external.qt import QtCore, QtGui
20 from IPython.external.qt import QtCore, QtGui
21
21
22 # Local imports
22 # Local imports
23 from IPython.core.inputsplitter import IPythonInputSplitter
23 from IPython.core.inputsplitter import IPythonInputSplitter
24 from IPython.core.inputtransformer import ipy_prompt
24 from IPython.core.inputtransformer import ipy_prompt
25 from IPython.utils.traitlets import Bool, Unicode
25 from IPython.utils.traitlets import Bool, Unicode
26 from .frontend_widget import FrontendWidget
26 from .frontend_widget import FrontendWidget
27 from . import styles
27 from . import styles
28
28
29 #-----------------------------------------------------------------------------
29 #-----------------------------------------------------------------------------
30 # Constants
30 # Constants
31 #-----------------------------------------------------------------------------
31 #-----------------------------------------------------------------------------
32
32
33 # Default strings to build and display input and output prompts (and separators
33 # Default strings to build and display input and output prompts (and separators
34 # in between)
34 # in between)
35 default_in_prompt = 'In [<span class="in-prompt-number">%i</span>]: '
35 default_in_prompt = 'In [<span class="in-prompt-number">%i</span>]: '
36 default_out_prompt = 'Out[<span class="out-prompt-number">%i</span>]: '
36 default_out_prompt = 'Out[<span class="out-prompt-number">%i</span>]: '
37 default_input_sep = '\n'
37 default_input_sep = '\n'
38 default_output_sep = ''
38 default_output_sep = ''
39 default_output_sep2 = ''
39 default_output_sep2 = ''
40
40
41 # Base path for most payload sources.
41 # Base path for most payload sources.
42 zmq_shell_source = 'IPython.kernel.zmq.zmqshell.ZMQInteractiveShell'
42 zmq_shell_source = 'IPython.kernel.zmq.zmqshell.ZMQInteractiveShell'
43
43
44 if sys.platform.startswith('win'):
44 if sys.platform.startswith('win'):
45 default_editor = 'notepad'
45 default_editor = 'notepad'
46 else:
46 else:
47 default_editor = ''
47 default_editor = ''
48
48
49 #-----------------------------------------------------------------------------
49 #-----------------------------------------------------------------------------
50 # IPythonWidget class
50 # IPythonWidget class
51 #-----------------------------------------------------------------------------
51 #-----------------------------------------------------------------------------
52
52
53 class IPythonWidget(FrontendWidget):
53 class IPythonWidget(FrontendWidget):
54 """ A FrontendWidget for an IPython kernel.
54 """ A FrontendWidget for an IPython kernel.
55 """
55 """
56
56
57 # If set, the 'custom_edit_requested(str, int)' signal will be emitted when
57 # If set, the 'custom_edit_requested(str, int)' signal will be emitted when
58 # an editor is needed for a file. This overrides 'editor' and 'editor_line'
58 # an editor is needed for a file. This overrides 'editor' and 'editor_line'
59 # settings.
59 # settings.
60 custom_edit = Bool(False)
60 custom_edit = Bool(False)
61 custom_edit_requested = QtCore.Signal(object, object)
61 custom_edit_requested = QtCore.Signal(object, object)
62
62
63 editor = Unicode(default_editor, config=True,
63 editor = Unicode(default_editor, config=True,
64 help="""
64 help="""
65 A command for invoking a system text editor. If the string contains a
65 A command for invoking a system text editor. If the string contains a
66 {filename} format specifier, it will be used. Otherwise, the filename
66 {filename} format specifier, it will be used. Otherwise, the filename
67 will be appended to the end the command.
67 will be appended to the end the command.
68 """)
68 """)
69
69
70 editor_line = Unicode(config=True,
70 editor_line = Unicode(config=True,
71 help="""
71 help="""
72 The editor command to use when a specific line number is requested. The
72 The editor command to use when a specific line number is requested. The
73 string should contain two format specifiers: {line} and {filename}. If
73 string should contain two format specifiers: {line} and {filename}. If
74 this parameter is not specified, the line number option to the %edit
74 this parameter is not specified, the line number option to the %edit
75 magic will be ignored.
75 magic will be ignored.
76 """)
76 """)
77
77
78 style_sheet = Unicode(config=True,
78 style_sheet = Unicode(config=True,
79 help="""
79 help="""
80 A CSS stylesheet. The stylesheet can contain classes for:
80 A CSS stylesheet. The stylesheet can contain classes for:
81 1. Qt: QPlainTextEdit, QFrame, QWidget, etc
81 1. Qt: QPlainTextEdit, QFrame, QWidget, etc
82 2. Pygments: .c, .k, .o, etc. (see PygmentsHighlighter)
82 2. Pygments: .c, .k, .o, etc. (see PygmentsHighlighter)
83 3. IPython: .error, .in-prompt, .out-prompt, etc
83 3. IPython: .error, .in-prompt, .out-prompt, etc
84 """)
84 """)
85
85
86 syntax_style = Unicode(config=True,
86 syntax_style = Unicode(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 = Unicode(default_in_prompt, config=True)
94 in_prompt = Unicode(default_in_prompt, config=True)
95 out_prompt = Unicode(default_out_prompt, config=True)
95 out_prompt = Unicode(default_out_prompt, config=True)
96 input_sep = Unicode(default_input_sep, config=True)
96 input_sep = Unicode(default_input_sep, config=True)
97 output_sep = Unicode(default_output_sep, config=True)
97 output_sep = Unicode(default_output_sep, config=True)
98 output_sep2 = Unicode(default_output_sep2, config=True)
98 output_sep2 = Unicode(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 _prompt_transformer = IPythonInputSplitter(physical_line_transforms=[ipy_prompt()],
102 _prompt_transformer = IPythonInputSplitter(physical_line_transforms=[ipy_prompt()],
103 logical_line_transforms=[],
103 logical_line_transforms=[],
104 python_line_transforms=[],
104 python_line_transforms=[],
105 )
105 )
106
106
107 # IPythonWidget protected class variables.
107 # IPythonWidget protected class variables.
108 _PromptBlock = namedtuple('_PromptBlock', ['block', 'length', 'number'])
108 _PromptBlock = namedtuple('_PromptBlock', ['block', 'length', 'number'])
109 _payload_source_edit = 'edit_magic'
109 _payload_source_edit = 'edit_magic'
110 _payload_source_exit = 'ask_exit'
110 _payload_source_exit = 'ask_exit'
111 _payload_source_next_input = 'set_next_input'
111 _payload_source_next_input = 'set_next_input'
112 _payload_source_page = 'page'
112 _payload_source_page = 'page'
113 _retrying_history_request = False
113 _retrying_history_request = False
114
114
115 #---------------------------------------------------------------------------
115 #---------------------------------------------------------------------------
116 # 'object' interface
116 # 'object' interface
117 #---------------------------------------------------------------------------
117 #---------------------------------------------------------------------------
118
118
119 def __init__(self, *args, **kw):
119 def __init__(self, *args, **kw):
120 super(IPythonWidget, self).__init__(*args, **kw)
120 super(IPythonWidget, self).__init__(*args, **kw)
121
121
122 # IPythonWidget protected variables.
122 # IPythonWidget protected variables.
123 self._payload_handlers = {
123 self._payload_handlers = {
124 self._payload_source_edit : self._handle_payload_edit,
124 self._payload_source_edit : self._handle_payload_edit,
125 self._payload_source_exit : self._handle_payload_exit,
125 self._payload_source_exit : self._handle_payload_exit,
126 self._payload_source_page : self._handle_payload_page,
126 self._payload_source_page : self._handle_payload_page,
127 self._payload_source_next_input : self._handle_payload_next_input }
127 self._payload_source_next_input : self._handle_payload_next_input }
128 self._previous_prompt_obj = None
128 self._previous_prompt_obj = None
129 self._keep_kernel_on_exit = None
129 self._keep_kernel_on_exit = None
130
130
131 # Initialize widget styling.
131 # Initialize widget styling.
132 if self.style_sheet:
132 if self.style_sheet:
133 self._style_sheet_changed()
133 self._style_sheet_changed()
134 self._syntax_style_changed()
134 self._syntax_style_changed()
135 else:
135 else:
136 self.set_default_style()
136 self.set_default_style()
137
137
138 self._guiref_loaded = False
138 self._guiref_loaded = False
139
139
140 #---------------------------------------------------------------------------
140 #---------------------------------------------------------------------------
141 # 'BaseFrontendMixin' abstract interface
141 # 'BaseFrontendMixin' abstract interface
142 #---------------------------------------------------------------------------
142 #---------------------------------------------------------------------------
143 def _handle_complete_reply(self, rep):
143 def _handle_complete_reply(self, rep):
144 """ Reimplemented to support IPython's improved completion machinery.
144 """ Reimplemented to support IPython's improved completion machinery.
145 """
145 """
146 self.log.debug("complete: %s", rep.get('content', ''))
146 self.log.debug("complete: %s", rep.get('content', ''))
147 cursor = self._get_cursor()
147 cursor = self._get_cursor()
148 info = self._request_info.get('complete')
148 info = self._request_info.get('complete')
149 if info and info.id == rep['parent_header']['msg_id'] and \
149 if info and info.id == rep['parent_header']['msg_id'] and \
150 info.pos == cursor.position():
150 info.pos == cursor.position():
151 matches = rep['content']['matches']
151 matches = rep['content']['matches']
152 text = rep['content']['matched_text']
152 text = rep['content']['matched_text']
153 offset = len(text)
153 offset = len(text)
154
154
155 # Clean up matches with period and path separators if the matched
155 # Clean up matches with period and path separators if the matched
156 # text has not been transformed. This is done by truncating all
156 # text has not been transformed. This is done by truncating all
157 # but the last component and then suitably decreasing the offset
157 # but the last component and then suitably decreasing the offset
158 # between the current cursor position and the start of completion.
158 # between the current cursor position and the start of completion.
159 if len(matches) > 1 and matches[0][:offset] == text:
159 if len(matches) > 1 and matches[0][:offset] == text:
160 parts = re.split(r'[./\\]', text)
160 parts = re.split(r'[./\\]', text)
161 sep_count = len(parts) - 1
161 sep_count = len(parts) - 1
162 if sep_count:
162 if sep_count:
163 chop_length = sum(map(len, parts[:sep_count])) + sep_count
163 chop_length = sum(map(len, parts[:sep_count])) + sep_count
164 matches = [ match[chop_length:] for match in matches ]
164 matches = [ match[chop_length:] for match in matches ]
165 offset -= chop_length
165 offset -= chop_length
166
166
167 # Move the cursor to the start of the match and complete.
167 # Move the cursor to the start of the match and complete.
168 cursor.movePosition(QtGui.QTextCursor.Left, n=offset)
168 cursor.movePosition(QtGui.QTextCursor.Left, n=offset)
169 self._complete_with_items(cursor, matches)
169 self._complete_with_items(cursor, matches)
170
170
171 def _handle_execute_reply(self, msg):
171 def _handle_execute_reply(self, msg):
172 """ Reimplemented to support prompt requests.
172 """ Reimplemented to support prompt requests.
173 """
173 """
174 msg_id = msg['parent_header'].get('msg_id')
174 msg_id = msg['parent_header'].get('msg_id')
175 info = self._request_info['execute'].get(msg_id)
175 info = self._request_info['execute'].get(msg_id)
176 if info and info.kind == 'prompt':
176 if info and info.kind == 'prompt':
177 content = msg['content']
177 content = msg['content']
178 if content['status'] == 'aborted':
178 if content['status'] == 'aborted':
179 self._show_interpreter_prompt()
179 self._show_interpreter_prompt()
180 else:
180 else:
181 number = content['execution_count'] + 1
181 number = content['execution_count'] + 1
182 self._show_interpreter_prompt(number)
182 self._show_interpreter_prompt(number)
183 self._request_info['execute'].pop(msg_id)
183 self._request_info['execute'].pop(msg_id)
184 else:
184 else:
185 super(IPythonWidget, self)._handle_execute_reply(msg)
185 super(IPythonWidget, self)._handle_execute_reply(msg)
186
186
187 def _handle_history_reply(self, msg):
187 def _handle_history_reply(self, msg):
188 """ Implemented to handle history tail replies, which are only supported
188 """ Implemented to handle history tail replies, which are only supported
189 by the IPython kernel.
189 by the IPython kernel.
190 """
190 """
191 content = msg['content']
191 content = msg['content']
192 if 'history' not in content:
192 if 'history' not in content:
193 self.log.error("History request failed: %r"%content)
193 self.log.error("History request failed: %r"%content)
194 if content.get('status', '') == 'aborted' and \
194 if content.get('status', '') == 'aborted' and \
195 not self._retrying_history_request:
195 not self._retrying_history_request:
196 # a *different* action caused this request to be aborted, so
196 # a *different* action caused this request to be aborted, so
197 # we should try again.
197 # we should try again.
198 self.log.error("Retrying aborted history request")
198 self.log.error("Retrying aborted history request")
199 # prevent multiple retries of aborted requests:
199 # prevent multiple retries of aborted requests:
200 self._retrying_history_request = True
200 self._retrying_history_request = True
201 # wait out the kernel's queue flush, which is currently timed at 0.1s
201 # wait out the kernel's queue flush, which is currently timed at 0.1s
202 time.sleep(0.25)
202 time.sleep(0.25)
203 self.kernel_client.shell_channel.history(hist_access_type='tail',n=1000)
203 self.kernel_client.shell_channel.history(hist_access_type='tail',n=1000)
204 else:
204 else:
205 self._retrying_history_request = False
205 self._retrying_history_request = False
206 return
206 return
207 # reset retry flag
207 # reset retry flag
208 self._retrying_history_request = False
208 self._retrying_history_request = False
209 history_items = content['history']
209 history_items = content['history']
210 self.log.debug("Received history reply with %i entries", len(history_items))
210 self.log.debug("Received history reply with %i entries", len(history_items))
211 items = []
211 items = []
212 last_cell = u""
212 last_cell = u""
213 for _, _, cell in history_items:
213 for _, _, cell in history_items:
214 cell = cell.rstrip()
214 cell = cell.rstrip()
215 if cell != last_cell:
215 if cell != last_cell:
216 items.append(cell)
216 items.append(cell)
217 last_cell = cell
217 last_cell = cell
218 self._set_history(items)
218 self._set_history(items)
219
219
220 def _handle_pyout(self, msg):
220 def _handle_pyout(self, msg):
221 """ Reimplemented for IPython-style "display hook".
221 """ Reimplemented for IPython-style "display hook".
222 """
222 """
223 self.log.debug("pyout: %s", msg.get('content', ''))
223 self.log.debug("pyout: %s", msg.get('content', ''))
224 if not self._hidden and self._is_from_this_session(msg):
224 if not self._hidden and self._is_from_this_session(msg):
225 self.flush_clearoutput()
225 self.flush_clearoutput()
226 content = msg['content']
226 content = msg['content']
227 prompt_number = content.get('execution_count', 0)
227 prompt_number = content.get('execution_count', 0)
228 data = content['data']
228 data = content['data']
229 if 'text/html' in data:
229 if 'text/plain' in data:
230 self._append_plain_text(self.output_sep, True)
231 self._append_html(self._make_out_prompt(prompt_number), True)
232 html = data['text/html']
233 self._append_plain_text('\n', True)
234 self._append_html(html + self.output_sep2, True)
235 elif 'text/plain' in data:
236 self._append_plain_text(self.output_sep, True)
230 self._append_plain_text(self.output_sep, True)
237 self._append_html(self._make_out_prompt(prompt_number), True)
231 self._append_html(self._make_out_prompt(prompt_number), True)
238 text = data['text/plain']
232 text = data['text/plain']
239 # If the repr is multiline, make sure we start on a new line,
233 # If the repr is multiline, make sure we start on a new line,
240 # so that its lines are aligned.
234 # so that its lines are aligned.
241 if "\n" in text and not self.output_sep.endswith("\n"):
235 if "\n" in text and not self.output_sep.endswith("\n"):
242 self._append_plain_text('\n', True)
236 self._append_plain_text('\n', True)
243 self._append_plain_text(text + self.output_sep2, True)
237 self._append_plain_text(text + self.output_sep2, True)
244
238
245 def _handle_display_data(self, msg):
239 def _handle_display_data(self, msg):
246 """ The base handler for the ``display_data`` message.
240 """ The base handler for the ``display_data`` message.
247 """
241 """
248 self.log.debug("display: %s", msg.get('content', ''))
242 self.log.debug("display: %s", msg.get('content', ''))
249 # For now, we don't display data from other frontends, but we
243 # For now, we don't display data from other frontends, but we
250 # eventually will as this allows all frontends to monitor the display
244 # eventually will as this allows all frontends to monitor the display
251 # data. But we need to figure out how to handle this in the GUI.
245 # data. But we need to figure out how to handle this in the GUI.
252 if not self._hidden and self._is_from_this_session(msg):
246 if not self._hidden and self._is_from_this_session(msg):
253 self.flush_clearoutput()
247 self.flush_clearoutput()
254 source = msg['content']['source']
248 source = msg['content']['source']
255 data = msg['content']['data']
249 data = msg['content']['data']
256 metadata = msg['content']['metadata']
250 metadata = msg['content']['metadata']
257 # In the regular IPythonWidget, we simply print the plain text
251 # In the regular IPythonWidget, we simply print the plain text
258 # representation.
252 # representation.
259 if 'text/html' in data:
253 if 'text/plain' in data:
260 html = data['text/html']
261 self._append_html(html, True)
262 elif 'text/plain' in data:
263 text = data['text/plain']
254 text = data['text/plain']
264 self._append_plain_text(text, True)
255 self._append_plain_text(text, True)
265 # This newline seems to be needed for text and html output.
256 # This newline seems to be needed for text and html output.
266 self._append_plain_text(u'\n', True)
257 self._append_plain_text(u'\n', True)
267
258
268 def _handle_kernel_info_reply(self, rep):
259 def _handle_kernel_info_reply(self, rep):
269 """ Handle kernel info replies.
260 """ Handle kernel info replies.
270 """
261 """
271 if not self._guiref_loaded:
262 if not self._guiref_loaded:
272 if rep['content'].get('language') == 'python':
263 if rep['content'].get('language') == 'python':
273 self._load_guiref_magic()
264 self._load_guiref_magic()
274 self._guiref_loaded = True
265 self._guiref_loaded = True
275
266
276 def _started_channels(self):
267 def _started_channels(self):
277 """Reimplemented to make a history request and load %guiref."""
268 """Reimplemented to make a history request and load %guiref."""
278 super(IPythonWidget, self)._started_channels()
269 super(IPythonWidget, self)._started_channels()
279
270
280 # The reply will trigger %guiref load provided language=='python'
271 # The reply will trigger %guiref load provided language=='python'
281 self.kernel_client.kernel_info()
272 self.kernel_client.kernel_info()
282
273
283 self.kernel_client.shell_channel.history(hist_access_type='tail',
274 self.kernel_client.shell_channel.history(hist_access_type='tail',
284 n=1000)
275 n=1000)
285
276
286 def _started_kernel(self):
277 def _started_kernel(self):
287 """Load %guiref when the kernel starts (if channels are also started).
278 """Load %guiref when the kernel starts (if channels are also started).
288
279
289 Principally triggered by kernel restart.
280 Principally triggered by kernel restart.
290 """
281 """
291 if self.kernel_client.shell_channel is not None:
282 if self.kernel_client.shell_channel is not None:
292 self._load_guiref_magic()
283 self._load_guiref_magic()
293
284
294 def _load_guiref_magic(self):
285 def _load_guiref_magic(self):
295 """Load %guiref magic."""
286 """Load %guiref magic."""
296 self.kernel_client.shell_channel.execute('\n'.join([
287 self.kernel_client.shell_channel.execute('\n'.join([
297 "try:",
288 "try:",
298 " _usage",
289 " _usage",
299 "except:",
290 "except:",
300 " from IPython.core import usage as _usage",
291 " from IPython.core import usage as _usage",
301 " get_ipython().register_magic_function(_usage.page_guiref, 'line', 'guiref')",
292 " get_ipython().register_magic_function(_usage.page_guiref, 'line', 'guiref')",
302 " del _usage",
293 " del _usage",
303 ]), silent=True)
294 ]), silent=True)
304
295
305 #---------------------------------------------------------------------------
296 #---------------------------------------------------------------------------
306 # 'ConsoleWidget' public interface
297 # 'ConsoleWidget' public interface
307 #---------------------------------------------------------------------------
298 #---------------------------------------------------------------------------
308
299
309 #---------------------------------------------------------------------------
300 #---------------------------------------------------------------------------
310 # 'FrontendWidget' public interface
301 # 'FrontendWidget' public interface
311 #---------------------------------------------------------------------------
302 #---------------------------------------------------------------------------
312
303
313 def execute_file(self, path, hidden=False):
304 def execute_file(self, path, hidden=False):
314 """ Reimplemented to use the 'run' magic.
305 """ Reimplemented to use the 'run' magic.
315 """
306 """
316 # Use forward slashes on Windows to avoid escaping each separator.
307 # Use forward slashes on Windows to avoid escaping each separator.
317 if sys.platform == 'win32':
308 if sys.platform == 'win32':
318 path = os.path.normpath(path).replace('\\', '/')
309 path = os.path.normpath(path).replace('\\', '/')
319
310
320 # Perhaps we should not be using %run directly, but while we
311 # Perhaps we should not be using %run directly, but while we
321 # are, it is necessary to quote or escape filenames containing spaces
312 # are, it is necessary to quote or escape filenames containing spaces
322 # or quotes.
313 # or quotes.
323
314
324 # In earlier code here, to minimize escaping, we sometimes quoted the
315 # In earlier code here, to minimize escaping, we sometimes quoted the
325 # filename with single quotes. But to do this, this code must be
316 # filename with single quotes. But to do this, this code must be
326 # platform-aware, because run uses shlex rather than python string
317 # platform-aware, because run uses shlex rather than python string
327 # parsing, so that:
318 # parsing, so that:
328 # * In Win: single quotes can be used in the filename without quoting,
319 # * In Win: single quotes can be used in the filename without quoting,
329 # and we cannot use single quotes to quote the filename.
320 # and we cannot use single quotes to quote the filename.
330 # * In *nix: we can escape double quotes in a double quoted filename,
321 # * In *nix: we can escape double quotes in a double quoted filename,
331 # but can't escape single quotes in a single quoted filename.
322 # but can't escape single quotes in a single quoted filename.
332
323
333 # So to keep this code non-platform-specific and simple, we now only
324 # So to keep this code non-platform-specific and simple, we now only
334 # use double quotes to quote filenames, and escape when needed:
325 # use double quotes to quote filenames, and escape when needed:
335 if ' ' in path or "'" in path or '"' in path:
326 if ' ' in path or "'" in path or '"' in path:
336 path = '"%s"' % path.replace('"', '\\"')
327 path = '"%s"' % path.replace('"', '\\"')
337 self.execute('%%run %s' % path, hidden=hidden)
328 self.execute('%%run %s' % path, hidden=hidden)
338
329
339 #---------------------------------------------------------------------------
330 #---------------------------------------------------------------------------
340 # 'FrontendWidget' protected interface
331 # 'FrontendWidget' protected interface
341 #---------------------------------------------------------------------------
332 #---------------------------------------------------------------------------
342
333
343 def _complete(self):
334 def _complete(self):
344 """ Reimplemented to support IPython's improved completion machinery.
335 """ Reimplemented to support IPython's improved completion machinery.
345 """
336 """
346 # We let the kernel split the input line, so we *always* send an empty
337 # We let the kernel split the input line, so we *always* send an empty
347 # text field. Readline-based frontends do get a real text field which
338 # text field. Readline-based frontends do get a real text field which
348 # they can use.
339 # they can use.
349 text = ''
340 text = ''
350
341
351 # Send the completion request to the kernel
342 # Send the completion request to the kernel
352 msg_id = self.kernel_client.shell_channel.complete(
343 msg_id = self.kernel_client.shell_channel.complete(
353 text, # text
344 text, # text
354 self._get_input_buffer_cursor_line(), # line
345 self._get_input_buffer_cursor_line(), # line
355 self._get_input_buffer_cursor_column(), # cursor_pos
346 self._get_input_buffer_cursor_column(), # cursor_pos
356 self.input_buffer) # block
347 self.input_buffer) # block
357 pos = self._get_cursor().position()
348 pos = self._get_cursor().position()
358 info = self._CompletionRequest(msg_id, pos)
349 info = self._CompletionRequest(msg_id, pos)
359 self._request_info['complete'] = info
350 self._request_info['complete'] = info
360
351
361 def _process_execute_error(self, msg):
352 def _process_execute_error(self, msg):
362 """ Reimplemented for IPython-style traceback formatting.
353 """ Reimplemented for IPython-style traceback formatting.
363 """
354 """
364 content = msg['content']
355 content = msg['content']
365 traceback = '\n'.join(content['traceback']) + '\n'
356 traceback = '\n'.join(content['traceback']) + '\n'
366 if False:
357 if False:
367 # FIXME: For now, tracebacks come as plain text, so we can't use
358 # FIXME: For now, tracebacks come as plain text, so we can't use
368 # the html renderer yet. Once we refactor ultratb to produce
359 # the html renderer yet. Once we refactor ultratb to produce
369 # properly styled tracebacks, this branch should be the default
360 # properly styled tracebacks, this branch should be the default
370 traceback = traceback.replace(' ', '&nbsp;')
361 traceback = traceback.replace(' ', '&nbsp;')
371 traceback = traceback.replace('\n', '<br/>')
362 traceback = traceback.replace('\n', '<br/>')
372
363
373 ename = content['ename']
364 ename = content['ename']
374 ename_styled = '<span class="error">%s</span>' % ename
365 ename_styled = '<span class="error">%s</span>' % ename
375 traceback = traceback.replace(ename, ename_styled)
366 traceback = traceback.replace(ename, ename_styled)
376
367
377 self._append_html(traceback)
368 self._append_html(traceback)
378 else:
369 else:
379 # This is the fallback for now, using plain text with ansi escapes
370 # This is the fallback for now, using plain text with ansi escapes
380 self._append_plain_text(traceback)
371 self._append_plain_text(traceback)
381
372
382 def _process_execute_payload(self, item):
373 def _process_execute_payload(self, item):
383 """ Reimplemented to dispatch payloads to handler methods.
374 """ Reimplemented to dispatch payloads to handler methods.
384 """
375 """
385 handler = self._payload_handlers.get(item['source'])
376 handler = self._payload_handlers.get(item['source'])
386 if handler is None:
377 if handler is None:
387 # We have no handler for this type of payload, simply ignore it
378 # We have no handler for this type of payload, simply ignore it
388 return False
379 return False
389 else:
380 else:
390 handler(item)
381 handler(item)
391 return True
382 return True
392
383
393 def _show_interpreter_prompt(self, number=None):
384 def _show_interpreter_prompt(self, number=None):
394 """ Reimplemented for IPython-style prompts.
385 """ Reimplemented for IPython-style prompts.
395 """
386 """
396 # If a number was not specified, make a prompt number request.
387 # If a number was not specified, make a prompt number request.
397 if number is None:
388 if number is None:
398 msg_id = self.kernel_client.shell_channel.execute('', silent=True)
389 msg_id = self.kernel_client.shell_channel.execute('', silent=True)
399 info = self._ExecutionRequest(msg_id, 'prompt')
390 info = self._ExecutionRequest(msg_id, 'prompt')
400 self._request_info['execute'][msg_id] = info
391 self._request_info['execute'][msg_id] = info
401 return
392 return
402
393
403 # Show a new prompt and save information about it so that it can be
394 # Show a new prompt and save information about it so that it can be
404 # updated later if the prompt number turns out to be wrong.
395 # updated later if the prompt number turns out to be wrong.
405 self._prompt_sep = self.input_sep
396 self._prompt_sep = self.input_sep
406 self._show_prompt(self._make_in_prompt(number), html=True)
397 self._show_prompt(self._make_in_prompt(number), html=True)
407 block = self._control.document().lastBlock()
398 block = self._control.document().lastBlock()
408 length = len(self._prompt)
399 length = len(self._prompt)
409 self._previous_prompt_obj = self._PromptBlock(block, length, number)
400 self._previous_prompt_obj = self._PromptBlock(block, length, number)
410
401
411 # Update continuation prompt to reflect (possibly) new prompt length.
402 # Update continuation prompt to reflect (possibly) new prompt length.
412 self._set_continuation_prompt(
403 self._set_continuation_prompt(
413 self._make_continuation_prompt(self._prompt), html=True)
404 self._make_continuation_prompt(self._prompt), html=True)
414
405
415 def _show_interpreter_prompt_for_reply(self, msg):
406 def _show_interpreter_prompt_for_reply(self, msg):
416 """ Reimplemented for IPython-style prompts.
407 """ Reimplemented for IPython-style prompts.
417 """
408 """
418 # Update the old prompt number if necessary.
409 # Update the old prompt number if necessary.
419 content = msg['content']
410 content = msg['content']
420 # abort replies do not have any keys:
411 # abort replies do not have any keys:
421 if content['status'] == 'aborted':
412 if content['status'] == 'aborted':
422 if self._previous_prompt_obj:
413 if self._previous_prompt_obj:
423 previous_prompt_number = self._previous_prompt_obj.number
414 previous_prompt_number = self._previous_prompt_obj.number
424 else:
415 else:
425 previous_prompt_number = 0
416 previous_prompt_number = 0
426 else:
417 else:
427 previous_prompt_number = content['execution_count']
418 previous_prompt_number = content['execution_count']
428 if self._previous_prompt_obj and \
419 if self._previous_prompt_obj and \
429 self._previous_prompt_obj.number != previous_prompt_number:
420 self._previous_prompt_obj.number != previous_prompt_number:
430 block = self._previous_prompt_obj.block
421 block = self._previous_prompt_obj.block
431
422
432 # Make sure the prompt block has not been erased.
423 # Make sure the prompt block has not been erased.
433 if block.isValid() and block.text():
424 if block.isValid() and block.text():
434
425
435 # Remove the old prompt and insert a new prompt.
426 # Remove the old prompt and insert a new prompt.
436 cursor = QtGui.QTextCursor(block)
427 cursor = QtGui.QTextCursor(block)
437 cursor.movePosition(QtGui.QTextCursor.Right,
428 cursor.movePosition(QtGui.QTextCursor.Right,
438 QtGui.QTextCursor.KeepAnchor,
429 QtGui.QTextCursor.KeepAnchor,
439 self._previous_prompt_obj.length)
430 self._previous_prompt_obj.length)
440 prompt = self._make_in_prompt(previous_prompt_number)
431 prompt = self._make_in_prompt(previous_prompt_number)
441 self._prompt = self._insert_html_fetching_plain_text(
432 self._prompt = self._insert_html_fetching_plain_text(
442 cursor, prompt)
433 cursor, prompt)
443
434
444 # When the HTML is inserted, Qt blows away the syntax
435 # When the HTML is inserted, Qt blows away the syntax
445 # highlighting for the line, so we need to rehighlight it.
436 # highlighting for the line, so we need to rehighlight it.
446 self._highlighter.rehighlightBlock(cursor.block())
437 self._highlighter.rehighlightBlock(cursor.block())
447
438
448 self._previous_prompt_obj = None
439 self._previous_prompt_obj = None
449
440
450 # Show a new prompt with the kernel's estimated prompt number.
441 # Show a new prompt with the kernel's estimated prompt number.
451 self._show_interpreter_prompt(previous_prompt_number + 1)
442 self._show_interpreter_prompt(previous_prompt_number + 1)
452
443
453 #---------------------------------------------------------------------------
444 #---------------------------------------------------------------------------
454 # 'IPythonWidget' interface
445 # 'IPythonWidget' interface
455 #---------------------------------------------------------------------------
446 #---------------------------------------------------------------------------
456
447
457 def set_default_style(self, colors='lightbg'):
448 def set_default_style(self, colors='lightbg'):
458 """ Sets the widget style to the class defaults.
449 """ Sets the widget style to the class defaults.
459
450
460 Parameters
451 Parameters
461 ----------
452 ----------
462 colors : str, optional (default lightbg)
453 colors : str, optional (default lightbg)
463 Whether to use the default IPython light background or dark
454 Whether to use the default IPython light background or dark
464 background or B&W style.
455 background or B&W style.
465 """
456 """
466 colors = colors.lower()
457 colors = colors.lower()
467 if colors=='lightbg':
458 if colors=='lightbg':
468 self.style_sheet = styles.default_light_style_sheet
459 self.style_sheet = styles.default_light_style_sheet
469 self.syntax_style = styles.default_light_syntax_style
460 self.syntax_style = styles.default_light_syntax_style
470 elif colors=='linux':
461 elif colors=='linux':
471 self.style_sheet = styles.default_dark_style_sheet
462 self.style_sheet = styles.default_dark_style_sheet
472 self.syntax_style = styles.default_dark_syntax_style
463 self.syntax_style = styles.default_dark_syntax_style
473 elif colors=='nocolor':
464 elif colors=='nocolor':
474 self.style_sheet = styles.default_bw_style_sheet
465 self.style_sheet = styles.default_bw_style_sheet
475 self.syntax_style = styles.default_bw_syntax_style
466 self.syntax_style = styles.default_bw_syntax_style
476 else:
467 else:
477 raise KeyError("No such color scheme: %s"%colors)
468 raise KeyError("No such color scheme: %s"%colors)
478
469
479 #---------------------------------------------------------------------------
470 #---------------------------------------------------------------------------
480 # 'IPythonWidget' protected interface
471 # 'IPythonWidget' protected interface
481 #---------------------------------------------------------------------------
472 #---------------------------------------------------------------------------
482
473
483 def _edit(self, filename, line=None):
474 def _edit(self, filename, line=None):
484 """ Opens a Python script for editing.
475 """ Opens a Python script for editing.
485
476
486 Parameters
477 Parameters
487 ----------
478 ----------
488 filename : str
479 filename : str
489 A path to a local system file.
480 A path to a local system file.
490
481
491 line : int, optional
482 line : int, optional
492 A line of interest in the file.
483 A line of interest in the file.
493 """
484 """
494 if self.custom_edit:
485 if self.custom_edit:
495 self.custom_edit_requested.emit(filename, line)
486 self.custom_edit_requested.emit(filename, line)
496 elif not self.editor:
487 elif not self.editor:
497 self._append_plain_text('No default editor available.\n'
488 self._append_plain_text('No default editor available.\n'
498 'Specify a GUI text editor in the `IPythonWidget.editor` '
489 'Specify a GUI text editor in the `IPythonWidget.editor` '
499 'configurable to enable the %edit magic')
490 'configurable to enable the %edit magic')
500 else:
491 else:
501 try:
492 try:
502 filename = '"%s"' % filename
493 filename = '"%s"' % filename
503 if line and self.editor_line:
494 if line and self.editor_line:
504 command = self.editor_line.format(filename=filename,
495 command = self.editor_line.format(filename=filename,
505 line=line)
496 line=line)
506 else:
497 else:
507 try:
498 try:
508 command = self.editor.format()
499 command = self.editor.format()
509 except KeyError:
500 except KeyError:
510 command = self.editor.format(filename=filename)
501 command = self.editor.format(filename=filename)
511 else:
502 else:
512 command += ' ' + filename
503 command += ' ' + filename
513 except KeyError:
504 except KeyError:
514 self._append_plain_text('Invalid editor command.\n')
505 self._append_plain_text('Invalid editor command.\n')
515 else:
506 else:
516 try:
507 try:
517 Popen(command, shell=True)
508 Popen(command, shell=True)
518 except OSError:
509 except OSError:
519 msg = 'Opening editor with command "%s" failed.\n'
510 msg = 'Opening editor with command "%s" failed.\n'
520 self._append_plain_text(msg % command)
511 self._append_plain_text(msg % command)
521
512
522 def _make_in_prompt(self, number):
513 def _make_in_prompt(self, number):
523 """ Given a prompt number, returns an HTML In prompt.
514 """ Given a prompt number, returns an HTML In prompt.
524 """
515 """
525 try:
516 try:
526 body = self.in_prompt % number
517 body = self.in_prompt % number
527 except TypeError:
518 except TypeError:
528 # allow in_prompt to leave out number, e.g. '>>> '
519 # allow in_prompt to leave out number, e.g. '>>> '
529 body = self.in_prompt
520 body = self.in_prompt
530 return '<span class="in-prompt">%s</span>' % body
521 return '<span class="in-prompt">%s</span>' % body
531
522
532 def _make_continuation_prompt(self, prompt):
523 def _make_continuation_prompt(self, prompt):
533 """ Given a plain text version of an In prompt, returns an HTML
524 """ Given a plain text version of an In prompt, returns an HTML
534 continuation prompt.
525 continuation prompt.
535 """
526 """
536 end_chars = '...: '
527 end_chars = '...: '
537 space_count = len(prompt.lstrip('\n')) - len(end_chars)
528 space_count = len(prompt.lstrip('\n')) - len(end_chars)
538 body = '&nbsp;' * space_count + end_chars
529 body = '&nbsp;' * space_count + end_chars
539 return '<span class="in-prompt">%s</span>' % body
530 return '<span class="in-prompt">%s</span>' % body
540
531
541 def _make_out_prompt(self, number):
532 def _make_out_prompt(self, number):
542 """ Given a prompt number, returns an HTML Out prompt.
533 """ Given a prompt number, returns an HTML Out prompt.
543 """
534 """
544 body = self.out_prompt % number
535 body = self.out_prompt % number
545 return '<span class="out-prompt">%s</span>' % body
536 return '<span class="out-prompt">%s</span>' % body
546
537
547 #------ Payload handlers --------------------------------------------------
538 #------ Payload handlers --------------------------------------------------
548
539
549 # Payload handlers with a generic interface: each takes the opaque payload
540 # Payload handlers with a generic interface: each takes the opaque payload
550 # dict, unpacks it and calls the underlying functions with the necessary
541 # dict, unpacks it and calls the underlying functions with the necessary
551 # arguments.
542 # arguments.
552
543
553 def _handle_payload_edit(self, item):
544 def _handle_payload_edit(self, item):
554 self._edit(item['filename'], item['line_number'])
545 self._edit(item['filename'], item['line_number'])
555
546
556 def _handle_payload_exit(self, item):
547 def _handle_payload_exit(self, item):
557 self._keep_kernel_on_exit = item['keepkernel']
548 self._keep_kernel_on_exit = item['keepkernel']
558 self.exit_requested.emit(self)
549 self.exit_requested.emit(self)
559
550
560 def _handle_payload_next_input(self, item):
551 def _handle_payload_next_input(self, item):
561 self.input_buffer = item['text']
552 self.input_buffer = item['text']
562
553
563 def _handle_payload_page(self, item):
554 def _handle_payload_page(self, item):
564 # Since the plain text widget supports only a very small subset of HTML
555 # Since the plain text widget supports only a very small subset of HTML
565 # and we have no control over the HTML source, we only page HTML
556 # and we have no control over the HTML source, we only page HTML
566 # payloads in the rich text widget.
557 # payloads in the rich text widget.
567 if item['html'] and self.kind == 'rich':
558 if item['html'] and self.kind == 'rich':
568 self._page(item['html'], html=True)
559 self._page(item['html'], html=True)
569 else:
560 else:
570 self._page(item['text'], html=False)
561 self._page(item['text'], html=False)
571
562
572 #------ Trait change handlers --------------------------------------------
563 #------ Trait change handlers --------------------------------------------
573
564
574 def _style_sheet_changed(self):
565 def _style_sheet_changed(self):
575 """ Set the style sheets of the underlying widgets.
566 """ Set the style sheets of the underlying widgets.
576 """
567 """
577 self.setStyleSheet(self.style_sheet)
568 self.setStyleSheet(self.style_sheet)
578 if self._control is not None:
569 if self._control is not None:
579 self._control.document().setDefaultStyleSheet(self.style_sheet)
570 self._control.document().setDefaultStyleSheet(self.style_sheet)
580 bg_color = self._control.palette().window().color()
571 bg_color = self._control.palette().window().color()
581 self._ansi_processor.set_background_color(bg_color)
572 self._ansi_processor.set_background_color(bg_color)
582
573
583 if self._page_control is not None:
574 if self._page_control is not None:
584 self._page_control.document().setDefaultStyleSheet(self.style_sheet)
575 self._page_control.document().setDefaultStyleSheet(self.style_sheet)
585
576
586
577
587
578
588 def _syntax_style_changed(self):
579 def _syntax_style_changed(self):
589 """ Set the style for the syntax highlighter.
580 """ Set the style for the syntax highlighter.
590 """
581 """
591 if self._highlighter is None:
582 if self._highlighter is None:
592 # ignore premature calls
583 # ignore premature calls
593 return
584 return
594 if self.syntax_style:
585 if self.syntax_style:
595 self._highlighter.set_style(self.syntax_style)
586 self._highlighter.set_style(self.syntax_style)
596 else:
587 else:
597 self._highlighter.set_style_sheet(self.style_sheet)
588 self._highlighter.set_style_sheet(self.style_sheet)
598
589
599 #------ Trait default initializers -----------------------------------------
590 #------ Trait default initializers -----------------------------------------
600
591
601 def _banner_default(self):
592 def _banner_default(self):
602 from IPython.core.usage import default_gui_banner
593 from IPython.core.usage import default_gui_banner
603 return default_gui_banner
594 return default_gui_banner
General Comments 0
You need to be logged in to leave comments. Login now