##// END OF EJS Templates
Fix Windows-specific bug in path handling for Qt console.
Evan Patterson -
Show More
@@ -1,616 +1,616 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
92 # Emitted when a user visible 'execute_request' has been submitted to the
93 # kernel from the FrontendWidget. Contains the code to be executed.
93 # kernel from the FrontendWidget. Contains the code to be executed.
94 executing = QtCore.Signal(object)
94 executing = QtCore.Signal(object)
95
95
96 # 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
97 # kernel and processed by the FrontendWidget. Contains the response message.
97 # kernel and processed by the FrontendWidget. Contains the response message.
98 executed = QtCore.Signal(object)
98 executed = QtCore.Signal(object)
99
99
100 # Emitted when an exit request has been received from the kernel.
100 # Emitted when an exit request has been received from the kernel.
101 exit_requested = QtCore.Signal()
101 exit_requested = QtCore.Signal()
102
102
103 # Protected class variables.
103 # Protected class variables.
104 _CallTipRequest = namedtuple('_CallTipRequest', ['id', 'pos'])
104 _CallTipRequest = namedtuple('_CallTipRequest', ['id', 'pos'])
105 _CompletionRequest = namedtuple('_CompletionRequest', ['id', 'pos'])
105 _CompletionRequest = namedtuple('_CompletionRequest', ['id', 'pos'])
106 _ExecutionRequest = namedtuple('_ExecutionRequest', ['id', 'kind'])
106 _ExecutionRequest = namedtuple('_ExecutionRequest', ['id', 'kind'])
107 _input_splitter_class = InputSplitter
107 _input_splitter_class = InputSplitter
108 _local_kernel = False
108 _local_kernel = False
109
109
110 #---------------------------------------------------------------------------
110 #---------------------------------------------------------------------------
111 # 'object' interface
111 # 'object' interface
112 #---------------------------------------------------------------------------
112 #---------------------------------------------------------------------------
113
113
114 def __init__(self, *args, **kw):
114 def __init__(self, *args, **kw):
115 super(FrontendWidget, self).__init__(*args, **kw)
115 super(FrontendWidget, self).__init__(*args, **kw)
116
116
117 # FrontendWidget protected variables.
117 # FrontendWidget protected variables.
118 self._bracket_matcher = BracketMatcher(self._control)
118 self._bracket_matcher = BracketMatcher(self._control)
119 self._call_tip_widget = CallTipWidget(self._control)
119 self._call_tip_widget = CallTipWidget(self._control)
120 self._completion_lexer = CompletionLexer(PythonLexer())
120 self._completion_lexer = CompletionLexer(PythonLexer())
121 self._copy_raw_action = QtGui.QAction('Copy (Raw Text)', None)
121 self._copy_raw_action = QtGui.QAction('Copy (Raw Text)', None)
122 self._hidden = False
122 self._hidden = False
123 self._highlighter = FrontendHighlighter(self)
123 self._highlighter = FrontendHighlighter(self)
124 self._input_splitter = self._input_splitter_class(input_mode='cell')
124 self._input_splitter = self._input_splitter_class(input_mode='cell')
125 self._kernel_manager = None
125 self._kernel_manager = None
126 self._request_info = {}
126 self._request_info = {}
127
127
128 # Configure the ConsoleWidget.
128 # Configure the ConsoleWidget.
129 self.tab_width = 4
129 self.tab_width = 4
130 self._set_continuation_prompt('... ')
130 self._set_continuation_prompt('... ')
131
131
132 # Configure the CallTipWidget.
132 # Configure the CallTipWidget.
133 self._call_tip_widget.setFont(self.font)
133 self._call_tip_widget.setFont(self.font)
134 self.font_changed.connect(self._call_tip_widget.setFont)
134 self.font_changed.connect(self._call_tip_widget.setFont)
135
135
136 # Configure actions.
136 # Configure actions.
137 action = self._copy_raw_action
137 action = self._copy_raw_action
138 key = QtCore.Qt.CTRL | QtCore.Qt.SHIFT | QtCore.Qt.Key_C
138 key = QtCore.Qt.CTRL | QtCore.Qt.SHIFT | QtCore.Qt.Key_C
139 action.setEnabled(False)
139 action.setEnabled(False)
140 action.setShortcut(QtGui.QKeySequence(key))
140 action.setShortcut(QtGui.QKeySequence(key))
141 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
141 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
142 action.triggered.connect(self.copy_raw)
142 action.triggered.connect(self.copy_raw)
143 self.copy_available.connect(action.setEnabled)
143 self.copy_available.connect(action.setEnabled)
144 self.addAction(action)
144 self.addAction(action)
145
145
146 # Connect signal handlers.
146 # Connect signal handlers.
147 document = self._control.document()
147 document = self._control.document()
148 document.contentsChange.connect(self._document_contents_change)
148 document.contentsChange.connect(self._document_contents_change)
149
149
150 # Set flag for whether we are connected via localhost.
150 # Set flag for whether we are connected via localhost.
151 self._local_kernel = kw.get('local_kernel',
151 self._local_kernel = kw.get('local_kernel',
152 FrontendWidget._local_kernel)
152 FrontendWidget._local_kernel)
153
153
154 #---------------------------------------------------------------------------
154 #---------------------------------------------------------------------------
155 # 'ConsoleWidget' public interface
155 # 'ConsoleWidget' public interface
156 #---------------------------------------------------------------------------
156 #---------------------------------------------------------------------------
157
157
158 def copy(self):
158 def copy(self):
159 """ Copy the currently selected text to the clipboard, removing prompts.
159 """ Copy the currently selected text to the clipboard, removing prompts.
160 """
160 """
161 text = self._control.textCursor().selection().toPlainText()
161 text = self._control.textCursor().selection().toPlainText()
162 if text:
162 if text:
163 lines = map(transform_classic_prompt, text.splitlines())
163 lines = map(transform_classic_prompt, text.splitlines())
164 text = '\n'.join(lines)
164 text = '\n'.join(lines)
165 QtGui.QApplication.clipboard().setText(text)
165 QtGui.QApplication.clipboard().setText(text)
166
166
167 #---------------------------------------------------------------------------
167 #---------------------------------------------------------------------------
168 # 'ConsoleWidget' abstract interface
168 # 'ConsoleWidget' abstract interface
169 #---------------------------------------------------------------------------
169 #---------------------------------------------------------------------------
170
170
171 def _is_complete(self, source, interactive):
171 def _is_complete(self, source, interactive):
172 """ Returns whether 'source' can be completely processed and a new
172 """ Returns whether 'source' can be completely processed and a new
173 prompt created. When triggered by an Enter/Return key press,
173 prompt created. When triggered by an Enter/Return key press,
174 'interactive' is True; otherwise, it is False.
174 'interactive' is True; otherwise, it is False.
175 """
175 """
176 complete = self._input_splitter.push(source)
176 complete = self._input_splitter.push(source)
177 if interactive:
177 if interactive:
178 complete = not self._input_splitter.push_accepts_more()
178 complete = not self._input_splitter.push_accepts_more()
179 return complete
179 return complete
180
180
181 def _execute(self, source, hidden):
181 def _execute(self, source, hidden):
182 """ Execute 'source'. If 'hidden', do not show any output.
182 """ Execute 'source'. If 'hidden', do not show any output.
183
183
184 See parent class :meth:`execute` docstring for full details.
184 See parent class :meth:`execute` docstring for full details.
185 """
185 """
186 msg_id = self.kernel_manager.xreq_channel.execute(source, hidden)
186 msg_id = self.kernel_manager.xreq_channel.execute(source, hidden)
187 self._request_info['execute'] = self._ExecutionRequest(msg_id, 'user')
187 self._request_info['execute'] = self._ExecutionRequest(msg_id, 'user')
188 self._hidden = hidden
188 self._hidden = hidden
189 if not hidden:
189 if not hidden:
190 self.executing.emit(source)
190 self.executing.emit(source)
191
191
192 def _prompt_started_hook(self):
192 def _prompt_started_hook(self):
193 """ Called immediately after a new prompt is displayed.
193 """ Called immediately after a new prompt is displayed.
194 """
194 """
195 if not self._reading:
195 if not self._reading:
196 self._highlighter.highlighting_on = True
196 self._highlighter.highlighting_on = True
197
197
198 def _prompt_finished_hook(self):
198 def _prompt_finished_hook(self):
199 """ 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
200 will be processed and a new prompt displayed.
200 will be processed and a new prompt displayed.
201 """
201 """
202 # 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
203 # reading input starts with a clean buffer.
203 # reading input starts with a clean buffer.
204 self._input_splitter.reset()
204 self._input_splitter.reset()
205
205
206 if not self._reading:
206 if not self._reading:
207 self._highlighter.highlighting_on = False
207 self._highlighter.highlighting_on = False
208
208
209 def _tab_pressed(self):
209 def _tab_pressed(self):
210 """ Called when the tab key is pressed. Returns whether to continue
210 """ Called when the tab key is pressed. Returns whether to continue
211 processing the event.
211 processing the event.
212 """
212 """
213 # Perform tab completion if:
213 # Perform tab completion if:
214 # 1) The cursor is in the input buffer.
214 # 1) The cursor is in the input buffer.
215 # 2) There is a non-whitespace character before the cursor.
215 # 2) There is a non-whitespace character before the cursor.
216 text = self._get_input_buffer_cursor_line()
216 text = self._get_input_buffer_cursor_line()
217 if text is None:
217 if text is None:
218 return False
218 return False
219 complete = bool(text[:self._get_input_buffer_cursor_column()].strip())
219 complete = bool(text[:self._get_input_buffer_cursor_column()].strip())
220 if complete:
220 if complete:
221 self._complete()
221 self._complete()
222 return not complete
222 return not complete
223
223
224 #---------------------------------------------------------------------------
224 #---------------------------------------------------------------------------
225 # 'ConsoleWidget' protected interface
225 # 'ConsoleWidget' protected interface
226 #---------------------------------------------------------------------------
226 #---------------------------------------------------------------------------
227
227
228 def _context_menu_make(self, pos):
228 def _context_menu_make(self, pos):
229 """ Reimplemented to add an action for raw copy.
229 """ Reimplemented to add an action for raw copy.
230 """
230 """
231 menu = super(FrontendWidget, self)._context_menu_make(pos)
231 menu = super(FrontendWidget, self)._context_menu_make(pos)
232 for before_action in menu.actions():
232 for before_action in menu.actions():
233 if before_action.shortcut().matches(QtGui.QKeySequence.Paste) == \
233 if before_action.shortcut().matches(QtGui.QKeySequence.Paste) == \
234 QtGui.QKeySequence.ExactMatch:
234 QtGui.QKeySequence.ExactMatch:
235 menu.insertAction(before_action, self._copy_raw_action)
235 menu.insertAction(before_action, self._copy_raw_action)
236 break
236 break
237 return menu
237 return menu
238
238
239 def _event_filter_console_keypress(self, event):
239 def _event_filter_console_keypress(self, event):
240 """ Reimplemented for execution interruption and smart backspace.
240 """ Reimplemented for execution interruption and smart backspace.
241 """
241 """
242 key = event.key()
242 key = event.key()
243 if self._control_key_down(event.modifiers(), include_command=False):
243 if self._control_key_down(event.modifiers(), include_command=False):
244
244
245 if key == QtCore.Qt.Key_C and self._executing:
245 if key == QtCore.Qt.Key_C and self._executing:
246 self.interrupt_kernel()
246 self.interrupt_kernel()
247 return True
247 return True
248
248
249 elif key == QtCore.Qt.Key_Period:
249 elif key == QtCore.Qt.Key_Period:
250 message = 'Are you sure you want to restart the kernel?'
250 message = 'Are you sure you want to restart the kernel?'
251 self.restart_kernel(message, now=False)
251 self.restart_kernel(message, now=False)
252 return True
252 return True
253
253
254 elif not event.modifiers() & QtCore.Qt.AltModifier:
254 elif not event.modifiers() & QtCore.Qt.AltModifier:
255
255
256 # Smart backspace: remove four characters in one backspace if:
256 # Smart backspace: remove four characters in one backspace if:
257 # 1) everything left of the cursor is whitespace
257 # 1) everything left of the cursor is whitespace
258 # 2) the four characters immediately left of the cursor are spaces
258 # 2) the four characters immediately left of the cursor are spaces
259 if key == QtCore.Qt.Key_Backspace:
259 if key == QtCore.Qt.Key_Backspace:
260 col = self._get_input_buffer_cursor_column()
260 col = self._get_input_buffer_cursor_column()
261 cursor = self._control.textCursor()
261 cursor = self._control.textCursor()
262 if col > 3 and not cursor.hasSelection():
262 if col > 3 and not cursor.hasSelection():
263 text = self._get_input_buffer_cursor_line()[:col]
263 text = self._get_input_buffer_cursor_line()[:col]
264 if text.endswith(' ') and not text.strip():
264 if text.endswith(' ') and not text.strip():
265 cursor.movePosition(QtGui.QTextCursor.Left,
265 cursor.movePosition(QtGui.QTextCursor.Left,
266 QtGui.QTextCursor.KeepAnchor, 4)
266 QtGui.QTextCursor.KeepAnchor, 4)
267 cursor.removeSelectedText()
267 cursor.removeSelectedText()
268 return True
268 return True
269
269
270 return super(FrontendWidget, self)._event_filter_console_keypress(event)
270 return super(FrontendWidget, self)._event_filter_console_keypress(event)
271
271
272 def _insert_continuation_prompt(self, cursor):
272 def _insert_continuation_prompt(self, cursor):
273 """ Reimplemented for auto-indentation.
273 """ Reimplemented for auto-indentation.
274 """
274 """
275 super(FrontendWidget, self)._insert_continuation_prompt(cursor)
275 super(FrontendWidget, self)._insert_continuation_prompt(cursor)
276 cursor.insertText(' ' * self._input_splitter.indent_spaces)
276 cursor.insertText(' ' * self._input_splitter.indent_spaces)
277
277
278 #---------------------------------------------------------------------------
278 #---------------------------------------------------------------------------
279 # 'BaseFrontendMixin' abstract interface
279 # 'BaseFrontendMixin' abstract interface
280 #---------------------------------------------------------------------------
280 #---------------------------------------------------------------------------
281
281
282 def _handle_complete_reply(self, rep):
282 def _handle_complete_reply(self, rep):
283 """ Handle replies for tab completion.
283 """ Handle replies for tab completion.
284 """
284 """
285 cursor = self._get_cursor()
285 cursor = self._get_cursor()
286 info = self._request_info.get('complete')
286 info = self._request_info.get('complete')
287 if info and info.id == rep['parent_header']['msg_id'] and \
287 if info and info.id == rep['parent_header']['msg_id'] and \
288 info.pos == cursor.position():
288 info.pos == cursor.position():
289 text = '.'.join(self._get_context())
289 text = '.'.join(self._get_context())
290 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
290 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
291 self._complete_with_items(cursor, rep['content']['matches'])
291 self._complete_with_items(cursor, rep['content']['matches'])
292
292
293 def _handle_execute_reply(self, msg):
293 def _handle_execute_reply(self, msg):
294 """ Handles replies for code execution.
294 """ Handles replies for code execution.
295 """
295 """
296 info = self._request_info.get('execute')
296 info = self._request_info.get('execute')
297 if info and info.id == msg['parent_header']['msg_id'] and \
297 if info and info.id == msg['parent_header']['msg_id'] and \
298 info.kind == 'user' and not self._hidden:
298 info.kind == 'user' and not self._hidden:
299 # 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
300 # before writing a new prompt.
300 # before writing a new prompt.
301 self.kernel_manager.sub_channel.flush()
301 self.kernel_manager.sub_channel.flush()
302
302
303 # Reset the ANSI style information to prevent bad text in stdout
303 # Reset the ANSI style information to prevent bad text in stdout
304 # 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
305 # allowed to do this.
305 # allowed to do this.
306 if self.ansi_codes:
306 if self.ansi_codes:
307 self._ansi_processor.reset_sgr()
307 self._ansi_processor.reset_sgr()
308
308
309 content = msg['content']
309 content = msg['content']
310 status = content['status']
310 status = content['status']
311 if status == 'ok':
311 if status == 'ok':
312 self._process_execute_ok(msg)
312 self._process_execute_ok(msg)
313 elif status == 'error':
313 elif status == 'error':
314 self._process_execute_error(msg)
314 self._process_execute_error(msg)
315 elif status == 'abort':
315 elif status == 'abort':
316 self._process_execute_abort(msg)
316 self._process_execute_abort(msg)
317
317
318 self._show_interpreter_prompt_for_reply(msg)
318 self._show_interpreter_prompt_for_reply(msg)
319 self.executed.emit(msg)
319 self.executed.emit(msg)
320
320
321 def _handle_input_request(self, msg):
321 def _handle_input_request(self, msg):
322 """ Handle requests for raw_input.
322 """ Handle requests for raw_input.
323 """
323 """
324 if self._hidden:
324 if self._hidden:
325 raise RuntimeError('Request for raw input during hidden execution.')
325 raise RuntimeError('Request for raw input during hidden execution.')
326
326
327 # 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
328 # before entering readline mode.
328 # before entering readline mode.
329 self.kernel_manager.sub_channel.flush()
329 self.kernel_manager.sub_channel.flush()
330
330
331 def callback(line):
331 def callback(line):
332 self.kernel_manager.rep_channel.input(line)
332 self.kernel_manager.rep_channel.input(line)
333 self._readline(msg['content']['prompt'], callback=callback)
333 self._readline(msg['content']['prompt'], callback=callback)
334
334
335 def _handle_kernel_died(self, since_last_heartbeat):
335 def _handle_kernel_died(self, since_last_heartbeat):
336 """ 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.
337 """
337 """
338 if self.custom_restart:
338 if self.custom_restart:
339 self.custom_restart_kernel_died.emit(since_last_heartbeat)
339 self.custom_restart_kernel_died.emit(since_last_heartbeat)
340 else:
340 else:
341 message = 'The kernel heartbeat has been inactive for %.2f ' \
341 message = 'The kernel heartbeat has been inactive for %.2f ' \
342 'seconds. Do you want to restart the kernel? You may ' \
342 'seconds. Do you want to restart the kernel? You may ' \
343 'first want to check the network connection.' % \
343 'first want to check the network connection.' % \
344 since_last_heartbeat
344 since_last_heartbeat
345 self.restart_kernel(message, now=True)
345 self.restart_kernel(message, now=True)
346
346
347 def _handle_object_info_reply(self, rep):
347 def _handle_object_info_reply(self, rep):
348 """ Handle replies for call tips.
348 """ Handle replies for call tips.
349 """
349 """
350 cursor = self._get_cursor()
350 cursor = self._get_cursor()
351 info = self._request_info.get('call_tip')
351 info = self._request_info.get('call_tip')
352 if info and info.id == rep['parent_header']['msg_id'] and \
352 if info and info.id == rep['parent_header']['msg_id'] and \
353 info.pos == cursor.position():
353 info.pos == cursor.position():
354 # 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
355 # 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
356 # syntax-highlight it ourselves for nicer formatting in the
356 # syntax-highlight it ourselves for nicer formatting in the
357 # calltip.
357 # calltip.
358 if rep['content']['ismagic']:
358 if rep['content']['ismagic']:
359 # Don't generate a call-tip for magics. Ideally, we should
359 # Don't generate a call-tip for magics. Ideally, we should
360 # generate a tooltip, but not on ( like we do for actual
360 # generate a tooltip, but not on ( like we do for actual
361 # callables.
361 # callables.
362 call_info, doc = None, None
362 call_info, doc = None, None
363 else:
363 else:
364 call_info, doc = call_tip(rep['content'], format_call=True)
364 call_info, doc = call_tip(rep['content'], format_call=True)
365 if call_info or doc:
365 if call_info or doc:
366 self._call_tip_widget.show_call_info(call_info, doc)
366 self._call_tip_widget.show_call_info(call_info, doc)
367
367
368 def _handle_pyout(self, msg):
368 def _handle_pyout(self, msg):
369 """ Handle display hook output.
369 """ Handle display hook output.
370 """
370 """
371 if not self._hidden and self._is_from_this_session(msg):
371 if not self._hidden and self._is_from_this_session(msg):
372 self._append_plain_text(msg['content']['data']['text/plain'] + '\n')
372 self._append_plain_text(msg['content']['data']['text/plain'] + '\n')
373
373
374 def _handle_stream(self, msg):
374 def _handle_stream(self, msg):
375 """ Handle stdout, stderr, and stdin.
375 """ Handle stdout, stderr, and stdin.
376 """
376 """
377 if not self._hidden and self._is_from_this_session(msg):
377 if not self._hidden and self._is_from_this_session(msg):
378 # Most consoles treat tabs as being 8 space characters. Convert tabs
378 # Most consoles treat tabs as being 8 space characters. Convert tabs
379 # to spaces so that output looks as expected regardless of this
379 # to spaces so that output looks as expected regardless of this
380 # widget's tab width.
380 # widget's tab width.
381 text = msg['content']['data'].expandtabs(8)
381 text = msg['content']['data'].expandtabs(8)
382
382
383 self._append_plain_text(text)
383 self._append_plain_text(text)
384 self._control.moveCursor(QtGui.QTextCursor.End)
384 self._control.moveCursor(QtGui.QTextCursor.End)
385
385
386 def _handle_shutdown_reply(self, msg):
386 def _handle_shutdown_reply(self, msg):
387 """ Handle shutdown signal, only if from other console.
387 """ Handle shutdown signal, only if from other console.
388 """
388 """
389 if not self._hidden and not self._is_from_this_session(msg):
389 if not self._hidden and not self._is_from_this_session(msg):
390 if self._local_kernel:
390 if self._local_kernel:
391 if not msg['content']['restart']:
391 if not msg['content']['restart']:
392 sys.exit(0)
392 sys.exit(0)
393 else:
393 else:
394 # we just got notified of a restart!
394 # we just got notified of a restart!
395 time.sleep(0.25) # wait 1/4 sec to reset
395 time.sleep(0.25) # wait 1/4 sec to reset
396 # lest the request for a new prompt
396 # lest the request for a new prompt
397 # goes to the old kernel
397 # goes to the old kernel
398 self.reset()
398 self.reset()
399 else: # remote kernel, prompt on Kernel shutdown/reset
399 else: # remote kernel, prompt on Kernel shutdown/reset
400 title = self.window().windowTitle()
400 title = self.window().windowTitle()
401 if not msg['content']['restart']:
401 if not msg['content']['restart']:
402 reply = QtGui.QMessageBox.question(self, title,
402 reply = QtGui.QMessageBox.question(self, title,
403 "Kernel has been shutdown permanently. "
403 "Kernel has been shutdown permanently. "
404 "Close the Console?",
404 "Close the Console?",
405 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
405 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
406 if reply == QtGui.QMessageBox.Yes:
406 if reply == QtGui.QMessageBox.Yes:
407 sys.exit(0)
407 sys.exit(0)
408 else:
408 else:
409 reply = QtGui.QMessageBox.question(self, title,
409 reply = QtGui.QMessageBox.question(self, title,
410 "Kernel has been reset. Clear the Console?",
410 "Kernel has been reset. Clear the Console?",
411 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
411 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
412 if reply == QtGui.QMessageBox.Yes:
412 if reply == QtGui.QMessageBox.Yes:
413 time.sleep(0.25) # wait 1/4 sec to reset
413 time.sleep(0.25) # wait 1/4 sec to reset
414 # lest the request for a new prompt
414 # lest the request for a new prompt
415 # goes to the old kernel
415 # goes to the old kernel
416 self.reset()
416 self.reset()
417
417
418 def _started_channels(self):
418 def _started_channels(self):
419 """ Called when the KernelManager channels have started listening or
419 """ Called when the KernelManager channels have started listening or
420 when the frontend is assigned an already listening KernelManager.
420 when the frontend is assigned an already listening KernelManager.
421 """
421 """
422 self.reset()
422 self.reset()
423
423
424 #---------------------------------------------------------------------------
424 #---------------------------------------------------------------------------
425 # 'FrontendWidget' public interface
425 # 'FrontendWidget' public interface
426 #---------------------------------------------------------------------------
426 #---------------------------------------------------------------------------
427
427
428 def copy_raw(self):
428 def copy_raw(self):
429 """ Copy the currently selected text to the clipboard without attempting
429 """ Copy the currently selected text to the clipboard without attempting
430 to remove prompts or otherwise alter the text.
430 to remove prompts or otherwise alter the text.
431 """
431 """
432 self._control.copy()
432 self._control.copy()
433
433
434 def execute_file(self, path, hidden=False):
434 def execute_file(self, path, hidden=False):
435 """ Attempts to execute file with 'path'. If 'hidden', no output is
435 """ Attempts to execute file with 'path'. If 'hidden', no output is
436 shown.
436 shown.
437 """
437 """
438 self.execute('execfile("%s")' % path, hidden=hidden)
438 self.execute('execfile(%r)' % path, hidden=hidden)
439
439
440 def interrupt_kernel(self):
440 def interrupt_kernel(self):
441 """ Attempts to interrupt the running kernel.
441 """ Attempts to interrupt the running kernel.
442 """
442 """
443 if self.custom_interrupt:
443 if self.custom_interrupt:
444 self.custom_interrupt_requested.emit()
444 self.custom_interrupt_requested.emit()
445 elif self.kernel_manager.has_kernel:
445 elif self.kernel_manager.has_kernel:
446 self.kernel_manager.interrupt_kernel()
446 self.kernel_manager.interrupt_kernel()
447 else:
447 else:
448 self._append_plain_text('Kernel process is either remote or '
448 self._append_plain_text('Kernel process is either remote or '
449 'unspecified. Cannot interrupt.\n')
449 'unspecified. Cannot interrupt.\n')
450
450
451 def reset(self):
451 def reset(self):
452 """ Resets the widget to its initial state. Similar to ``clear``, but
452 """ Resets the widget to its initial state. Similar to ``clear``, but
453 also re-writes the banner and aborts execution if necessary.
453 also re-writes the banner and aborts execution if necessary.
454 """
454 """
455 if self._executing:
455 if self._executing:
456 self._executing = False
456 self._executing = False
457 self._request_info['execute'] = None
457 self._request_info['execute'] = None
458 self._reading = False
458 self._reading = False
459 self._highlighter.highlighting_on = False
459 self._highlighter.highlighting_on = False
460
460
461 self._control.clear()
461 self._control.clear()
462 self._append_plain_text(self._get_banner())
462 self._append_plain_text(self._get_banner())
463 self._show_interpreter_prompt()
463 self._show_interpreter_prompt()
464
464
465 def restart_kernel(self, message, now=False):
465 def restart_kernel(self, message, now=False):
466 """ Attempts to restart the running kernel.
466 """ Attempts to restart the running kernel.
467 """
467 """
468 # FIXME: now should be configurable via a checkbox in the dialog. Right
468 # FIXME: now should be configurable via a checkbox in the dialog. Right
469 # now at least the heartbeat path sets it to True and the manual restart
469 # now at least the heartbeat path sets it to True and the manual restart
470 # to False. But those should just be the pre-selected states of a
470 # to False. But those should just be the pre-selected states of a
471 # checkbox that the user could override if so desired. But I don't know
471 # checkbox that the user could override if so desired. But I don't know
472 # enough Qt to go implementing the checkbox now.
472 # enough Qt to go implementing the checkbox now.
473
473
474 if self.custom_restart:
474 if self.custom_restart:
475 self.custom_restart_requested.emit()
475 self.custom_restart_requested.emit()
476
476
477 elif self.kernel_manager.has_kernel:
477 elif self.kernel_manager.has_kernel:
478 # Pause the heart beat channel to prevent further warnings.
478 # Pause the heart beat channel to prevent further warnings.
479 self.kernel_manager.hb_channel.pause()
479 self.kernel_manager.hb_channel.pause()
480
480
481 # Prompt the user to restart the kernel. Un-pause the heartbeat if
481 # Prompt the user to restart the kernel. Un-pause the heartbeat if
482 # they decline. (If they accept, the heartbeat will be un-paused
482 # they decline. (If they accept, the heartbeat will be un-paused
483 # automatically when the kernel is restarted.)
483 # automatically when the kernel is restarted.)
484 buttons = QtGui.QMessageBox.Yes | QtGui.QMessageBox.No
484 buttons = QtGui.QMessageBox.Yes | QtGui.QMessageBox.No
485 result = QtGui.QMessageBox.question(self, 'Restart kernel?',
485 result = QtGui.QMessageBox.question(self, 'Restart kernel?',
486 message, buttons)
486 message, buttons)
487 if result == QtGui.QMessageBox.Yes:
487 if result == QtGui.QMessageBox.Yes:
488 try:
488 try:
489 self.kernel_manager.restart_kernel(now=now)
489 self.kernel_manager.restart_kernel(now=now)
490 except RuntimeError:
490 except RuntimeError:
491 self._append_plain_text('Kernel started externally. '
491 self._append_plain_text('Kernel started externally. '
492 'Cannot restart.\n')
492 'Cannot restart.\n')
493 else:
493 else:
494 self.reset()
494 self.reset()
495 else:
495 else:
496 self.kernel_manager.hb_channel.unpause()
496 self.kernel_manager.hb_channel.unpause()
497
497
498 else:
498 else:
499 self._append_plain_text('Kernel process is either remote or '
499 self._append_plain_text('Kernel process is either remote or '
500 'unspecified. Cannot restart.\n')
500 'unspecified. Cannot restart.\n')
501
501
502 #---------------------------------------------------------------------------
502 #---------------------------------------------------------------------------
503 # 'FrontendWidget' protected interface
503 # 'FrontendWidget' protected interface
504 #---------------------------------------------------------------------------
504 #---------------------------------------------------------------------------
505
505
506 def _call_tip(self):
506 def _call_tip(self):
507 """ Shows a call tip, if appropriate, at the current cursor location.
507 """ Shows a call tip, if appropriate, at the current cursor location.
508 """
508 """
509 # Decide if it makes sense to show a call tip
509 # Decide if it makes sense to show a call tip
510 cursor = self._get_cursor()
510 cursor = self._get_cursor()
511 cursor.movePosition(QtGui.QTextCursor.Left)
511 cursor.movePosition(QtGui.QTextCursor.Left)
512 if cursor.document().characterAt(cursor.position()) != '(':
512 if cursor.document().characterAt(cursor.position()) != '(':
513 return False
513 return False
514 context = self._get_context(cursor)
514 context = self._get_context(cursor)
515 if not context:
515 if not context:
516 return False
516 return False
517
517
518 # Send the metadata request to the kernel
518 # Send the metadata request to the kernel
519 name = '.'.join(context)
519 name = '.'.join(context)
520 msg_id = self.kernel_manager.xreq_channel.object_info(name)
520 msg_id = self.kernel_manager.xreq_channel.object_info(name)
521 pos = self._get_cursor().position()
521 pos = self._get_cursor().position()
522 self._request_info['call_tip'] = self._CallTipRequest(msg_id, pos)
522 self._request_info['call_tip'] = self._CallTipRequest(msg_id, pos)
523 return True
523 return True
524
524
525 def _complete(self):
525 def _complete(self):
526 """ Performs completion at the current cursor location.
526 """ Performs completion at the current cursor location.
527 """
527 """
528 context = self._get_context()
528 context = self._get_context()
529 if context:
529 if context:
530 # Send the completion request to the kernel
530 # Send the completion request to the kernel
531 msg_id = self.kernel_manager.xreq_channel.complete(
531 msg_id = self.kernel_manager.xreq_channel.complete(
532 '.'.join(context), # text
532 '.'.join(context), # text
533 self._get_input_buffer_cursor_line(), # line
533 self._get_input_buffer_cursor_line(), # line
534 self._get_input_buffer_cursor_column(), # cursor_pos
534 self._get_input_buffer_cursor_column(), # cursor_pos
535 self.input_buffer) # block
535 self.input_buffer) # block
536 pos = self._get_cursor().position()
536 pos = self._get_cursor().position()
537 info = self._CompletionRequest(msg_id, pos)
537 info = self._CompletionRequest(msg_id, pos)
538 self._request_info['complete'] = info
538 self._request_info['complete'] = info
539
539
540 def _get_banner(self):
540 def _get_banner(self):
541 """ Gets a banner to display at the beginning of a session.
541 """ Gets a banner to display at the beginning of a session.
542 """
542 """
543 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
543 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
544 '"license" for more information.'
544 '"license" for more information.'
545 return banner % (sys.version, sys.platform)
545 return banner % (sys.version, sys.platform)
546
546
547 def _get_context(self, cursor=None):
547 def _get_context(self, cursor=None):
548 """ Gets the context for the specified cursor (or the current cursor
548 """ Gets the context for the specified cursor (or the current cursor
549 if none is specified).
549 if none is specified).
550 """
550 """
551 if cursor is None:
551 if cursor is None:
552 cursor = self._get_cursor()
552 cursor = self._get_cursor()
553 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
553 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
554 QtGui.QTextCursor.KeepAnchor)
554 QtGui.QTextCursor.KeepAnchor)
555 text = cursor.selection().toPlainText()
555 text = cursor.selection().toPlainText()
556 return self._completion_lexer.get_context(text)
556 return self._completion_lexer.get_context(text)
557
557
558 def _process_execute_abort(self, msg):
558 def _process_execute_abort(self, msg):
559 """ Process a reply for an aborted execution request.
559 """ Process a reply for an aborted execution request.
560 """
560 """
561 self._append_plain_text("ERROR: execution aborted\n")
561 self._append_plain_text("ERROR: execution aborted\n")
562
562
563 def _process_execute_error(self, msg):
563 def _process_execute_error(self, msg):
564 """ Process a reply for an execution request that resulted in an error.
564 """ Process a reply for an execution request that resulted in an error.
565 """
565 """
566 content = msg['content']
566 content = msg['content']
567 # If a SystemExit is passed along, this means exit() was called - also
567 # If a SystemExit is passed along, this means exit() was called - also
568 # all the ipython %exit magic syntax of '-k' to be used to keep
568 # all the ipython %exit magic syntax of '-k' to be used to keep
569 # the kernel running
569 # the kernel running
570 if content['ename']=='SystemExit':
570 if content['ename']=='SystemExit':
571 keepkernel = content['evalue']=='-k' or content['evalue']=='True'
571 keepkernel = content['evalue']=='-k' or content['evalue']=='True'
572 self._keep_kernel_on_exit = keepkernel
572 self._keep_kernel_on_exit = keepkernel
573 self.exit_requested.emit()
573 self.exit_requested.emit()
574 else:
574 else:
575 traceback = ''.join(content['traceback'])
575 traceback = ''.join(content['traceback'])
576 self._append_plain_text(traceback)
576 self._append_plain_text(traceback)
577
577
578 def _process_execute_ok(self, msg):
578 def _process_execute_ok(self, msg):
579 """ Process a reply for a successful execution equest.
579 """ Process a reply for a successful execution equest.
580 """
580 """
581 payload = msg['content']['payload']
581 payload = msg['content']['payload']
582 for item in payload:
582 for item in payload:
583 if not self._process_execute_payload(item):
583 if not self._process_execute_payload(item):
584 warning = 'Warning: received unknown payload of type %s'
584 warning = 'Warning: received unknown payload of type %s'
585 print(warning % repr(item['source']))
585 print(warning % repr(item['source']))
586
586
587 def _process_execute_payload(self, item):
587 def _process_execute_payload(self, item):
588 """ Process a single payload item from the list of payload items in an
588 """ Process a single payload item from the list of payload items in an
589 execution reply. Returns whether the payload was handled.
589 execution reply. Returns whether the payload was handled.
590 """
590 """
591 # The basic FrontendWidget doesn't handle payloads, as they are a
591 # The basic FrontendWidget doesn't handle payloads, as they are a
592 # mechanism for going beyond the standard Python interpreter model.
592 # mechanism for going beyond the standard Python interpreter model.
593 return False
593 return False
594
594
595 def _show_interpreter_prompt(self):
595 def _show_interpreter_prompt(self):
596 """ Shows a prompt for the interpreter.
596 """ Shows a prompt for the interpreter.
597 """
597 """
598 self._show_prompt('>>> ')
598 self._show_prompt('>>> ')
599
599
600 def _show_interpreter_prompt_for_reply(self, msg):
600 def _show_interpreter_prompt_for_reply(self, msg):
601 """ Shows a prompt for the interpreter given an 'execute_reply' message.
601 """ Shows a prompt for the interpreter given an 'execute_reply' message.
602 """
602 """
603 self._show_interpreter_prompt()
603 self._show_interpreter_prompt()
604
604
605 #------ Signal handlers ----------------------------------------------------
605 #------ Signal handlers ----------------------------------------------------
606
606
607 def _document_contents_change(self, position, removed, added):
607 def _document_contents_change(self, position, removed, added):
608 """ Called whenever the document's content changes. Display a call tip
608 """ Called whenever the document's content changes. Display a call tip
609 if appropriate.
609 if appropriate.
610 """
610 """
611 # Calculate where the cursor should be *after* the change:
611 # Calculate where the cursor should be *after* the change:
612 position += added
612 position += added
613
613
614 document = self._control.document()
614 document = self._control.document()
615 if position == self._get_cursor().position():
615 if position == self._get_cursor().position():
616 self._call_tip()
616 self._call_tip()
@@ -1,490 +1,496 b''
1 """ A FrontendWidget that emulates the interface of the console IPython and
1 """ A FrontendWidget that emulates the interface of the console IPython and
2 supports the additional functionality provided by the IPython kernel.
2 supports the additional functionality provided by the IPython kernel.
3 """
3 """
4
4
5 #-----------------------------------------------------------------------------
5 #-----------------------------------------------------------------------------
6 # Imports
6 # Imports
7 #-----------------------------------------------------------------------------
7 #-----------------------------------------------------------------------------
8
8
9 # Standard library imports
9 # Standard library imports
10 from collections import namedtuple
10 from collections import namedtuple
11 import os.path
11 import re
12 import re
12 from subprocess import Popen
13 from subprocess import Popen
14 import sys
13 from textwrap import dedent
15 from textwrap import dedent
14
16
15 # System library imports
17 # System library imports
16 from IPython.external.qt import QtCore, QtGui
18 from IPython.external.qt import QtCore, QtGui
17
19
18 # Local imports
20 # Local imports
19 from IPython.core.inputsplitter import IPythonInputSplitter, \
21 from IPython.core.inputsplitter import IPythonInputSplitter, \
20 transform_ipy_prompt
22 transform_ipy_prompt
21 from IPython.core.usage import default_gui_banner
23 from IPython.core.usage import default_gui_banner
22 from IPython.utils.traitlets import Bool, Str, Unicode
24 from IPython.utils.traitlets import Bool, Str, Unicode
23 from frontend_widget import FrontendWidget
25 from frontend_widget import FrontendWidget
24 from styles import (default_light_style_sheet, default_light_syntax_style,
26 from styles import (default_light_style_sheet, default_light_syntax_style,
25 default_dark_style_sheet, default_dark_syntax_style,
27 default_dark_style_sheet, default_dark_syntax_style,
26 default_bw_style_sheet, default_bw_syntax_style)
28 default_bw_style_sheet, default_bw_syntax_style)
27
29
28 #-----------------------------------------------------------------------------
30 #-----------------------------------------------------------------------------
29 # Constants
31 # Constants
30 #-----------------------------------------------------------------------------
32 #-----------------------------------------------------------------------------
31
33
32 # Default strings to build and display input and output prompts (and separators
34 # Default strings to build and display input and output prompts (and separators
33 # in between)
35 # in between)
34 default_in_prompt = 'In [<span class="in-prompt-number">%i</span>]: '
36 default_in_prompt = 'In [<span class="in-prompt-number">%i</span>]: '
35 default_out_prompt = 'Out[<span class="out-prompt-number">%i</span>]: '
37 default_out_prompt = 'Out[<span class="out-prompt-number">%i</span>]: '
36 default_input_sep = '\n'
38 default_input_sep = '\n'
37 default_output_sep = ''
39 default_output_sep = ''
38 default_output_sep2 = ''
40 default_output_sep2 = ''
39
41
40 # Base path for most payload sources.
42 # Base path for most payload sources.
41 zmq_shell_source = 'IPython.zmq.zmqshell.ZMQInteractiveShell'
43 zmq_shell_source = 'IPython.zmq.zmqshell.ZMQInteractiveShell'
42
44
43 #-----------------------------------------------------------------------------
45 #-----------------------------------------------------------------------------
44 # IPythonWidget class
46 # IPythonWidget class
45 #-----------------------------------------------------------------------------
47 #-----------------------------------------------------------------------------
46
48
47 class IPythonWidget(FrontendWidget):
49 class IPythonWidget(FrontendWidget):
48 """ A FrontendWidget for an IPython kernel.
50 """ A FrontendWidget for an IPython kernel.
49 """
51 """
50
52
51 # If set, the 'custom_edit_requested(str, int)' signal will be emitted when
53 # If set, the 'custom_edit_requested(str, int)' signal will be emitted when
52 # an editor is needed for a file. This overrides 'editor' and 'editor_line'
54 # an editor is needed for a file. This overrides 'editor' and 'editor_line'
53 # settings.
55 # settings.
54 custom_edit = Bool(False)
56 custom_edit = Bool(False)
55 custom_edit_requested = QtCore.Signal(object, object)
57 custom_edit_requested = QtCore.Signal(object, object)
56
58
57 # A command for invoking a system text editor. If the string contains a
59 # A command for invoking a system text editor. If the string contains a
58 # {filename} format specifier, it will be used. Otherwise, the filename will
60 # {filename} format specifier, it will be used. Otherwise, the filename will
59 # be appended to the end the command.
61 # be appended to the end the command.
60 editor = Unicode('default', config=True)
62 editor = Unicode('default', config=True)
61
63
62 # The editor command to use when a specific line number is requested. The
64 # The editor command to use when a specific line number is requested. The
63 # string should contain two format specifiers: {line} and {filename}. If
65 # string should contain two format specifiers: {line} and {filename}. If
64 # this parameter is not specified, the line number option to the %edit magic
66 # this parameter is not specified, the line number option to the %edit magic
65 # will be ignored.
67 # will be ignored.
66 editor_line = Unicode(config=True)
68 editor_line = Unicode(config=True)
67
69
68 # A CSS stylesheet. The stylesheet can contain classes for:
70 # A CSS stylesheet. The stylesheet can contain classes for:
69 # 1. Qt: QPlainTextEdit, QFrame, QWidget, etc
71 # 1. Qt: QPlainTextEdit, QFrame, QWidget, etc
70 # 2. Pygments: .c, .k, .o, etc (see PygmentsHighlighter)
72 # 2. Pygments: .c, .k, .o, etc (see PygmentsHighlighter)
71 # 3. IPython: .error, .in-prompt, .out-prompt, etc
73 # 3. IPython: .error, .in-prompt, .out-prompt, etc
72 style_sheet = Unicode(config=True)
74 style_sheet = Unicode(config=True)
73
75
74 # If not empty, use this Pygments style for syntax highlighting. Otherwise,
76 # If not empty, use this Pygments style for syntax highlighting. Otherwise,
75 # the style sheet is queried for Pygments style information.
77 # the style sheet is queried for Pygments style information.
76 syntax_style = Str(config=True)
78 syntax_style = Str(config=True)
77
79
78 # Prompts.
80 # Prompts.
79 in_prompt = Str(default_in_prompt, config=True)
81 in_prompt = Str(default_in_prompt, config=True)
80 out_prompt = Str(default_out_prompt, config=True)
82 out_prompt = Str(default_out_prompt, config=True)
81 input_sep = Str(default_input_sep, config=True)
83 input_sep = Str(default_input_sep, config=True)
82 output_sep = Str(default_output_sep, config=True)
84 output_sep = Str(default_output_sep, config=True)
83 output_sep2 = Str(default_output_sep2, config=True)
85 output_sep2 = Str(default_output_sep2, config=True)
84
86
85 # FrontendWidget protected class variables.
87 # FrontendWidget protected class variables.
86 _input_splitter_class = IPythonInputSplitter
88 _input_splitter_class = IPythonInputSplitter
87
89
88 # IPythonWidget protected class variables.
90 # IPythonWidget protected class variables.
89 _PromptBlock = namedtuple('_PromptBlock', ['block', 'length', 'number'])
91 _PromptBlock = namedtuple('_PromptBlock', ['block', 'length', 'number'])
90 _payload_source_edit = zmq_shell_source + '.edit_magic'
92 _payload_source_edit = zmq_shell_source + '.edit_magic'
91 _payload_source_exit = zmq_shell_source + '.ask_exit'
93 _payload_source_exit = zmq_shell_source + '.ask_exit'
92 _payload_source_loadpy = zmq_shell_source + '.magic_loadpy'
94 _payload_source_loadpy = zmq_shell_source + '.magic_loadpy'
93 _payload_source_page = 'IPython.zmq.page.page'
95 _payload_source_page = 'IPython.zmq.page.page'
94
96
95 #---------------------------------------------------------------------------
97 #---------------------------------------------------------------------------
96 # 'object' interface
98 # 'object' interface
97 #---------------------------------------------------------------------------
99 #---------------------------------------------------------------------------
98
100
99 def __init__(self, *args, **kw):
101 def __init__(self, *args, **kw):
100 super(IPythonWidget, self).__init__(*args, **kw)
102 super(IPythonWidget, self).__init__(*args, **kw)
101
103
102 # IPythonWidget protected variables.
104 # IPythonWidget protected variables.
103 self._code_to_load = None
105 self._code_to_load = None
104 self._payload_handlers = {
106 self._payload_handlers = {
105 self._payload_source_edit : self._handle_payload_edit,
107 self._payload_source_edit : self._handle_payload_edit,
106 self._payload_source_exit : self._handle_payload_exit,
108 self._payload_source_exit : self._handle_payload_exit,
107 self._payload_source_page : self._handle_payload_page,
109 self._payload_source_page : self._handle_payload_page,
108 self._payload_source_loadpy : self._handle_payload_loadpy }
110 self._payload_source_loadpy : self._handle_payload_loadpy }
109 self._previous_prompt_obj = None
111 self._previous_prompt_obj = None
110 self._keep_kernel_on_exit = None
112 self._keep_kernel_on_exit = None
111
113
112 # Initialize widget styling.
114 # Initialize widget styling.
113 if self.style_sheet:
115 if self.style_sheet:
114 self._style_sheet_changed()
116 self._style_sheet_changed()
115 self._syntax_style_changed()
117 self._syntax_style_changed()
116 else:
118 else:
117 self.set_default_style()
119 self.set_default_style()
118
120
119 #---------------------------------------------------------------------------
121 #---------------------------------------------------------------------------
120 # 'BaseFrontendMixin' abstract interface
122 # 'BaseFrontendMixin' abstract interface
121 #---------------------------------------------------------------------------
123 #---------------------------------------------------------------------------
122
124
123 def _handle_complete_reply(self, rep):
125 def _handle_complete_reply(self, rep):
124 """ Reimplemented to support IPython's improved completion machinery.
126 """ Reimplemented to support IPython's improved completion machinery.
125 """
127 """
126 cursor = self._get_cursor()
128 cursor = self._get_cursor()
127 info = self._request_info.get('complete')
129 info = self._request_info.get('complete')
128 if info and info.id == rep['parent_header']['msg_id'] and \
130 if info and info.id == rep['parent_header']['msg_id'] and \
129 info.pos == cursor.position():
131 info.pos == cursor.position():
130 matches = rep['content']['matches']
132 matches = rep['content']['matches']
131 text = rep['content']['matched_text']
133 text = rep['content']['matched_text']
132 offset = len(text)
134 offset = len(text)
133
135
134 # Clean up matches with period and path separators if the matched
136 # Clean up matches with period and path separators if the matched
135 # text has not been transformed. This is done by truncating all
137 # text has not been transformed. This is done by truncating all
136 # but the last component and then suitably decreasing the offset
138 # but the last component and then suitably decreasing the offset
137 # between the current cursor position and the start of completion.
139 # between the current cursor position and the start of completion.
138 if len(matches) > 1 and matches[0][:offset] == text:
140 if len(matches) > 1 and matches[0][:offset] == text:
139 parts = re.split(r'[./\\]', text)
141 parts = re.split(r'[./\\]', text)
140 sep_count = len(parts) - 1
142 sep_count = len(parts) - 1
141 if sep_count:
143 if sep_count:
142 chop_length = sum(map(len, parts[:sep_count])) + sep_count
144 chop_length = sum(map(len, parts[:sep_count])) + sep_count
143 matches = [ match[chop_length:] for match in matches ]
145 matches = [ match[chop_length:] for match in matches ]
144 offset -= chop_length
146 offset -= chop_length
145
147
146 # Move the cursor to the start of the match and complete.
148 # Move the cursor to the start of the match and complete.
147 cursor.movePosition(QtGui.QTextCursor.Left, n=offset)
149 cursor.movePosition(QtGui.QTextCursor.Left, n=offset)
148 self._complete_with_items(cursor, matches)
150 self._complete_with_items(cursor, matches)
149
151
150 def _handle_execute_reply(self, msg):
152 def _handle_execute_reply(self, msg):
151 """ Reimplemented to support prompt requests.
153 """ Reimplemented to support prompt requests.
152 """
154 """
153 info = self._request_info.get('execute')
155 info = self._request_info.get('execute')
154 if info and info.id == msg['parent_header']['msg_id']:
156 if info and info.id == msg['parent_header']['msg_id']:
155 if info.kind == 'prompt':
157 if info.kind == 'prompt':
156 number = msg['content']['execution_count'] + 1
158 number = msg['content']['execution_count'] + 1
157 self._show_interpreter_prompt(number)
159 self._show_interpreter_prompt(number)
158 else:
160 else:
159 super(IPythonWidget, self)._handle_execute_reply(msg)
161 super(IPythonWidget, self)._handle_execute_reply(msg)
160
162
161 def _handle_history_tail_reply(self, msg):
163 def _handle_history_tail_reply(self, msg):
162 """ Implemented to handle history tail replies, which are only supported
164 """ Implemented to handle history tail replies, which are only supported
163 by the IPython kernel.
165 by the IPython kernel.
164 """
166 """
165 history_items = msg['content']['history']
167 history_items = msg['content']['history']
166 items = [ line.rstrip() for _, _, line in history_items ]
168 items = [ line.rstrip() for _, _, line in history_items ]
167 self._set_history(items)
169 self._set_history(items)
168
170
169 def _handle_pyout(self, msg):
171 def _handle_pyout(self, msg):
170 """ Reimplemented for IPython-style "display hook".
172 """ Reimplemented for IPython-style "display hook".
171 """
173 """
172 if not self._hidden and self._is_from_this_session(msg):
174 if not self._hidden and self._is_from_this_session(msg):
173 content = msg['content']
175 content = msg['content']
174 prompt_number = content['execution_count']
176 prompt_number = content['execution_count']
175 data = content['data']
177 data = content['data']
176 if data.has_key('text/html'):
178 if data.has_key('text/html'):
177 self._append_plain_text(self.output_sep)
179 self._append_plain_text(self.output_sep)
178 self._append_html(self._make_out_prompt(prompt_number))
180 self._append_html(self._make_out_prompt(prompt_number))
179 html = data['text/html']
181 html = data['text/html']
180 self._append_plain_text('\n')
182 self._append_plain_text('\n')
181 self._append_html(html + self.output_sep2)
183 self._append_html(html + self.output_sep2)
182 elif data.has_key('text/plain'):
184 elif data.has_key('text/plain'):
183 self._append_plain_text(self.output_sep)
185 self._append_plain_text(self.output_sep)
184 self._append_html(self._make_out_prompt(prompt_number))
186 self._append_html(self._make_out_prompt(prompt_number))
185 text = data['text/plain']
187 text = data['text/plain']
186 self._append_plain_text(text + self.output_sep2)
188 self._append_plain_text(text + self.output_sep2)
187
189
188 def _handle_display_data(self, msg):
190 def _handle_display_data(self, msg):
189 """ The base handler for the ``display_data`` message.
191 """ The base handler for the ``display_data`` message.
190 """
192 """
191 # For now, we don't display data from other frontends, but we
193 # For now, we don't display data from other frontends, but we
192 # eventually will as this allows all frontends to monitor the display
194 # eventually will as this allows all frontends to monitor the display
193 # data. But we need to figure out how to handle this in the GUI.
195 # data. But we need to figure out how to handle this in the GUI.
194 if not self._hidden and self._is_from_this_session(msg):
196 if not self._hidden and self._is_from_this_session(msg):
195 source = msg['content']['source']
197 source = msg['content']['source']
196 data = msg['content']['data']
198 data = msg['content']['data']
197 metadata = msg['content']['metadata']
199 metadata = msg['content']['metadata']
198 # In the regular IPythonWidget, we simply print the plain text
200 # In the regular IPythonWidget, we simply print the plain text
199 # representation.
201 # representation.
200 if data.has_key('text/html'):
202 if data.has_key('text/html'):
201 html = data['text/html']
203 html = data['text/html']
202 self._append_html(html)
204 self._append_html(html)
203 elif data.has_key('text/plain'):
205 elif data.has_key('text/plain'):
204 text = data['text/plain']
206 text = data['text/plain']
205 self._append_plain_text(text)
207 self._append_plain_text(text)
206 # This newline seems to be needed for text and html output.
208 # This newline seems to be needed for text and html output.
207 self._append_plain_text(u'\n')
209 self._append_plain_text(u'\n')
208
210
209 def _started_channels(self):
211 def _started_channels(self):
210 """ Reimplemented to make a history request.
212 """ Reimplemented to make a history request.
211 """
213 """
212 super(IPythonWidget, self)._started_channels()
214 super(IPythonWidget, self)._started_channels()
213 self.kernel_manager.xreq_channel.history_tail(1000)
215 self.kernel_manager.xreq_channel.history_tail(1000)
214
216
215 #---------------------------------------------------------------------------
217 #---------------------------------------------------------------------------
216 # 'ConsoleWidget' public interface
218 # 'ConsoleWidget' public interface
217 #---------------------------------------------------------------------------
219 #---------------------------------------------------------------------------
218
220
219 def copy(self):
221 def copy(self):
220 """ Copy the currently selected text to the clipboard, removing prompts
222 """ Copy the currently selected text to the clipboard, removing prompts
221 if possible.
223 if possible.
222 """
224 """
223 text = self._control.textCursor().selection().toPlainText()
225 text = self._control.textCursor().selection().toPlainText()
224 if text:
226 if text:
225 lines = map(transform_ipy_prompt, text.splitlines())
227 lines = map(transform_ipy_prompt, text.splitlines())
226 text = '\n'.join(lines)
228 text = '\n'.join(lines)
227 QtGui.QApplication.clipboard().setText(text)
229 QtGui.QApplication.clipboard().setText(text)
228
230
229 #---------------------------------------------------------------------------
231 #---------------------------------------------------------------------------
230 # 'FrontendWidget' public interface
232 # 'FrontendWidget' public interface
231 #---------------------------------------------------------------------------
233 #---------------------------------------------------------------------------
232
234
233 def execute_file(self, path, hidden=False):
235 def execute_file(self, path, hidden=False):
234 """ Reimplemented to use the 'run' magic.
236 """ Reimplemented to use the 'run' magic.
235 """
237 """
238 # Use forward slashes on Windows to avoid escaping each separator.
239 if sys.platform == 'win32':
240 path = os.path.normpath(path).replace('\\', '/')
241
236 self.execute('%%run %s' % path, hidden=hidden)
242 self.execute('%%run %s' % path, hidden=hidden)
237
243
238 #---------------------------------------------------------------------------
244 #---------------------------------------------------------------------------
239 # 'FrontendWidget' protected interface
245 # 'FrontendWidget' protected interface
240 #---------------------------------------------------------------------------
246 #---------------------------------------------------------------------------
241
247
242 def _complete(self):
248 def _complete(self):
243 """ Reimplemented to support IPython's improved completion machinery.
249 """ Reimplemented to support IPython's improved completion machinery.
244 """
250 """
245 # We let the kernel split the input line, so we *always* send an empty
251 # We let the kernel split the input line, so we *always* send an empty
246 # text field. Readline-based frontends do get a real text field which
252 # text field. Readline-based frontends do get a real text field which
247 # they can use.
253 # they can use.
248 text = ''
254 text = ''
249
255
250 # Send the completion request to the kernel
256 # Send the completion request to the kernel
251 msg_id = self.kernel_manager.xreq_channel.complete(
257 msg_id = self.kernel_manager.xreq_channel.complete(
252 text, # text
258 text, # text
253 self._get_input_buffer_cursor_line(), # line
259 self._get_input_buffer_cursor_line(), # line
254 self._get_input_buffer_cursor_column(), # cursor_pos
260 self._get_input_buffer_cursor_column(), # cursor_pos
255 self.input_buffer) # block
261 self.input_buffer) # block
256 pos = self._get_cursor().position()
262 pos = self._get_cursor().position()
257 info = self._CompletionRequest(msg_id, pos)
263 info = self._CompletionRequest(msg_id, pos)
258 self._request_info['complete'] = info
264 self._request_info['complete'] = info
259
265
260 def _get_banner(self):
266 def _get_banner(self):
261 """ Reimplemented to return IPython's default banner.
267 """ Reimplemented to return IPython's default banner.
262 """
268 """
263 return default_gui_banner
269 return default_gui_banner
264
270
265 def _process_execute_error(self, msg):
271 def _process_execute_error(self, msg):
266 """ Reimplemented for IPython-style traceback formatting.
272 """ Reimplemented for IPython-style traceback formatting.
267 """
273 """
268 content = msg['content']
274 content = msg['content']
269 traceback = '\n'.join(content['traceback']) + '\n'
275 traceback = '\n'.join(content['traceback']) + '\n'
270 if False:
276 if False:
271 # FIXME: For now, tracebacks come as plain text, so we can't use
277 # FIXME: For now, tracebacks come as plain text, so we can't use
272 # the html renderer yet. Once we refactor ultratb to produce
278 # the html renderer yet. Once we refactor ultratb to produce
273 # properly styled tracebacks, this branch should be the default
279 # properly styled tracebacks, this branch should be the default
274 traceback = traceback.replace(' ', '&nbsp;')
280 traceback = traceback.replace(' ', '&nbsp;')
275 traceback = traceback.replace('\n', '<br/>')
281 traceback = traceback.replace('\n', '<br/>')
276
282
277 ename = content['ename']
283 ename = content['ename']
278 ename_styled = '<span class="error">%s</span>' % ename
284 ename_styled = '<span class="error">%s</span>' % ename
279 traceback = traceback.replace(ename, ename_styled)
285 traceback = traceback.replace(ename, ename_styled)
280
286
281 self._append_html(traceback)
287 self._append_html(traceback)
282 else:
288 else:
283 # This is the fallback for now, using plain text with ansi escapes
289 # This is the fallback for now, using plain text with ansi escapes
284 self._append_plain_text(traceback)
290 self._append_plain_text(traceback)
285
291
286 def _process_execute_payload(self, item):
292 def _process_execute_payload(self, item):
287 """ Reimplemented to dispatch payloads to handler methods.
293 """ Reimplemented to dispatch payloads to handler methods.
288 """
294 """
289 handler = self._payload_handlers.get(item['source'])
295 handler = self._payload_handlers.get(item['source'])
290 if handler is None:
296 if handler is None:
291 # We have no handler for this type of payload, simply ignore it
297 # We have no handler for this type of payload, simply ignore it
292 return False
298 return False
293 else:
299 else:
294 handler(item)
300 handler(item)
295 return True
301 return True
296
302
297 def _show_interpreter_prompt(self, number=None):
303 def _show_interpreter_prompt(self, number=None):
298 """ Reimplemented for IPython-style prompts.
304 """ Reimplemented for IPython-style prompts.
299 """
305 """
300 # If a number was not specified, make a prompt number request.
306 # If a number was not specified, make a prompt number request.
301 if number is None:
307 if number is None:
302 msg_id = self.kernel_manager.xreq_channel.execute('', silent=True)
308 msg_id = self.kernel_manager.xreq_channel.execute('', silent=True)
303 info = self._ExecutionRequest(msg_id, 'prompt')
309 info = self._ExecutionRequest(msg_id, 'prompt')
304 self._request_info['execute'] = info
310 self._request_info['execute'] = info
305 return
311 return
306
312
307 # Show a new prompt and save information about it so that it can be
313 # Show a new prompt and save information about it so that it can be
308 # updated later if the prompt number turns out to be wrong.
314 # updated later if the prompt number turns out to be wrong.
309 self._prompt_sep = self.input_sep
315 self._prompt_sep = self.input_sep
310 self._show_prompt(self._make_in_prompt(number), html=True)
316 self._show_prompt(self._make_in_prompt(number), html=True)
311 block = self._control.document().lastBlock()
317 block = self._control.document().lastBlock()
312 length = len(self._prompt)
318 length = len(self._prompt)
313 self._previous_prompt_obj = self._PromptBlock(block, length, number)
319 self._previous_prompt_obj = self._PromptBlock(block, length, number)
314
320
315 # Update continuation prompt to reflect (possibly) new prompt length.
321 # Update continuation prompt to reflect (possibly) new prompt length.
316 self._set_continuation_prompt(
322 self._set_continuation_prompt(
317 self._make_continuation_prompt(self._prompt), html=True)
323 self._make_continuation_prompt(self._prompt), html=True)
318
324
319 # Load code from the %loadpy magic, if necessary.
325 # Load code from the %loadpy magic, if necessary.
320 if self._code_to_load is not None:
326 if self._code_to_load is not None:
321 self.input_buffer = dedent(self._code_to_load.rstrip())
327 self.input_buffer = dedent(self._code_to_load.rstrip())
322 self._code_to_load = None
328 self._code_to_load = None
323
329
324 def _show_interpreter_prompt_for_reply(self, msg):
330 def _show_interpreter_prompt_for_reply(self, msg):
325 """ Reimplemented for IPython-style prompts.
331 """ Reimplemented for IPython-style prompts.
326 """
332 """
327 # Update the old prompt number if necessary.
333 # Update the old prompt number if necessary.
328 content = msg['content']
334 content = msg['content']
329 previous_prompt_number = content['execution_count']
335 previous_prompt_number = content['execution_count']
330 if self._previous_prompt_obj and \
336 if self._previous_prompt_obj and \
331 self._previous_prompt_obj.number != previous_prompt_number:
337 self._previous_prompt_obj.number != previous_prompt_number:
332 block = self._previous_prompt_obj.block
338 block = self._previous_prompt_obj.block
333
339
334 # Make sure the prompt block has not been erased.
340 # Make sure the prompt block has not been erased.
335 if block.isValid() and block.text():
341 if block.isValid() and block.text():
336
342
337 # Remove the old prompt and insert a new prompt.
343 # Remove the old prompt and insert a new prompt.
338 cursor = QtGui.QTextCursor(block)
344 cursor = QtGui.QTextCursor(block)
339 cursor.movePosition(QtGui.QTextCursor.Right,
345 cursor.movePosition(QtGui.QTextCursor.Right,
340 QtGui.QTextCursor.KeepAnchor,
346 QtGui.QTextCursor.KeepAnchor,
341 self._previous_prompt_obj.length)
347 self._previous_prompt_obj.length)
342 prompt = self._make_in_prompt(previous_prompt_number)
348 prompt = self._make_in_prompt(previous_prompt_number)
343 self._prompt = self._insert_html_fetching_plain_text(
349 self._prompt = self._insert_html_fetching_plain_text(
344 cursor, prompt)
350 cursor, prompt)
345
351
346 # When the HTML is inserted, Qt blows away the syntax
352 # When the HTML is inserted, Qt blows away the syntax
347 # highlighting for the line, so we need to rehighlight it.
353 # highlighting for the line, so we need to rehighlight it.
348 self._highlighter.rehighlightBlock(cursor.block())
354 self._highlighter.rehighlightBlock(cursor.block())
349
355
350 self._previous_prompt_obj = None
356 self._previous_prompt_obj = None
351
357
352 # Show a new prompt with the kernel's estimated prompt number.
358 # Show a new prompt with the kernel's estimated prompt number.
353 self._show_interpreter_prompt(previous_prompt_number + 1)
359 self._show_interpreter_prompt(previous_prompt_number + 1)
354
360
355 #---------------------------------------------------------------------------
361 #---------------------------------------------------------------------------
356 # 'IPythonWidget' interface
362 # 'IPythonWidget' interface
357 #---------------------------------------------------------------------------
363 #---------------------------------------------------------------------------
358
364
359 def set_default_style(self, colors='lightbg'):
365 def set_default_style(self, colors='lightbg'):
360 """ Sets the widget style to the class defaults.
366 """ Sets the widget style to the class defaults.
361
367
362 Parameters:
368 Parameters:
363 -----------
369 -----------
364 colors : str, optional (default lightbg)
370 colors : str, optional (default lightbg)
365 Whether to use the default IPython light background or dark
371 Whether to use the default IPython light background or dark
366 background or B&W style.
372 background or B&W style.
367 """
373 """
368 colors = colors.lower()
374 colors = colors.lower()
369 if colors=='lightbg':
375 if colors=='lightbg':
370 self.style_sheet = default_light_style_sheet
376 self.style_sheet = default_light_style_sheet
371 self.syntax_style = default_light_syntax_style
377 self.syntax_style = default_light_syntax_style
372 elif colors=='linux':
378 elif colors=='linux':
373 self.style_sheet = default_dark_style_sheet
379 self.style_sheet = default_dark_style_sheet
374 self.syntax_style = default_dark_syntax_style
380 self.syntax_style = default_dark_syntax_style
375 elif colors=='nocolor':
381 elif colors=='nocolor':
376 self.style_sheet = default_bw_style_sheet
382 self.style_sheet = default_bw_style_sheet
377 self.syntax_style = default_bw_syntax_style
383 self.syntax_style = default_bw_syntax_style
378 else:
384 else:
379 raise KeyError("No such color scheme: %s"%colors)
385 raise KeyError("No such color scheme: %s"%colors)
380
386
381 #---------------------------------------------------------------------------
387 #---------------------------------------------------------------------------
382 # 'IPythonWidget' protected interface
388 # 'IPythonWidget' protected interface
383 #---------------------------------------------------------------------------
389 #---------------------------------------------------------------------------
384
390
385 def _edit(self, filename, line=None):
391 def _edit(self, filename, line=None):
386 """ Opens a Python script for editing.
392 """ Opens a Python script for editing.
387
393
388 Parameters:
394 Parameters:
389 -----------
395 -----------
390 filename : str
396 filename : str
391 A path to a local system file.
397 A path to a local system file.
392
398
393 line : int, optional
399 line : int, optional
394 A line of interest in the file.
400 A line of interest in the file.
395 """
401 """
396 if self.custom_edit:
402 if self.custom_edit:
397 self.custom_edit_requested.emit(filename, line)
403 self.custom_edit_requested.emit(filename, line)
398 elif self.editor == 'default':
404 elif self.editor == 'default':
399 self._append_plain_text('No default editor available.\n')
405 self._append_plain_text('No default editor available.\n')
400 else:
406 else:
401 try:
407 try:
402 filename = '"%s"' % filename
408 filename = '"%s"' % filename
403 if line and self.editor_line:
409 if line and self.editor_line:
404 command = self.editor_line.format(filename=filename,
410 command = self.editor_line.format(filename=filename,
405 line=line)
411 line=line)
406 else:
412 else:
407 try:
413 try:
408 command = self.editor.format()
414 command = self.editor.format()
409 except KeyError:
415 except KeyError:
410 command = self.editor.format(filename=filename)
416 command = self.editor.format(filename=filename)
411 else:
417 else:
412 command += ' ' + filename
418 command += ' ' + filename
413 except KeyError:
419 except KeyError:
414 self._append_plain_text('Invalid editor command.\n')
420 self._append_plain_text('Invalid editor command.\n')
415 else:
421 else:
416 try:
422 try:
417 Popen(command, shell=True)
423 Popen(command, shell=True)
418 except OSError:
424 except OSError:
419 msg = 'Opening editor with command "%s" failed.\n'
425 msg = 'Opening editor with command "%s" failed.\n'
420 self._append_plain_text(msg % command)
426 self._append_plain_text(msg % command)
421
427
422 def _make_in_prompt(self, number):
428 def _make_in_prompt(self, number):
423 """ Given a prompt number, returns an HTML In prompt.
429 """ Given a prompt number, returns an HTML In prompt.
424 """
430 """
425 body = self.in_prompt % number
431 body = self.in_prompt % number
426 return '<span class="in-prompt">%s</span>' % body
432 return '<span class="in-prompt">%s</span>' % body
427
433
428 def _make_continuation_prompt(self, prompt):
434 def _make_continuation_prompt(self, prompt):
429 """ Given a plain text version of an In prompt, returns an HTML
435 """ Given a plain text version of an In prompt, returns an HTML
430 continuation prompt.
436 continuation prompt.
431 """
437 """
432 end_chars = '...: '
438 end_chars = '...: '
433 space_count = len(prompt.lstrip('\n')) - len(end_chars)
439 space_count = len(prompt.lstrip('\n')) - len(end_chars)
434 body = '&nbsp;' * space_count + end_chars
440 body = '&nbsp;' * space_count + end_chars
435 return '<span class="in-prompt">%s</span>' % body
441 return '<span class="in-prompt">%s</span>' % body
436
442
437 def _make_out_prompt(self, number):
443 def _make_out_prompt(self, number):
438 """ Given a prompt number, returns an HTML Out prompt.
444 """ Given a prompt number, returns an HTML Out prompt.
439 """
445 """
440 body = self.out_prompt % number
446 body = self.out_prompt % number
441 return '<span class="out-prompt">%s</span>' % body
447 return '<span class="out-prompt">%s</span>' % body
442
448
443 #------ Payload handlers --------------------------------------------------
449 #------ Payload handlers --------------------------------------------------
444
450
445 # Payload handlers with a generic interface: each takes the opaque payload
451 # Payload handlers with a generic interface: each takes the opaque payload
446 # dict, unpacks it and calls the underlying functions with the necessary
452 # dict, unpacks it and calls the underlying functions with the necessary
447 # arguments.
453 # arguments.
448
454
449 def _handle_payload_edit(self, item):
455 def _handle_payload_edit(self, item):
450 self._edit(item['filename'], item['line_number'])
456 self._edit(item['filename'], item['line_number'])
451
457
452 def _handle_payload_exit(self, item):
458 def _handle_payload_exit(self, item):
453 self._keep_kernel_on_exit = item['keepkernel']
459 self._keep_kernel_on_exit = item['keepkernel']
454 self.exit_requested.emit()
460 self.exit_requested.emit()
455
461
456 def _handle_payload_loadpy(self, item):
462 def _handle_payload_loadpy(self, item):
457 # Simple save the text of the .py file for later. The text is written
463 # Simple save the text of the .py file for later. The text is written
458 # to the buffer when _prompt_started_hook is called.
464 # to the buffer when _prompt_started_hook is called.
459 self._code_to_load = item['text']
465 self._code_to_load = item['text']
460
466
461 def _handle_payload_page(self, item):
467 def _handle_payload_page(self, item):
462 # Since the plain text widget supports only a very small subset of HTML
468 # Since the plain text widget supports only a very small subset of HTML
463 # and we have no control over the HTML source, we only page HTML
469 # and we have no control over the HTML source, we only page HTML
464 # payloads in the rich text widget.
470 # payloads in the rich text widget.
465 if item['html'] and self.kind == 'rich':
471 if item['html'] and self.kind == 'rich':
466 self._page(item['html'], html=True)
472 self._page(item['html'], html=True)
467 else:
473 else:
468 self._page(item['text'], html=False)
474 self._page(item['text'], html=False)
469
475
470 #------ Trait change handlers --------------------------------------------
476 #------ Trait change handlers --------------------------------------------
471
477
472 def _style_sheet_changed(self):
478 def _style_sheet_changed(self):
473 """ Set the style sheets of the underlying widgets.
479 """ Set the style sheets of the underlying widgets.
474 """
480 """
475 self.setStyleSheet(self.style_sheet)
481 self.setStyleSheet(self.style_sheet)
476 self._control.document().setDefaultStyleSheet(self.style_sheet)
482 self._control.document().setDefaultStyleSheet(self.style_sheet)
477 if self._page_control:
483 if self._page_control:
478 self._page_control.document().setDefaultStyleSheet(self.style_sheet)
484 self._page_control.document().setDefaultStyleSheet(self.style_sheet)
479
485
480 bg_color = self._control.palette().window().color()
486 bg_color = self._control.palette().window().color()
481 self._ansi_processor.set_background_color(bg_color)
487 self._ansi_processor.set_background_color(bg_color)
482
488
483 def _syntax_style_changed(self):
489 def _syntax_style_changed(self):
484 """ Set the style for the syntax highlighter.
490 """ Set the style for the syntax highlighter.
485 """
491 """
486 if self.syntax_style:
492 if self.syntax_style:
487 self._highlighter.set_style(self.syntax_style)
493 self._highlighter.set_style(self.syntax_style)
488 else:
494 else:
489 self._highlighter.set_style_sheet(self.style_sheet)
495 self._highlighter.set_style_sheet(self.style_sheet)
490
496
General Comments 0
You need to be logged in to leave comments. Login now