##// END OF EJS Templates
Merge pull request #963 from minrk/pyside...
Min RK -
r5286:23b354bf merge
parent child Browse files
Show More
@@ -1,659 +1,667 b''
1 from __future__ import print_function
1 from __future__ import print_function
2
2
3 # Standard library imports
3 # Standard library imports
4 from collections import namedtuple
4 from collections import namedtuple
5 import sys
5 import sys
6 import time
6 import time
7
7
8 # System library imports
8 # System library imports
9 from pygments.lexers import PythonLexer
9 from pygments.lexers import PythonLexer
10 from IPython.external import qt
10 from IPython.external.qt import QtCore, QtGui
11 from IPython.external.qt import QtCore, QtGui
11
12
12 # Local imports
13 # Local imports
13 from IPython.core.inputsplitter import InputSplitter, transform_classic_prompt
14 from IPython.core.inputsplitter import InputSplitter, transform_classic_prompt
14 from IPython.core.oinspect import call_tip
15 from IPython.core.oinspect import call_tip
15 from IPython.frontend.qt.base_frontend_mixin import BaseFrontendMixin
16 from IPython.frontend.qt.base_frontend_mixin import BaseFrontendMixin
16 from IPython.utils.traitlets import Bool, Instance, Unicode
17 from IPython.utils.traitlets import Bool, Instance, Unicode
17 from bracket_matcher import BracketMatcher
18 from bracket_matcher import BracketMatcher
18 from call_tip_widget import CallTipWidget
19 from call_tip_widget import CallTipWidget
19 from completion_lexer import CompletionLexer
20 from completion_lexer import CompletionLexer
20 from history_console_widget import HistoryConsoleWidget
21 from history_console_widget import HistoryConsoleWidget
21 from pygments_highlighter import PygmentsHighlighter
22 from pygments_highlighter import PygmentsHighlighter
22
23
23
24
24 class FrontendHighlighter(PygmentsHighlighter):
25 class FrontendHighlighter(PygmentsHighlighter):
25 """ A PygmentsHighlighter that understands and ignores prompts.
26 """ A PygmentsHighlighter that understands and ignores prompts.
26 """
27 """
27
28
28 def __init__(self, frontend):
29 def __init__(self, frontend):
29 super(FrontendHighlighter, self).__init__(frontend._control.document())
30 super(FrontendHighlighter, self).__init__(frontend._control.document())
30 self._current_offset = 0
31 self._current_offset = 0
31 self._frontend = frontend
32 self._frontend = frontend
32 self.highlighting_on = False
33 self.highlighting_on = False
33
34
34 def highlightBlock(self, string):
35 def highlightBlock(self, string):
35 """ Highlight a block of text. Reimplemented to highlight selectively.
36 """ Highlight a block of text. Reimplemented to highlight selectively.
36 """
37 """
37 if not self.highlighting_on:
38 if not self.highlighting_on:
38 return
39 return
39
40
40 # The input to this function is a unicode string that may contain
41 # The input to this function is a unicode string that may contain
41 # paragraph break characters, non-breaking spaces, etc. Here we acquire
42 # paragraph break characters, non-breaking spaces, etc. Here we acquire
42 # the string as plain text so we can compare it.
43 # the string as plain text so we can compare it.
43 current_block = self.currentBlock()
44 current_block = self.currentBlock()
44 string = self._frontend._get_block_plain_text(current_block)
45 string = self._frontend._get_block_plain_text(current_block)
45
46
46 # Decide whether to check for the regular or continuation prompt.
47 # Decide whether to check for the regular or continuation prompt.
47 if current_block.contains(self._frontend._prompt_pos):
48 if current_block.contains(self._frontend._prompt_pos):
48 prompt = self._frontend._prompt
49 prompt = self._frontend._prompt
49 else:
50 else:
50 prompt = self._frontend._continuation_prompt
51 prompt = self._frontend._continuation_prompt
51
52
52 # Only highlight if we can identify a prompt, but make sure not to
53 # Only highlight if we can identify a prompt, but make sure not to
53 # highlight the prompt.
54 # highlight the prompt.
54 if string.startswith(prompt):
55 if string.startswith(prompt):
55 self._current_offset = len(prompt)
56 self._current_offset = len(prompt)
56 string = string[len(prompt):]
57 string = string[len(prompt):]
57 super(FrontendHighlighter, self).highlightBlock(string)
58 super(FrontendHighlighter, self).highlightBlock(string)
58
59
59 def rehighlightBlock(self, block):
60 def rehighlightBlock(self, block):
60 """ Reimplemented to temporarily enable highlighting if disabled.
61 """ Reimplemented to temporarily enable highlighting if disabled.
61 """
62 """
62 old = self.highlighting_on
63 old = self.highlighting_on
63 self.highlighting_on = True
64 self.highlighting_on = True
64 super(FrontendHighlighter, self).rehighlightBlock(block)
65 super(FrontendHighlighter, self).rehighlightBlock(block)
65 self.highlighting_on = old
66 self.highlighting_on = old
66
67
67 def setFormat(self, start, count, format):
68 def setFormat(self, start, count, format):
68 """ Reimplemented to highlight selectively.
69 """ Reimplemented to highlight selectively.
69 """
70 """
70 start += self._current_offset
71 start += self._current_offset
71 super(FrontendHighlighter, self).setFormat(start, count, format)
72 super(FrontendHighlighter, self).setFormat(start, count, format)
72
73
73
74
74 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
75 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
75 """ A Qt frontend for a generic Python kernel.
76 """ A Qt frontend for a generic Python kernel.
76 """
77 """
77
78
78 # The text to show when the kernel is (re)started.
79 # The text to show when the kernel is (re)started.
79 banner = Unicode()
80 banner = Unicode()
80
81
81 # An option and corresponding signal for overriding the default kernel
82 # An option and corresponding signal for overriding the default kernel
82 # interrupt behavior.
83 # interrupt behavior.
83 custom_interrupt = Bool(False)
84 custom_interrupt = Bool(False)
84 custom_interrupt_requested = QtCore.Signal()
85 custom_interrupt_requested = QtCore.Signal()
85
86
86 # An option and corresponding signals for overriding the default kernel
87 # An option and corresponding signals for overriding the default kernel
87 # restart behavior.
88 # restart behavior.
88 custom_restart = Bool(False)
89 custom_restart = Bool(False)
89 custom_restart_kernel_died = QtCore.Signal(float)
90 custom_restart_kernel_died = QtCore.Signal(float)
90 custom_restart_requested = QtCore.Signal()
91 custom_restart_requested = QtCore.Signal()
91
92
92 # Whether to automatically show calltips on open-parentheses.
93 # Whether to automatically show calltips on open-parentheses.
93 enable_calltips = Bool(True, config=True,
94 enable_calltips = Bool(True, config=True,
94 help="Whether to draw information calltips on open-parentheses.")
95 help="Whether to draw information calltips on open-parentheses.")
95
96
96 # Emitted when a user visible 'execute_request' has been submitted to the
97 # Emitted when a user visible 'execute_request' has been submitted to the
97 # kernel from the FrontendWidget. Contains the code to be executed.
98 # kernel from the FrontendWidget. Contains the code to be executed.
98 executing = QtCore.Signal(object)
99 executing = QtCore.Signal(object)
99
100
100 # Emitted when a user-visible 'execute_reply' has been received from the
101 # Emitted when a user-visible 'execute_reply' has been received from the
101 # kernel and processed by the FrontendWidget. Contains the response message.
102 # kernel and processed by the FrontendWidget. Contains the response message.
102 executed = QtCore.Signal(object)
103 executed = QtCore.Signal(object)
103
104
104 # Emitted when an exit request has been received from the kernel.
105 # Emitted when an exit request has been received from the kernel.
105 exit_requested = QtCore.Signal(object)
106 exit_requested = QtCore.Signal(object)
106
107
107 # Protected class variables.
108 # Protected class variables.
108 _CallTipRequest = namedtuple('_CallTipRequest', ['id', 'pos'])
109 _CallTipRequest = namedtuple('_CallTipRequest', ['id', 'pos'])
109 _CompletionRequest = namedtuple('_CompletionRequest', ['id', 'pos'])
110 _CompletionRequest = namedtuple('_CompletionRequest', ['id', 'pos'])
110 _ExecutionRequest = namedtuple('_ExecutionRequest', ['id', 'kind'])
111 _ExecutionRequest = namedtuple('_ExecutionRequest', ['id', 'kind'])
111 _input_splitter_class = InputSplitter
112 _input_splitter_class = InputSplitter
112 _local_kernel = False
113 _local_kernel = False
113 _highlighter = Instance(FrontendHighlighter)
114 _highlighter = Instance(FrontendHighlighter)
114
115
115 #---------------------------------------------------------------------------
116 #---------------------------------------------------------------------------
116 # 'object' interface
117 # 'object' interface
117 #---------------------------------------------------------------------------
118 #---------------------------------------------------------------------------
118
119
119 def __init__(self, *args, **kw):
120 def __init__(self, *args, **kw):
120 super(FrontendWidget, self).__init__(*args, **kw)
121 super(FrontendWidget, self).__init__(*args, **kw)
122 # FIXME: remove this when PySide min version is updated past 1.0.7
123 # forcefully disable calltips if PySide is < 1.0.7, because they crash
124 if qt.QT_API == qt.QT_API_PYSIDE:
125 import PySide
126 if PySide.__version_info__ < (1,0,7):
127 self.log.warn("PySide %s < 1.0.7 detected, disabling calltips" % PySide.__version__)
128 self.enable_calltips = False
121
129
122 # FrontendWidget protected variables.
130 # FrontendWidget protected variables.
123 self._bracket_matcher = BracketMatcher(self._control)
131 self._bracket_matcher = BracketMatcher(self._control)
124 self._call_tip_widget = CallTipWidget(self._control)
132 self._call_tip_widget = CallTipWidget(self._control)
125 self._completion_lexer = CompletionLexer(PythonLexer())
133 self._completion_lexer = CompletionLexer(PythonLexer())
126 self._copy_raw_action = QtGui.QAction('Copy (Raw Text)', None)
134 self._copy_raw_action = QtGui.QAction('Copy (Raw Text)', None)
127 self._hidden = False
135 self._hidden = False
128 self._highlighter = FrontendHighlighter(self)
136 self._highlighter = FrontendHighlighter(self)
129 self._input_splitter = self._input_splitter_class(input_mode='cell')
137 self._input_splitter = self._input_splitter_class(input_mode='cell')
130 self._kernel_manager = None
138 self._kernel_manager = None
131 self._request_info = {}
139 self._request_info = {}
132
140
133 # Configure the ConsoleWidget.
141 # Configure the ConsoleWidget.
134 self.tab_width = 4
142 self.tab_width = 4
135 self._set_continuation_prompt('... ')
143 self._set_continuation_prompt('... ')
136
144
137 # Configure the CallTipWidget.
145 # Configure the CallTipWidget.
138 self._call_tip_widget.setFont(self.font)
146 self._call_tip_widget.setFont(self.font)
139 self.font_changed.connect(self._call_tip_widget.setFont)
147 self.font_changed.connect(self._call_tip_widget.setFont)
140
148
141 # Configure actions.
149 # Configure actions.
142 action = self._copy_raw_action
150 action = self._copy_raw_action
143 key = QtCore.Qt.CTRL | QtCore.Qt.SHIFT | QtCore.Qt.Key_C
151 key = QtCore.Qt.CTRL | QtCore.Qt.SHIFT | QtCore.Qt.Key_C
144 action.setEnabled(False)
152 action.setEnabled(False)
145 action.setShortcut(QtGui.QKeySequence(key))
153 action.setShortcut(QtGui.QKeySequence(key))
146 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
154 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
147 action.triggered.connect(self.copy_raw)
155 action.triggered.connect(self.copy_raw)
148 self.copy_available.connect(action.setEnabled)
156 self.copy_available.connect(action.setEnabled)
149 self.addAction(action)
157 self.addAction(action)
150
158
151 # Connect signal handlers.
159 # Connect signal handlers.
152 document = self._control.document()
160 document = self._control.document()
153 document.contentsChange.connect(self._document_contents_change)
161 document.contentsChange.connect(self._document_contents_change)
154
162
155 # Set flag for whether we are connected via localhost.
163 # Set flag for whether we are connected via localhost.
156 self._local_kernel = kw.get('local_kernel',
164 self._local_kernel = kw.get('local_kernel',
157 FrontendWidget._local_kernel)
165 FrontendWidget._local_kernel)
158
166
159 #---------------------------------------------------------------------------
167 #---------------------------------------------------------------------------
160 # 'ConsoleWidget' public interface
168 # 'ConsoleWidget' public interface
161 #---------------------------------------------------------------------------
169 #---------------------------------------------------------------------------
162
170
163 def copy(self):
171 def copy(self):
164 """ Copy the currently selected text to the clipboard, removing prompts.
172 """ Copy the currently selected text to the clipboard, removing prompts.
165 """
173 """
166 text = self._control.textCursor().selection().toPlainText()
174 text = self._control.textCursor().selection().toPlainText()
167 if text:
175 if text:
168 lines = map(transform_classic_prompt, text.splitlines())
176 lines = map(transform_classic_prompt, text.splitlines())
169 text = '\n'.join(lines)
177 text = '\n'.join(lines)
170 QtGui.QApplication.clipboard().setText(text)
178 QtGui.QApplication.clipboard().setText(text)
171
179
172 #---------------------------------------------------------------------------
180 #---------------------------------------------------------------------------
173 # 'ConsoleWidget' abstract interface
181 # 'ConsoleWidget' abstract interface
174 #---------------------------------------------------------------------------
182 #---------------------------------------------------------------------------
175
183
176 def _is_complete(self, source, interactive):
184 def _is_complete(self, source, interactive):
177 """ Returns whether 'source' can be completely processed and a new
185 """ Returns whether 'source' can be completely processed and a new
178 prompt created. When triggered by an Enter/Return key press,
186 prompt created. When triggered by an Enter/Return key press,
179 'interactive' is True; otherwise, it is False.
187 'interactive' is True; otherwise, it is False.
180 """
188 """
181 complete = self._input_splitter.push(source)
189 complete = self._input_splitter.push(source)
182 if interactive:
190 if interactive:
183 complete = not self._input_splitter.push_accepts_more()
191 complete = not self._input_splitter.push_accepts_more()
184 return complete
192 return complete
185
193
186 def _execute(self, source, hidden):
194 def _execute(self, source, hidden):
187 """ Execute 'source'. If 'hidden', do not show any output.
195 """ Execute 'source'. If 'hidden', do not show any output.
188
196
189 See parent class :meth:`execute` docstring for full details.
197 See parent class :meth:`execute` docstring for full details.
190 """
198 """
191 msg_id = self.kernel_manager.shell_channel.execute(source, hidden)
199 msg_id = self.kernel_manager.shell_channel.execute(source, hidden)
192 self._request_info['execute'] = self._ExecutionRequest(msg_id, 'user')
200 self._request_info['execute'] = self._ExecutionRequest(msg_id, 'user')
193 self._hidden = hidden
201 self._hidden = hidden
194 if not hidden:
202 if not hidden:
195 self.executing.emit(source)
203 self.executing.emit(source)
196
204
197 def _prompt_started_hook(self):
205 def _prompt_started_hook(self):
198 """ Called immediately after a new prompt is displayed.
206 """ Called immediately after a new prompt is displayed.
199 """
207 """
200 if not self._reading:
208 if not self._reading:
201 self._highlighter.highlighting_on = True
209 self._highlighter.highlighting_on = True
202
210
203 def _prompt_finished_hook(self):
211 def _prompt_finished_hook(self):
204 """ Called immediately after a prompt is finished, i.e. when some input
212 """ Called immediately after a prompt is finished, i.e. when some input
205 will be processed and a new prompt displayed.
213 will be processed and a new prompt displayed.
206 """
214 """
207 # Flush all state from the input splitter so the next round of
215 # Flush all state from the input splitter so the next round of
208 # reading input starts with a clean buffer.
216 # reading input starts with a clean buffer.
209 self._input_splitter.reset()
217 self._input_splitter.reset()
210
218
211 if not self._reading:
219 if not self._reading:
212 self._highlighter.highlighting_on = False
220 self._highlighter.highlighting_on = False
213
221
214 def _tab_pressed(self):
222 def _tab_pressed(self):
215 """ Called when the tab key is pressed. Returns whether to continue
223 """ Called when the tab key is pressed. Returns whether to continue
216 processing the event.
224 processing the event.
217 """
225 """
218 # Perform tab completion if:
226 # Perform tab completion if:
219 # 1) The cursor is in the input buffer.
227 # 1) The cursor is in the input buffer.
220 # 2) There is a non-whitespace character before the cursor.
228 # 2) There is a non-whitespace character before the cursor.
221 text = self._get_input_buffer_cursor_line()
229 text = self._get_input_buffer_cursor_line()
222 if text is None:
230 if text is None:
223 return False
231 return False
224 complete = bool(text[:self._get_input_buffer_cursor_column()].strip())
232 complete = bool(text[:self._get_input_buffer_cursor_column()].strip())
225 if complete:
233 if complete:
226 self._complete()
234 self._complete()
227 return not complete
235 return not complete
228
236
229 #---------------------------------------------------------------------------
237 #---------------------------------------------------------------------------
230 # 'ConsoleWidget' protected interface
238 # 'ConsoleWidget' protected interface
231 #---------------------------------------------------------------------------
239 #---------------------------------------------------------------------------
232
240
233 def _context_menu_make(self, pos):
241 def _context_menu_make(self, pos):
234 """ Reimplemented to add an action for raw copy.
242 """ Reimplemented to add an action for raw copy.
235 """
243 """
236 menu = super(FrontendWidget, self)._context_menu_make(pos)
244 menu = super(FrontendWidget, self)._context_menu_make(pos)
237 for before_action in menu.actions():
245 for before_action in menu.actions():
238 if before_action.shortcut().matches(QtGui.QKeySequence.Paste) == \
246 if before_action.shortcut().matches(QtGui.QKeySequence.Paste) == \
239 QtGui.QKeySequence.ExactMatch:
247 QtGui.QKeySequence.ExactMatch:
240 menu.insertAction(before_action, self._copy_raw_action)
248 menu.insertAction(before_action, self._copy_raw_action)
241 break
249 break
242 return menu
250 return menu
243
251
244 def request_interrupt_kernel(self):
252 def request_interrupt_kernel(self):
245 if self._executing:
253 if self._executing:
246 self.interrupt_kernel()
254 self.interrupt_kernel()
247
255
248 def request_restart_kernel(self):
256 def request_restart_kernel(self):
249 message = 'Are you sure you want to restart the kernel?'
257 message = 'Are you sure you want to restart the kernel?'
250 self.restart_kernel(message, now=False)
258 self.restart_kernel(message, now=False)
251
259
252 def _event_filter_console_keypress(self, event):
260 def _event_filter_console_keypress(self, event):
253 """ Reimplemented for execution interruption and smart backspace.
261 """ Reimplemented for execution interruption and smart backspace.
254 """
262 """
255 key = event.key()
263 key = event.key()
256 if self._control_key_down(event.modifiers(), include_command=False):
264 if self._control_key_down(event.modifiers(), include_command=False):
257
265
258 if key == QtCore.Qt.Key_C and self._executing:
266 if key == QtCore.Qt.Key_C and self._executing:
259 self.request_interrupt_kernel()
267 self.request_interrupt_kernel()
260 return True
268 return True
261
269
262 elif key == QtCore.Qt.Key_Period:
270 elif key == QtCore.Qt.Key_Period:
263 self.request_restart_kernel()
271 self.request_restart_kernel()
264 return True
272 return True
265
273
266 elif not event.modifiers() & QtCore.Qt.AltModifier:
274 elif not event.modifiers() & QtCore.Qt.AltModifier:
267
275
268 # Smart backspace: remove four characters in one backspace if:
276 # Smart backspace: remove four characters in one backspace if:
269 # 1) everything left of the cursor is whitespace
277 # 1) everything left of the cursor is whitespace
270 # 2) the four characters immediately left of the cursor are spaces
278 # 2) the four characters immediately left of the cursor are spaces
271 if key == QtCore.Qt.Key_Backspace:
279 if key == QtCore.Qt.Key_Backspace:
272 col = self._get_input_buffer_cursor_column()
280 col = self._get_input_buffer_cursor_column()
273 cursor = self._control.textCursor()
281 cursor = self._control.textCursor()
274 if col > 3 and not cursor.hasSelection():
282 if col > 3 and not cursor.hasSelection():
275 text = self._get_input_buffer_cursor_line()[:col]
283 text = self._get_input_buffer_cursor_line()[:col]
276 if text.endswith(' ') and not text.strip():
284 if text.endswith(' ') and not text.strip():
277 cursor.movePosition(QtGui.QTextCursor.Left,
285 cursor.movePosition(QtGui.QTextCursor.Left,
278 QtGui.QTextCursor.KeepAnchor, 4)
286 QtGui.QTextCursor.KeepAnchor, 4)
279 cursor.removeSelectedText()
287 cursor.removeSelectedText()
280 return True
288 return True
281
289
282 return super(FrontendWidget, self)._event_filter_console_keypress(event)
290 return super(FrontendWidget, self)._event_filter_console_keypress(event)
283
291
284 def _insert_continuation_prompt(self, cursor):
292 def _insert_continuation_prompt(self, cursor):
285 """ Reimplemented for auto-indentation.
293 """ Reimplemented for auto-indentation.
286 """
294 """
287 super(FrontendWidget, self)._insert_continuation_prompt(cursor)
295 super(FrontendWidget, self)._insert_continuation_prompt(cursor)
288 cursor.insertText(' ' * self._input_splitter.indent_spaces)
296 cursor.insertText(' ' * self._input_splitter.indent_spaces)
289
297
290 #---------------------------------------------------------------------------
298 #---------------------------------------------------------------------------
291 # 'BaseFrontendMixin' abstract interface
299 # 'BaseFrontendMixin' abstract interface
292 #---------------------------------------------------------------------------
300 #---------------------------------------------------------------------------
293
301
294 def _handle_complete_reply(self, rep):
302 def _handle_complete_reply(self, rep):
295 """ Handle replies for tab completion.
303 """ Handle replies for tab completion.
296 """
304 """
297 self.log.debug("complete: %s", rep.get('content', ''))
305 self.log.debug("complete: %s", rep.get('content', ''))
298 cursor = self._get_cursor()
306 cursor = self._get_cursor()
299 info = self._request_info.get('complete')
307 info = self._request_info.get('complete')
300 if info and info.id == rep['parent_header']['msg_id'] and \
308 if info and info.id == rep['parent_header']['msg_id'] and \
301 info.pos == cursor.position():
309 info.pos == cursor.position():
302 text = '.'.join(self._get_context())
310 text = '.'.join(self._get_context())
303 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
311 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
304 self._complete_with_items(cursor, rep['content']['matches'])
312 self._complete_with_items(cursor, rep['content']['matches'])
305
313
306 def _handle_execute_reply(self, msg):
314 def _handle_execute_reply(self, msg):
307 """ Handles replies for code execution.
315 """ Handles replies for code execution.
308 """
316 """
309 self.log.debug("execute: %s", msg.get('content', ''))
317 self.log.debug("execute: %s", msg.get('content', ''))
310 info = self._request_info.get('execute')
318 info = self._request_info.get('execute')
311 # unset reading flag, because if execute finished, raw_input can't
319 # unset reading flag, because if execute finished, raw_input can't
312 # still be pending.
320 # still be pending.
313 self._reading = False
321 self._reading = False
314 if info and info.id == msg['parent_header']['msg_id'] and \
322 if info and info.id == msg['parent_header']['msg_id'] and \
315 info.kind == 'user' and not self._hidden:
323 info.kind == 'user' and not self._hidden:
316 # Make sure that all output from the SUB channel has been processed
324 # Make sure that all output from the SUB channel has been processed
317 # before writing a new prompt.
325 # before writing a new prompt.
318 self.kernel_manager.sub_channel.flush()
326 self.kernel_manager.sub_channel.flush()
319
327
320 # Reset the ANSI style information to prevent bad text in stdout
328 # Reset the ANSI style information to prevent bad text in stdout
321 # from messing up our colors. We're not a true terminal so we're
329 # from messing up our colors. We're not a true terminal so we're
322 # allowed to do this.
330 # allowed to do this.
323 if self.ansi_codes:
331 if self.ansi_codes:
324 self._ansi_processor.reset_sgr()
332 self._ansi_processor.reset_sgr()
325
333
326 content = msg['content']
334 content = msg['content']
327 status = content['status']
335 status = content['status']
328 if status == 'ok':
336 if status == 'ok':
329 self._process_execute_ok(msg)
337 self._process_execute_ok(msg)
330 elif status == 'error':
338 elif status == 'error':
331 self._process_execute_error(msg)
339 self._process_execute_error(msg)
332 elif status == 'aborted':
340 elif status == 'aborted':
333 self._process_execute_abort(msg)
341 self._process_execute_abort(msg)
334
342
335 self._show_interpreter_prompt_for_reply(msg)
343 self._show_interpreter_prompt_for_reply(msg)
336 self.executed.emit(msg)
344 self.executed.emit(msg)
337 else:
345 else:
338 super(FrontendWidget, self)._handle_execute_reply(msg)
346 super(FrontendWidget, self)._handle_execute_reply(msg)
339
347
340 def _handle_input_request(self, msg):
348 def _handle_input_request(self, msg):
341 """ Handle requests for raw_input.
349 """ Handle requests for raw_input.
342 """
350 """
343 self.log.debug("input: %s", msg.get('content', ''))
351 self.log.debug("input: %s", msg.get('content', ''))
344 if self._hidden:
352 if self._hidden:
345 raise RuntimeError('Request for raw input during hidden execution.')
353 raise RuntimeError('Request for raw input during hidden execution.')
346
354
347 # Make sure that all output from the SUB channel has been processed
355 # Make sure that all output from the SUB channel has been processed
348 # before entering readline mode.
356 # before entering readline mode.
349 self.kernel_manager.sub_channel.flush()
357 self.kernel_manager.sub_channel.flush()
350
358
351 def callback(line):
359 def callback(line):
352 self.kernel_manager.stdin_channel.input(line)
360 self.kernel_manager.stdin_channel.input(line)
353 if self._reading:
361 if self._reading:
354 self.log.debug("Got second input request, assuming first was interrupted.")
362 self.log.debug("Got second input request, assuming first was interrupted.")
355 self._reading = False
363 self._reading = False
356 self._readline(msg['content']['prompt'], callback=callback)
364 self._readline(msg['content']['prompt'], callback=callback)
357
365
358 def _handle_kernel_died(self, since_last_heartbeat):
366 def _handle_kernel_died(self, since_last_heartbeat):
359 """ Handle the kernel's death by asking if the user wants to restart.
367 """ Handle the kernel's death by asking if the user wants to restart.
360 """
368 """
361 self.log.debug("kernel died: %s", since_last_heartbeat)
369 self.log.debug("kernel died: %s", since_last_heartbeat)
362 if self.custom_restart:
370 if self.custom_restart:
363 self.custom_restart_kernel_died.emit(since_last_heartbeat)
371 self.custom_restart_kernel_died.emit(since_last_heartbeat)
364 else:
372 else:
365 message = 'The kernel heartbeat has been inactive for %.2f ' \
373 message = 'The kernel heartbeat has been inactive for %.2f ' \
366 'seconds. Do you want to restart the kernel? You may ' \
374 'seconds. Do you want to restart the kernel? You may ' \
367 'first want to check the network connection.' % \
375 'first want to check the network connection.' % \
368 since_last_heartbeat
376 since_last_heartbeat
369 self.restart_kernel(message, now=True)
377 self.restart_kernel(message, now=True)
370
378
371 def _handle_object_info_reply(self, rep):
379 def _handle_object_info_reply(self, rep):
372 """ Handle replies for call tips.
380 """ Handle replies for call tips.
373 """
381 """
374 self.log.debug("oinfo: %s", rep.get('content', ''))
382 self.log.debug("oinfo: %s", rep.get('content', ''))
375 cursor = self._get_cursor()
383 cursor = self._get_cursor()
376 info = self._request_info.get('call_tip')
384 info = self._request_info.get('call_tip')
377 if info and info.id == rep['parent_header']['msg_id'] and \
385 if info and info.id == rep['parent_header']['msg_id'] and \
378 info.pos == cursor.position():
386 info.pos == cursor.position():
379 # Get the information for a call tip. For now we format the call
387 # Get the information for a call tip. For now we format the call
380 # line as string, later we can pass False to format_call and
388 # line as string, later we can pass False to format_call and
381 # syntax-highlight it ourselves for nicer formatting in the
389 # syntax-highlight it ourselves for nicer formatting in the
382 # calltip.
390 # calltip.
383 content = rep['content']
391 content = rep['content']
384 # if this is from pykernel, 'docstring' will be the only key
392 # if this is from pykernel, 'docstring' will be the only key
385 if content.get('ismagic', False):
393 if content.get('ismagic', False):
386 # Don't generate a call-tip for magics. Ideally, we should
394 # Don't generate a call-tip for magics. Ideally, we should
387 # generate a tooltip, but not on ( like we do for actual
395 # generate a tooltip, but not on ( like we do for actual
388 # callables.
396 # callables.
389 call_info, doc = None, None
397 call_info, doc = None, None
390 else:
398 else:
391 call_info, doc = call_tip(content, format_call=True)
399 call_info, doc = call_tip(content, format_call=True)
392 if call_info or doc:
400 if call_info or doc:
393 self._call_tip_widget.show_call_info(call_info, doc)
401 self._call_tip_widget.show_call_info(call_info, doc)
394
402
395 def _handle_pyout(self, msg):
403 def _handle_pyout(self, msg):
396 """ Handle display hook output.
404 """ Handle display hook output.
397 """
405 """
398 self.log.debug("pyout: %s", msg.get('content', ''))
406 self.log.debug("pyout: %s", msg.get('content', ''))
399 if not self._hidden and self._is_from_this_session(msg):
407 if not self._hidden and self._is_from_this_session(msg):
400 text = msg['content']['data']
408 text = msg['content']['data']
401 self._append_plain_text(text + '\n', before_prompt=True)
409 self._append_plain_text(text + '\n', before_prompt=True)
402
410
403 def _handle_stream(self, msg):
411 def _handle_stream(self, msg):
404 """ Handle stdout, stderr, and stdin.
412 """ Handle stdout, stderr, and stdin.
405 """
413 """
406 self.log.debug("stream: %s", msg.get('content', ''))
414 self.log.debug("stream: %s", msg.get('content', ''))
407 if not self._hidden and self._is_from_this_session(msg):
415 if not self._hidden and self._is_from_this_session(msg):
408 # Most consoles treat tabs as being 8 space characters. Convert tabs
416 # Most consoles treat tabs as being 8 space characters. Convert tabs
409 # to spaces so that output looks as expected regardless of this
417 # to spaces so that output looks as expected regardless of this
410 # widget's tab width.
418 # widget's tab width.
411 text = msg['content']['data'].expandtabs(8)
419 text = msg['content']['data'].expandtabs(8)
412
420
413 self._append_plain_text(text, before_prompt=True)
421 self._append_plain_text(text, before_prompt=True)
414 self._control.moveCursor(QtGui.QTextCursor.End)
422 self._control.moveCursor(QtGui.QTextCursor.End)
415
423
416 def _handle_shutdown_reply(self, msg):
424 def _handle_shutdown_reply(self, msg):
417 """ Handle shutdown signal, only if from other console.
425 """ Handle shutdown signal, only if from other console.
418 """
426 """
419 self.log.debug("shutdown: %s", msg.get('content', ''))
427 self.log.debug("shutdown: %s", msg.get('content', ''))
420 if not self._hidden and not self._is_from_this_session(msg):
428 if not self._hidden and not self._is_from_this_session(msg):
421 if self._local_kernel:
429 if self._local_kernel:
422 if not msg['content']['restart']:
430 if not msg['content']['restart']:
423 self.exit_requested.emit(self)
431 self.exit_requested.emit(self)
424 else:
432 else:
425 # we just got notified of a restart!
433 # we just got notified of a restart!
426 time.sleep(0.25) # wait 1/4 sec to reset
434 time.sleep(0.25) # wait 1/4 sec to reset
427 # lest the request for a new prompt
435 # lest the request for a new prompt
428 # goes to the old kernel
436 # goes to the old kernel
429 self.reset()
437 self.reset()
430 else: # remote kernel, prompt on Kernel shutdown/reset
438 else: # remote kernel, prompt on Kernel shutdown/reset
431 title = self.window().windowTitle()
439 title = self.window().windowTitle()
432 if not msg['content']['restart']:
440 if not msg['content']['restart']:
433 reply = QtGui.QMessageBox.question(self, title,
441 reply = QtGui.QMessageBox.question(self, title,
434 "Kernel has been shutdown permanently. "
442 "Kernel has been shutdown permanently. "
435 "Close the Console?",
443 "Close the Console?",
436 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
444 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
437 if reply == QtGui.QMessageBox.Yes:
445 if reply == QtGui.QMessageBox.Yes:
438 self.exit_requested.emit(self)
446 self.exit_requested.emit(self)
439 else:
447 else:
440 reply = QtGui.QMessageBox.question(self, title,
448 reply = QtGui.QMessageBox.question(self, title,
441 "Kernel has been reset. Clear the Console?",
449 "Kernel has been reset. Clear the Console?",
442 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
450 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
443 if reply == QtGui.QMessageBox.Yes:
451 if reply == QtGui.QMessageBox.Yes:
444 time.sleep(0.25) # wait 1/4 sec to reset
452 time.sleep(0.25) # wait 1/4 sec to reset
445 # lest the request for a new prompt
453 # lest the request for a new prompt
446 # goes to the old kernel
454 # goes to the old kernel
447 self.reset()
455 self.reset()
448
456
449 def _started_channels(self):
457 def _started_channels(self):
450 """ Called when the KernelManager channels have started listening or
458 """ Called when the KernelManager channels have started listening or
451 when the frontend is assigned an already listening KernelManager.
459 when the frontend is assigned an already listening KernelManager.
452 """
460 """
453 self.reset()
461 self.reset()
454
462
455 #---------------------------------------------------------------------------
463 #---------------------------------------------------------------------------
456 # 'FrontendWidget' public interface
464 # 'FrontendWidget' public interface
457 #---------------------------------------------------------------------------
465 #---------------------------------------------------------------------------
458
466
459 def copy_raw(self):
467 def copy_raw(self):
460 """ Copy the currently selected text to the clipboard without attempting
468 """ Copy the currently selected text to the clipboard without attempting
461 to remove prompts or otherwise alter the text.
469 to remove prompts or otherwise alter the text.
462 """
470 """
463 self._control.copy()
471 self._control.copy()
464
472
465 def execute_file(self, path, hidden=False):
473 def execute_file(self, path, hidden=False):
466 """ Attempts to execute file with 'path'. If 'hidden', no output is
474 """ Attempts to execute file with 'path'. If 'hidden', no output is
467 shown.
475 shown.
468 """
476 """
469 self.execute('execfile(%r)' % path, hidden=hidden)
477 self.execute('execfile(%r)' % path, hidden=hidden)
470
478
471 def interrupt_kernel(self):
479 def interrupt_kernel(self):
472 """ Attempts to interrupt the running kernel.
480 """ Attempts to interrupt the running kernel.
473
481
474 Also unsets _reading flag, to avoid runtime errors
482 Also unsets _reading flag, to avoid runtime errors
475 if raw_input is called again.
483 if raw_input is called again.
476 """
484 """
477 if self.custom_interrupt:
485 if self.custom_interrupt:
478 self._reading = False
486 self._reading = False
479 self.custom_interrupt_requested.emit()
487 self.custom_interrupt_requested.emit()
480 elif self.kernel_manager.has_kernel:
488 elif self.kernel_manager.has_kernel:
481 self._reading = False
489 self._reading = False
482 self.kernel_manager.interrupt_kernel()
490 self.kernel_manager.interrupt_kernel()
483 else:
491 else:
484 self._append_plain_text('Kernel process is either remote or '
492 self._append_plain_text('Kernel process is either remote or '
485 'unspecified. Cannot interrupt.\n')
493 'unspecified. Cannot interrupt.\n')
486
494
487 def reset(self):
495 def reset(self):
488 """ Resets the widget to its initial state. Similar to ``clear``, but
496 """ Resets the widget to its initial state. Similar to ``clear``, but
489 also re-writes the banner and aborts execution if necessary.
497 also re-writes the banner and aborts execution if necessary.
490 """
498 """
491 if self._executing:
499 if self._executing:
492 self._executing = False
500 self._executing = False
493 self._request_info['execute'] = None
501 self._request_info['execute'] = None
494 self._reading = False
502 self._reading = False
495 self._highlighter.highlighting_on = False
503 self._highlighter.highlighting_on = False
496
504
497 self._control.clear()
505 self._control.clear()
498 self._append_plain_text(self.banner)
506 self._append_plain_text(self.banner)
499 # update output marker for stdout/stderr, so that startup
507 # update output marker for stdout/stderr, so that startup
500 # messages appear after banner:
508 # messages appear after banner:
501 self._append_before_prompt_pos = self._get_cursor().position()
509 self._append_before_prompt_pos = self._get_cursor().position()
502 self._show_interpreter_prompt()
510 self._show_interpreter_prompt()
503
511
504 def restart_kernel(self, message, now=False):
512 def restart_kernel(self, message, now=False):
505 """ Attempts to restart the running kernel.
513 """ Attempts to restart the running kernel.
506 """
514 """
507 # FIXME: now should be configurable via a checkbox in the dialog. Right
515 # FIXME: now should be configurable via a checkbox in the dialog. Right
508 # now at least the heartbeat path sets it to True and the manual restart
516 # now at least the heartbeat path sets it to True and the manual restart
509 # to False. But those should just be the pre-selected states of a
517 # to False. But those should just be the pre-selected states of a
510 # checkbox that the user could override if so desired. But I don't know
518 # checkbox that the user could override if so desired. But I don't know
511 # enough Qt to go implementing the checkbox now.
519 # enough Qt to go implementing the checkbox now.
512
520
513 if self.custom_restart:
521 if self.custom_restart:
514 self.custom_restart_requested.emit()
522 self.custom_restart_requested.emit()
515
523
516 elif self.kernel_manager.has_kernel:
524 elif self.kernel_manager.has_kernel:
517 # Pause the heart beat channel to prevent further warnings.
525 # Pause the heart beat channel to prevent further warnings.
518 self.kernel_manager.hb_channel.pause()
526 self.kernel_manager.hb_channel.pause()
519
527
520 # Prompt the user to restart the kernel. Un-pause the heartbeat if
528 # Prompt the user to restart the kernel. Un-pause the heartbeat if
521 # they decline. (If they accept, the heartbeat will be un-paused
529 # they decline. (If they accept, the heartbeat will be un-paused
522 # automatically when the kernel is restarted.)
530 # automatically when the kernel is restarted.)
523 buttons = QtGui.QMessageBox.Yes | QtGui.QMessageBox.No
531 buttons = QtGui.QMessageBox.Yes | QtGui.QMessageBox.No
524 result = QtGui.QMessageBox.question(self, 'Restart kernel?',
532 result = QtGui.QMessageBox.question(self, 'Restart kernel?',
525 message, buttons)
533 message, buttons)
526 if result == QtGui.QMessageBox.Yes:
534 if result == QtGui.QMessageBox.Yes:
527 try:
535 try:
528 self.kernel_manager.restart_kernel(now=now)
536 self.kernel_manager.restart_kernel(now=now)
529 except RuntimeError:
537 except RuntimeError:
530 self._append_plain_text('Kernel started externally. '
538 self._append_plain_text('Kernel started externally. '
531 'Cannot restart.\n')
539 'Cannot restart.\n')
532 else:
540 else:
533 self.reset()
541 self.reset()
534 else:
542 else:
535 self.kernel_manager.hb_channel.unpause()
543 self.kernel_manager.hb_channel.unpause()
536
544
537 else:
545 else:
538 self._append_plain_text('Kernel process is either remote or '
546 self._append_plain_text('Kernel process is either remote or '
539 'unspecified. Cannot restart.\n')
547 'unspecified. Cannot restart.\n')
540
548
541 #---------------------------------------------------------------------------
549 #---------------------------------------------------------------------------
542 # 'FrontendWidget' protected interface
550 # 'FrontendWidget' protected interface
543 #---------------------------------------------------------------------------
551 #---------------------------------------------------------------------------
544
552
545 def _call_tip(self):
553 def _call_tip(self):
546 """ Shows a call tip, if appropriate, at the current cursor location.
554 """ Shows a call tip, if appropriate, at the current cursor location.
547 """
555 """
548 # Decide if it makes sense to show a call tip
556 # Decide if it makes sense to show a call tip
549 if not self.enable_calltips:
557 if not self.enable_calltips:
550 return False
558 return False
551 cursor = self._get_cursor()
559 cursor = self._get_cursor()
552 cursor.movePosition(QtGui.QTextCursor.Left)
560 cursor.movePosition(QtGui.QTextCursor.Left)
553 if cursor.document().characterAt(cursor.position()) != '(':
561 if cursor.document().characterAt(cursor.position()) != '(':
554 return False
562 return False
555 context = self._get_context(cursor)
563 context = self._get_context(cursor)
556 if not context:
564 if not context:
557 return False
565 return False
558
566
559 # Send the metadata request to the kernel
567 # Send the metadata request to the kernel
560 name = '.'.join(context)
568 name = '.'.join(context)
561 msg_id = self.kernel_manager.shell_channel.object_info(name)
569 msg_id = self.kernel_manager.shell_channel.object_info(name)
562 pos = self._get_cursor().position()
570 pos = self._get_cursor().position()
563 self._request_info['call_tip'] = self._CallTipRequest(msg_id, pos)
571 self._request_info['call_tip'] = self._CallTipRequest(msg_id, pos)
564 return True
572 return True
565
573
566 def _complete(self):
574 def _complete(self):
567 """ Performs completion at the current cursor location.
575 """ Performs completion at the current cursor location.
568 """
576 """
569 context = self._get_context()
577 context = self._get_context()
570 if context:
578 if context:
571 # Send the completion request to the kernel
579 # Send the completion request to the kernel
572 msg_id = self.kernel_manager.shell_channel.complete(
580 msg_id = self.kernel_manager.shell_channel.complete(
573 '.'.join(context), # text
581 '.'.join(context), # text
574 self._get_input_buffer_cursor_line(), # line
582 self._get_input_buffer_cursor_line(), # line
575 self._get_input_buffer_cursor_column(), # cursor_pos
583 self._get_input_buffer_cursor_column(), # cursor_pos
576 self.input_buffer) # block
584 self.input_buffer) # block
577 pos = self._get_cursor().position()
585 pos = self._get_cursor().position()
578 info = self._CompletionRequest(msg_id, pos)
586 info = self._CompletionRequest(msg_id, pos)
579 self._request_info['complete'] = info
587 self._request_info['complete'] = info
580
588
581 def _get_context(self, cursor=None):
589 def _get_context(self, cursor=None):
582 """ Gets the context for the specified cursor (or the current cursor
590 """ Gets the context for the specified cursor (or the current cursor
583 if none is specified).
591 if none is specified).
584 """
592 """
585 if cursor is None:
593 if cursor is None:
586 cursor = self._get_cursor()
594 cursor = self._get_cursor()
587 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
595 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
588 QtGui.QTextCursor.KeepAnchor)
596 QtGui.QTextCursor.KeepAnchor)
589 text = cursor.selection().toPlainText()
597 text = cursor.selection().toPlainText()
590 return self._completion_lexer.get_context(text)
598 return self._completion_lexer.get_context(text)
591
599
592 def _process_execute_abort(self, msg):
600 def _process_execute_abort(self, msg):
593 """ Process a reply for an aborted execution request.
601 """ Process a reply for an aborted execution request.
594 """
602 """
595 self._append_plain_text("ERROR: execution aborted\n")
603 self._append_plain_text("ERROR: execution aborted\n")
596
604
597 def _process_execute_error(self, msg):
605 def _process_execute_error(self, msg):
598 """ Process a reply for an execution request that resulted in an error.
606 """ Process a reply for an execution request that resulted in an error.
599 """
607 """
600 content = msg['content']
608 content = msg['content']
601 # If a SystemExit is passed along, this means exit() was called - also
609 # If a SystemExit is passed along, this means exit() was called - also
602 # all the ipython %exit magic syntax of '-k' to be used to keep
610 # all the ipython %exit magic syntax of '-k' to be used to keep
603 # the kernel running
611 # the kernel running
604 if content['ename']=='SystemExit':
612 if content['ename']=='SystemExit':
605 keepkernel = content['evalue']=='-k' or content['evalue']=='True'
613 keepkernel = content['evalue']=='-k' or content['evalue']=='True'
606 self._keep_kernel_on_exit = keepkernel
614 self._keep_kernel_on_exit = keepkernel
607 self.exit_requested.emit(self)
615 self.exit_requested.emit(self)
608 else:
616 else:
609 traceback = ''.join(content['traceback'])
617 traceback = ''.join(content['traceback'])
610 self._append_plain_text(traceback)
618 self._append_plain_text(traceback)
611
619
612 def _process_execute_ok(self, msg):
620 def _process_execute_ok(self, msg):
613 """ Process a reply for a successful execution equest.
621 """ Process a reply for a successful execution equest.
614 """
622 """
615 payload = msg['content']['payload']
623 payload = msg['content']['payload']
616 for item in payload:
624 for item in payload:
617 if not self._process_execute_payload(item):
625 if not self._process_execute_payload(item):
618 warning = 'Warning: received unknown payload of type %s'
626 warning = 'Warning: received unknown payload of type %s'
619 print(warning % repr(item['source']))
627 print(warning % repr(item['source']))
620
628
621 def _process_execute_payload(self, item):
629 def _process_execute_payload(self, item):
622 """ Process a single payload item from the list of payload items in an
630 """ Process a single payload item from the list of payload items in an
623 execution reply. Returns whether the payload was handled.
631 execution reply. Returns whether the payload was handled.
624 """
632 """
625 # The basic FrontendWidget doesn't handle payloads, as they are a
633 # The basic FrontendWidget doesn't handle payloads, as they are a
626 # mechanism for going beyond the standard Python interpreter model.
634 # mechanism for going beyond the standard Python interpreter model.
627 return False
635 return False
628
636
629 def _show_interpreter_prompt(self):
637 def _show_interpreter_prompt(self):
630 """ Shows a prompt for the interpreter.
638 """ Shows a prompt for the interpreter.
631 """
639 """
632 self._show_prompt('>>> ')
640 self._show_prompt('>>> ')
633
641
634 def _show_interpreter_prompt_for_reply(self, msg):
642 def _show_interpreter_prompt_for_reply(self, msg):
635 """ Shows a prompt for the interpreter given an 'execute_reply' message.
643 """ Shows a prompt for the interpreter given an 'execute_reply' message.
636 """
644 """
637 self._show_interpreter_prompt()
645 self._show_interpreter_prompt()
638
646
639 #------ Signal handlers ----------------------------------------------------
647 #------ Signal handlers ----------------------------------------------------
640
648
641 def _document_contents_change(self, position, removed, added):
649 def _document_contents_change(self, position, removed, added):
642 """ Called whenever the document's content changes. Display a call tip
650 """ Called whenever the document's content changes. Display a call tip
643 if appropriate.
651 if appropriate.
644 """
652 """
645 # Calculate where the cursor should be *after* the change:
653 # Calculate where the cursor should be *after* the change:
646 position += added
654 position += added
647
655
648 document = self._control.document()
656 document = self._control.document()
649 if position == self._get_cursor().position():
657 if position == self._get_cursor().position():
650 self._call_tip()
658 self._call_tip()
651
659
652 #------ Trait default initializers -----------------------------------------
660 #------ Trait default initializers -----------------------------------------
653
661
654 def _banner_default(self):
662 def _banner_default(self):
655 """ Returns the standard Python banner.
663 """ Returns the standard Python banner.
656 """
664 """
657 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
665 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
658 '"license" for more information.'
666 '"license" for more information.'
659 return banner % (sys.version, sys.platform)
667 return banner % (sys.version, sys.platform)
General Comments 0
You need to be logged in to leave comments. Login now