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