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