##// END OF EJS Templates
qtconsole: fix race-cond, handle multiple exec...
Matthias BUSSONNIER -
Show More
@@ -1,728 +1,731 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 import uuid
7 import uuid
8
8
9 # System library imports
9 # System library imports
10 from pygments.lexers import PythonLexer
10 from pygments.lexers import PythonLexer
11 from IPython.external import qt
11 from IPython.external import qt
12 from IPython.external.qt import QtCore, QtGui
12 from IPython.external.qt import QtCore, QtGui
13
13
14 # Local imports
14 # Local imports
15 from IPython.core.inputsplitter import InputSplitter, transform_classic_prompt
15 from IPython.core.inputsplitter import InputSplitter, transform_classic_prompt
16 from IPython.core.oinspect import call_tip
16 from IPython.core.oinspect import call_tip
17 from IPython.frontend.qt.base_frontend_mixin import BaseFrontendMixin
17 from IPython.frontend.qt.base_frontend_mixin import BaseFrontendMixin
18 from IPython.utils.traitlets import Bool, Instance, Unicode
18 from IPython.utils.traitlets import Bool, Instance, Unicode
19 from bracket_matcher import BracketMatcher
19 from bracket_matcher import BracketMatcher
20 from call_tip_widget import CallTipWidget
20 from call_tip_widget import CallTipWidget
21 from completion_lexer import CompletionLexer
21 from completion_lexer import CompletionLexer
22 from history_console_widget import HistoryConsoleWidget
22 from history_console_widget import HistoryConsoleWidget
23 from pygments_highlighter import PygmentsHighlighter
23 from pygments_highlighter import PygmentsHighlighter
24
24
25
25
26 class FrontendHighlighter(PygmentsHighlighter):
26 class FrontendHighlighter(PygmentsHighlighter):
27 """ A PygmentsHighlighter that understands and ignores prompts.
27 """ A PygmentsHighlighter that understands and ignores prompts.
28 """
28 """
29
29
30 def __init__(self, frontend):
30 def __init__(self, frontend):
31 super(FrontendHighlighter, self).__init__(frontend._control.document())
31 super(FrontendHighlighter, self).__init__(frontend._control.document())
32 self._current_offset = 0
32 self._current_offset = 0
33 self._frontend = frontend
33 self._frontend = frontend
34 self.highlighting_on = False
34 self.highlighting_on = False
35
35
36 def highlightBlock(self, string):
36 def highlightBlock(self, string):
37 """ Highlight a block of text. Reimplemented to highlight selectively.
37 """ Highlight a block of text. Reimplemented to highlight selectively.
38 """
38 """
39 if not self.highlighting_on:
39 if not self.highlighting_on:
40 return
40 return
41
41
42 # The input to this function is a unicode string that may contain
42 # The input to this function is a unicode string that may contain
43 # paragraph break characters, non-breaking spaces, etc. Here we acquire
43 # paragraph break characters, non-breaking spaces, etc. Here we acquire
44 # the string as plain text so we can compare it.
44 # the string as plain text so we can compare it.
45 current_block = self.currentBlock()
45 current_block = self.currentBlock()
46 string = self._frontend._get_block_plain_text(current_block)
46 string = self._frontend._get_block_plain_text(current_block)
47
47
48 # Decide whether to check for the regular or continuation prompt.
48 # Decide whether to check for the regular or continuation prompt.
49 if current_block.contains(self._frontend._prompt_pos):
49 if current_block.contains(self._frontend._prompt_pos):
50 prompt = self._frontend._prompt
50 prompt = self._frontend._prompt
51 else:
51 else:
52 prompt = self._frontend._continuation_prompt
52 prompt = self._frontend._continuation_prompt
53
53
54 # Only highlight if we can identify a prompt, but make sure not to
54 # Only highlight if we can identify a prompt, but make sure not to
55 # highlight the prompt.
55 # highlight the prompt.
56 if string.startswith(prompt):
56 if string.startswith(prompt):
57 self._current_offset = len(prompt)
57 self._current_offset = len(prompt)
58 string = string[len(prompt):]
58 string = string[len(prompt):]
59 super(FrontendHighlighter, self).highlightBlock(string)
59 super(FrontendHighlighter, self).highlightBlock(string)
60
60
61 def rehighlightBlock(self, block):
61 def rehighlightBlock(self, block):
62 """ Reimplemented to temporarily enable highlighting if disabled.
62 """ Reimplemented to temporarily enable highlighting if disabled.
63 """
63 """
64 old = self.highlighting_on
64 old = self.highlighting_on
65 self.highlighting_on = True
65 self.highlighting_on = True
66 super(FrontendHighlighter, self).rehighlightBlock(block)
66 super(FrontendHighlighter, self).rehighlightBlock(block)
67 self.highlighting_on = old
67 self.highlighting_on = old
68
68
69 def setFormat(self, start, count, format):
69 def setFormat(self, start, count, format):
70 """ Reimplemented to highlight selectively.
70 """ Reimplemented to highlight selectively.
71 """
71 """
72 start += self._current_offset
72 start += self._current_offset
73 super(FrontendHighlighter, self).setFormat(start, count, format)
73 super(FrontendHighlighter, self).setFormat(start, count, format)
74
74
75
75
76 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
76 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
77 """ A Qt frontend for a generic Python kernel.
77 """ A Qt frontend for a generic Python kernel.
78 """
78 """
79
79
80 # The text to show when the kernel is (re)started.
80 # The text to show when the kernel is (re)started.
81 banner = Unicode()
81 banner = Unicode()
82
82
83 # An option and corresponding signal for overriding the default kernel
83 # An option and corresponding signal for overriding the default kernel
84 # interrupt behavior.
84 # interrupt behavior.
85 custom_interrupt = Bool(False)
85 custom_interrupt = Bool(False)
86 custom_interrupt_requested = QtCore.Signal()
86 custom_interrupt_requested = QtCore.Signal()
87
87
88 # An option and corresponding signals for overriding the default kernel
88 # An option and corresponding signals for overriding the default kernel
89 # restart behavior.
89 # restart behavior.
90 custom_restart = Bool(False)
90 custom_restart = Bool(False)
91 custom_restart_kernel_died = QtCore.Signal(float)
91 custom_restart_kernel_died = QtCore.Signal(float)
92 custom_restart_requested = QtCore.Signal()
92 custom_restart_requested = QtCore.Signal()
93
93
94 # Whether to automatically show calltips on open-parentheses.
94 # Whether to automatically show calltips on open-parentheses.
95 enable_calltips = Bool(True, config=True,
95 enable_calltips = Bool(True, config=True,
96 help="Whether to draw information calltips on open-parentheses.")
96 help="Whether to draw information calltips on open-parentheses.")
97
97
98 # Emitted when a user visible 'execute_request' has been submitted to the
98 # Emitted when a user visible 'execute_request' has been submitted to the
99 # kernel from the FrontendWidget. Contains the code to be executed.
99 # kernel from the FrontendWidget. Contains the code to be executed.
100 executing = QtCore.Signal(object)
100 executing = QtCore.Signal(object)
101
101
102 # Emitted when a user-visible 'execute_reply' has been received from the
102 # Emitted when a user-visible 'execute_reply' has been received from the
103 # kernel and processed by the FrontendWidget. Contains the response message.
103 # kernel and processed by the FrontendWidget. Contains the response message.
104 executed = QtCore.Signal(object)
104 executed = QtCore.Signal(object)
105
105
106 # Emitted when an exit request has been received from the kernel.
106 # Emitted when an exit request has been received from the kernel.
107 exit_requested = QtCore.Signal(object)
107 exit_requested = QtCore.Signal(object)
108
108
109 # Protected class variables.
109 # Protected class variables.
110 _CallTipRequest = namedtuple('_CallTipRequest', ['id', 'pos'])
110 _CallTipRequest = namedtuple('_CallTipRequest', ['id', 'pos'])
111 _CompletionRequest = namedtuple('_CompletionRequest', ['id', 'pos'])
111 _CompletionRequest = namedtuple('_CompletionRequest', ['id', 'pos'])
112 _ExecutionRequest = namedtuple('_ExecutionRequest', ['id', 'kind'])
112 _ExecutionRequest = namedtuple('_ExecutionRequest', ['id', 'kind'])
113 _input_splitter_class = InputSplitter
113 _input_splitter_class = InputSplitter
114 _local_kernel = False
114 _local_kernel = False
115 _highlighter = Instance(FrontendHighlighter)
115 _highlighter = Instance(FrontendHighlighter)
116
116
117 #---------------------------------------------------------------------------
117 #---------------------------------------------------------------------------
118 # 'object' interface
118 # 'object' interface
119 #---------------------------------------------------------------------------
119 #---------------------------------------------------------------------------
120
120
121 def __init__(self, *args, **kw):
121 def __init__(self, *args, **kw):
122 super(FrontendWidget, self).__init__(*args, **kw)
122 super(FrontendWidget, self).__init__(*args, **kw)
123 # FIXME: remove this when PySide min version is updated past 1.0.7
123 # FIXME: remove this when PySide min version is updated past 1.0.7
124 # forcefully disable calltips if PySide is < 1.0.7, because they crash
124 # forcefully disable calltips if PySide is < 1.0.7, because they crash
125 if qt.QT_API == qt.QT_API_PYSIDE:
125 if qt.QT_API == qt.QT_API_PYSIDE:
126 import PySide
126 import PySide
127 if PySide.__version_info__ < (1,0,7):
127 if PySide.__version_info__ < (1,0,7):
128 self.log.warn("PySide %s < 1.0.7 detected, disabling calltips" % PySide.__version__)
128 self.log.warn("PySide %s < 1.0.7 detected, disabling calltips" % PySide.__version__)
129 self.enable_calltips = False
129 self.enable_calltips = False
130
130
131 # FrontendWidget protected variables.
131 # FrontendWidget protected variables.
132 self._bracket_matcher = BracketMatcher(self._control)
132 self._bracket_matcher = BracketMatcher(self._control)
133 self._call_tip_widget = CallTipWidget(self._control)
133 self._call_tip_widget = CallTipWidget(self._control)
134 self._completion_lexer = CompletionLexer(PythonLexer())
134 self._completion_lexer = CompletionLexer(PythonLexer())
135 self._copy_raw_action = QtGui.QAction('Copy (Raw Text)', None)
135 self._copy_raw_action = QtGui.QAction('Copy (Raw Text)', None)
136 self._hidden = False
136 self._hidden = False
137 self._highlighter = FrontendHighlighter(self)
137 self._highlighter = FrontendHighlighter(self)
138 self._input_splitter = self._input_splitter_class(input_mode='cell')
138 self._input_splitter = self._input_splitter_class(input_mode='cell')
139 self._kernel_manager = None
139 self._kernel_manager = None
140 self._request_info = {}
140 self._request_info = {}
141 self._request_info['execute'] = {};
141 self._callback_dict = {}
142 self._callback_dict = {}
142
143
143 # Configure the ConsoleWidget.
144 # Configure the ConsoleWidget.
144 self.tab_width = 4
145 self.tab_width = 4
145 self._set_continuation_prompt('... ')
146 self._set_continuation_prompt('... ')
146
147
147 # Configure the CallTipWidget.
148 # Configure the CallTipWidget.
148 self._call_tip_widget.setFont(self.font)
149 self._call_tip_widget.setFont(self.font)
149 self.font_changed.connect(self._call_tip_widget.setFont)
150 self.font_changed.connect(self._call_tip_widget.setFont)
150
151
151 # Configure actions.
152 # Configure actions.
152 action = self._copy_raw_action
153 action = self._copy_raw_action
153 key = QtCore.Qt.CTRL | QtCore.Qt.SHIFT | QtCore.Qt.Key_C
154 key = QtCore.Qt.CTRL | QtCore.Qt.SHIFT | QtCore.Qt.Key_C
154 action.setEnabled(False)
155 action.setEnabled(False)
155 action.setShortcut(QtGui.QKeySequence(key))
156 action.setShortcut(QtGui.QKeySequence(key))
156 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
157 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
157 action.triggered.connect(self.copy_raw)
158 action.triggered.connect(self.copy_raw)
158 self.copy_available.connect(action.setEnabled)
159 self.copy_available.connect(action.setEnabled)
159 self.addAction(action)
160 self.addAction(action)
160
161
161 # Connect signal handlers.
162 # Connect signal handlers.
162 document = self._control.document()
163 document = self._control.document()
163 document.contentsChange.connect(self._document_contents_change)
164 document.contentsChange.connect(self._document_contents_change)
164
165
165 # Set flag for whether we are connected via localhost.
166 # Set flag for whether we are connected via localhost.
166 self._local_kernel = kw.get('local_kernel',
167 self._local_kernel = kw.get('local_kernel',
167 FrontendWidget._local_kernel)
168 FrontendWidget._local_kernel)
168
169
169 #---------------------------------------------------------------------------
170 #---------------------------------------------------------------------------
170 # 'ConsoleWidget' public interface
171 # 'ConsoleWidget' public interface
171 #---------------------------------------------------------------------------
172 #---------------------------------------------------------------------------
172
173
173 def copy(self):
174 def copy(self):
174 """ Copy the currently selected text to the clipboard, removing prompts.
175 """ Copy the currently selected text to the clipboard, removing prompts.
175 """
176 """
176 text = self._control.textCursor().selection().toPlainText()
177 text = self._control.textCursor().selection().toPlainText()
177 if text:
178 if text:
178 lines = map(transform_classic_prompt, text.splitlines())
179 lines = map(transform_classic_prompt, text.splitlines())
179 text = '\n'.join(lines)
180 text = '\n'.join(lines)
180 QtGui.QApplication.clipboard().setText(text)
181 QtGui.QApplication.clipboard().setText(text)
181
182
182 #---------------------------------------------------------------------------
183 #---------------------------------------------------------------------------
183 # 'ConsoleWidget' abstract interface
184 # 'ConsoleWidget' abstract interface
184 #---------------------------------------------------------------------------
185 #---------------------------------------------------------------------------
185
186
186 def _is_complete(self, source, interactive):
187 def _is_complete(self, source, interactive):
187 """ Returns whether 'source' can be completely processed and a new
188 """ Returns whether 'source' can be completely processed and a new
188 prompt created. When triggered by an Enter/Return key press,
189 prompt created. When triggered by an Enter/Return key press,
189 'interactive' is True; otherwise, it is False.
190 'interactive' is True; otherwise, it is False.
190 """
191 """
191 complete = self._input_splitter.push(source)
192 complete = self._input_splitter.push(source)
192 if interactive:
193 if interactive:
193 complete = not self._input_splitter.push_accepts_more()
194 complete = not self._input_splitter.push_accepts_more()
194 return complete
195 return complete
195
196
196 def _execute(self, source, hidden):
197 def _execute(self, source, hidden):
197 """ Execute 'source'. If 'hidden', do not show any output.
198 """ Execute 'source'. If 'hidden', do not show any output.
198
199
199 See parent class :meth:`execute` docstring for full details.
200 See parent class :meth:`execute` docstring for full details.
200 """
201 """
201 msg_id = self.kernel_manager.shell_channel.execute(source, hidden)
202 msg_id = self.kernel_manager.shell_channel.execute(source, hidden)
202 self._request_info['execute'] = self._ExecutionRequest(msg_id, 'user')
203 self._request_info['execute'][msg_id] = self._ExecutionRequest(msg_id, 'user')
203 self._hidden = hidden
204 self._hidden = hidden
204 if not hidden:
205 if not hidden:
205 self.executing.emit(source)
206 self.executing.emit(source)
206
207
207 def _prompt_started_hook(self):
208 def _prompt_started_hook(self):
208 """ Called immediately after a new prompt is displayed.
209 """ Called immediately after a new prompt is displayed.
209 """
210 """
210 if not self._reading:
211 if not self._reading:
211 self._highlighter.highlighting_on = True
212 self._highlighter.highlighting_on = True
212
213
213 def _prompt_finished_hook(self):
214 def _prompt_finished_hook(self):
214 """ Called immediately after a prompt is finished, i.e. when some input
215 """ Called immediately after a prompt is finished, i.e. when some input
215 will be processed and a new prompt displayed.
216 will be processed and a new prompt displayed.
216 """
217 """
217 # Flush all state from the input splitter so the next round of
218 # Flush all state from the input splitter so the next round of
218 # reading input starts with a clean buffer.
219 # reading input starts with a clean buffer.
219 self._input_splitter.reset()
220 self._input_splitter.reset()
220
221
221 if not self._reading:
222 if not self._reading:
222 self._highlighter.highlighting_on = False
223 self._highlighter.highlighting_on = False
223
224
224 def _tab_pressed(self):
225 def _tab_pressed(self):
225 """ Called when the tab key is pressed. Returns whether to continue
226 """ Called when the tab key is pressed. Returns whether to continue
226 processing the event.
227 processing the event.
227 """
228 """
228 # Perform tab completion if:
229 # Perform tab completion if:
229 # 1) The cursor is in the input buffer.
230 # 1) The cursor is in the input buffer.
230 # 2) There is a non-whitespace character before the cursor.
231 # 2) There is a non-whitespace character before the cursor.
231 text = self._get_input_buffer_cursor_line()
232 text = self._get_input_buffer_cursor_line()
232 if text is None:
233 if text is None:
233 return False
234 return False
234 complete = bool(text[:self._get_input_buffer_cursor_column()].strip())
235 complete = bool(text[:self._get_input_buffer_cursor_column()].strip())
235 if complete:
236 if complete:
236 self._complete()
237 self._complete()
237 return not complete
238 return not complete
238
239
239 #---------------------------------------------------------------------------
240 #---------------------------------------------------------------------------
240 # 'ConsoleWidget' protected interface
241 # 'ConsoleWidget' protected interface
241 #---------------------------------------------------------------------------
242 #---------------------------------------------------------------------------
242
243
243 def _context_menu_make(self, pos):
244 def _context_menu_make(self, pos):
244 """ Reimplemented to add an action for raw copy.
245 """ Reimplemented to add an action for raw copy.
245 """
246 """
246 menu = super(FrontendWidget, self)._context_menu_make(pos)
247 menu = super(FrontendWidget, self)._context_menu_make(pos)
247 for before_action in menu.actions():
248 for before_action in menu.actions():
248 if before_action.shortcut().matches(QtGui.QKeySequence.Paste) == \
249 if before_action.shortcut().matches(QtGui.QKeySequence.Paste) == \
249 QtGui.QKeySequence.ExactMatch:
250 QtGui.QKeySequence.ExactMatch:
250 menu.insertAction(before_action, self._copy_raw_action)
251 menu.insertAction(before_action, self._copy_raw_action)
251 break
252 break
252 return menu
253 return menu
253
254
254 def request_interrupt_kernel(self):
255 def request_interrupt_kernel(self):
255 if self._executing:
256 if self._executing:
256 self.interrupt_kernel()
257 self.interrupt_kernel()
257
258
258 def request_restart_kernel(self):
259 def request_restart_kernel(self):
259 message = 'Are you sure you want to restart the kernel?'
260 message = 'Are you sure you want to restart the kernel?'
260 self.restart_kernel(message, now=False)
261 self.restart_kernel(message, now=False)
261
262
262 def _event_filter_console_keypress(self, event):
263 def _event_filter_console_keypress(self, event):
263 """ Reimplemented for execution interruption and smart backspace.
264 """ Reimplemented for execution interruption and smart backspace.
264 """
265 """
265 key = event.key()
266 key = event.key()
266 if self._control_key_down(event.modifiers(), include_command=False):
267 if self._control_key_down(event.modifiers(), include_command=False):
267
268
268 if key == QtCore.Qt.Key_C and self._executing:
269 if key == QtCore.Qt.Key_C and self._executing:
269 self.request_interrupt_kernel()
270 self.request_interrupt_kernel()
270 return True
271 return True
271
272
272 elif key == QtCore.Qt.Key_Period:
273 elif key == QtCore.Qt.Key_Period:
273 self.request_restart_kernel()
274 self.request_restart_kernel()
274 return True
275 return True
275
276
276 elif not event.modifiers() & QtCore.Qt.AltModifier:
277 elif not event.modifiers() & QtCore.Qt.AltModifier:
277
278
278 # Smart backspace: remove four characters in one backspace if:
279 # Smart backspace: remove four characters in one backspace if:
279 # 1) everything left of the cursor is whitespace
280 # 1) everything left of the cursor is whitespace
280 # 2) the four characters immediately left of the cursor are spaces
281 # 2) the four characters immediately left of the cursor are spaces
281 if key == QtCore.Qt.Key_Backspace:
282 if key == QtCore.Qt.Key_Backspace:
282 col = self._get_input_buffer_cursor_column()
283 col = self._get_input_buffer_cursor_column()
283 cursor = self._control.textCursor()
284 cursor = self._control.textCursor()
284 if col > 3 and not cursor.hasSelection():
285 if col > 3 and not cursor.hasSelection():
285 text = self._get_input_buffer_cursor_line()[:col]
286 text = self._get_input_buffer_cursor_line()[:col]
286 if text.endswith(' ') and not text.strip():
287 if text.endswith(' ') and not text.strip():
287 cursor.movePosition(QtGui.QTextCursor.Left,
288 cursor.movePosition(QtGui.QTextCursor.Left,
288 QtGui.QTextCursor.KeepAnchor, 4)
289 QtGui.QTextCursor.KeepAnchor, 4)
289 cursor.removeSelectedText()
290 cursor.removeSelectedText()
290 return True
291 return True
291
292
292 return super(FrontendWidget, self)._event_filter_console_keypress(event)
293 return super(FrontendWidget, self)._event_filter_console_keypress(event)
293
294
294 def _insert_continuation_prompt(self, cursor):
295 def _insert_continuation_prompt(self, cursor):
295 """ Reimplemented for auto-indentation.
296 """ Reimplemented for auto-indentation.
296 """
297 """
297 super(FrontendWidget, self)._insert_continuation_prompt(cursor)
298 super(FrontendWidget, self)._insert_continuation_prompt(cursor)
298 cursor.insertText(' ' * self._input_splitter.indent_spaces)
299 cursor.insertText(' ' * self._input_splitter.indent_spaces)
299
300
300 #---------------------------------------------------------------------------
301 #---------------------------------------------------------------------------
301 # 'BaseFrontendMixin' abstract interface
302 # 'BaseFrontendMixin' abstract interface
302 #---------------------------------------------------------------------------
303 #---------------------------------------------------------------------------
303
304
304 def _handle_complete_reply(self, rep):
305 def _handle_complete_reply(self, rep):
305 """ Handle replies for tab completion.
306 """ Handle replies for tab completion.
306 """
307 """
307 self.log.debug("complete: %s", rep.get('content', ''))
308 self.log.debug("complete: %s", rep.get('content', ''))
308 cursor = self._get_cursor()
309 cursor = self._get_cursor()
309 info = self._request_info.get('complete')
310 info = self._request_info.get('complete')
310 if info and info.id == rep['parent_header']['msg_id'] and \
311 if info and info.id == rep['parent_header']['msg_id'] and \
311 info.pos == cursor.position():
312 info.pos == cursor.position():
312 text = '.'.join(self._get_context())
313 text = '.'.join(self._get_context())
313 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
314 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
314 self._complete_with_items(cursor, rep['content']['matches'])
315 self._complete_with_items(cursor, rep['content']['matches'])
315
316
316 def _silent_exec_callback(self, expr, callback):
317 def _silent_exec_callback(self, expr, callback):
317 """Silently execute `expr` in the kernel and call `callback` with reply
318 """Silently execute `expr` in the kernel and call `callback` with reply
318
319
319 the `expr` is evaluated silently in the kernel (without) output in
320 the `expr` is evaluated silently in the kernel (without) output in
320 the frontend. Call `callback` with the
321 the frontend. Call `callback` with the
321 `repr <http://docs.python.org/library/functions.html#repr> `_ as first argument
322 `repr <http://docs.python.org/library/functions.html#repr> `_ as first argument
322
323
323 Parameters
324 Parameters
324 ----------
325 ----------
325 expr : string
326 expr : string
326 valid string to be executed by the kernel.
327 valid string to be executed by the kernel.
327 callback : function
328 callback : function
328 function accepting one arguement, as a string. The string will be
329 function accepting one arguement, as a string. The string will be
329 the `repr` of the result of evaluating `expr`
330 the `repr` of the result of evaluating `expr`
330
331
331 The `callback` is called with the 'repr()' of the result of `expr` as
332 The `callback` is called with the 'repr()' of the result of `expr` as
332 first argument. To get the object, do 'eval()' onthe passed value.
333 first argument. To get the object, do 'eval()' onthe passed value.
333
334
334 See Also
335 See Also
335 --------
336 --------
336 _handle_exec_callback : private method, deal with calling callback with reply
337 _handle_exec_callback : private method, deal with calling callback with reply
337
338
338 """
339 """
339
340
340 # generate uuid, which would be used as a indication of wether or not
341 # generate uuid, which would be used as a indication of wether or not
341 # the unique request originate from here (can use msg id ?)
342 # the unique request originate from here (can use msg id ?)
342 local_uuid = str(uuid.uuid1())
343 local_uuid = str(uuid.uuid1())
343 msg_id = self.kernel_manager.shell_channel.execute('',
344 msg_id = self.kernel_manager.shell_channel.execute('',
344 silent=True, user_expressions={ local_uuid:expr })
345 silent=True, user_expressions={ local_uuid:expr })
345 self._callback_dict[local_uuid] = callback
346 self._callback_dict[local_uuid] = callback
346 self._request_info['execute'] = self._ExecutionRequest(msg_id, 'silent_exec_callback')
347 self._request_info['execute'][msg_id] = self._ExecutionRequest(msg_id, 'silent_exec_callback')
347
348
348 def _handle_exec_callback(self, msg):
349 def _handle_exec_callback(self, msg):
349 """Execute `callback` corresonding to `msg` reply, after ``_silent_exec_callback``
350 """Execute `callback` corresonding to `msg` reply, after ``_silent_exec_callback``
350
351
351 Parameters
352 Parameters
352 ----------
353 ----------
353 msg : raw message send by the kernel containing an `user_expressions`
354 msg : raw message send by the kernel containing an `user_expressions`
354 and having a 'silent_exec_callback' kind.
355 and having a 'silent_exec_callback' kind.
355
356
356 Notes
357 Notes
357 -----
358 -----
358 This fonction will look for a `callback` associated with the
359 This fonction will look for a `callback` associated with the
359 corresponding message id. Association has been made by
360 corresponding message id. Association has been made by
360 `_silent_exec_callback`. `callback` is then called with the `repr()`
361 `_silent_exec_callback`. `callback` is then called with the `repr()`
361 of the value of corresponding `user_expressions` as argument.
362 of the value of corresponding `user_expressions` as argument.
362 `callback` is then removed from the known list so that any message
363 `callback` is then removed from the known list so that any message
363 coming again with the same id won't trigger it.
364 coming again with the same id won't trigger it.
364
365
365 """
366 """
366
367
367 user_exp = msg['content']['user_expressions']
368 user_exp = msg['content']['user_expressions']
368 for expression in user_exp:
369 for expression in user_exp:
369 if expression in self._callback_dict:
370 if expression in self._callback_dict:
370 self._callback_dict.pop(expression)(user_exp[expression])
371 self._callback_dict.pop(expression)(user_exp[expression])
371
372
372 def _handle_execute_reply(self, msg):
373 def _handle_execute_reply(self, msg):
373 """ Handles replies for code execution.
374 """ Handles replies for code execution.
374 """
375 """
375 self.log.debug("execute: %s", msg.get('content', ''))
376 self.log.debug("execute: %s", msg.get('content', ''))
376 info = self._request_info.get('execute')
377 info_list = self._request_info.get('execute')
378 msg_id = msg['parent_header']['msg_id']
379 if msg_id in info_list:
380 info = info_list[msg_id]
377 # unset reading flag, because if execute finished, raw_input can't
381 # unset reading flag, because if execute finished, raw_input can't
378 # still be pending.
382 # still be pending.
379 self._reading = False
383 self._reading = False
380 if info and info.id == msg['parent_header']['msg_id'] and \
384 if info and info.kind == 'user' and not self._hidden:
381 info.kind == 'user' and not self._hidden:
382 # Make sure that all output from the SUB channel has been processed
385 # Make sure that all output from the SUB channel has been processed
383 # before writing a new prompt.
386 # before writing a new prompt.
384 self.kernel_manager.sub_channel.flush()
387 self.kernel_manager.sub_channel.flush()
385
388
386 # Reset the ANSI style information to prevent bad text in stdout
389 # Reset the ANSI style information to prevent bad text in stdout
387 # from messing up our colors. We're not a true terminal so we're
390 # from messing up our colors. We're not a true terminal so we're
388 # allowed to do this.
391 # allowed to do this.
389 if self.ansi_codes:
392 if self.ansi_codes:
390 self._ansi_processor.reset_sgr()
393 self._ansi_processor.reset_sgr()
391
394
392 content = msg['content']
395 content = msg['content']
393 status = content['status']
396 status = content['status']
394 if status == 'ok':
397 if status == 'ok':
395 self._process_execute_ok(msg)
398 self._process_execute_ok(msg)
396 elif status == 'error':
399 elif status == 'error':
397 self._process_execute_error(msg)
400 self._process_execute_error(msg)
398 elif status == 'aborted':
401 elif status == 'aborted':
399 self._process_execute_abort(msg)
402 self._process_execute_abort(msg)
400
403
401 self._show_interpreter_prompt_for_reply(msg)
404 self._show_interpreter_prompt_for_reply(msg)
402 self.executed.emit(msg)
405 self.executed.emit(msg)
403 elif info and info.id == msg['parent_header']['msg_id'] and \
406 info_list.pop(msg_id)
404 info.kind == 'silent_exec_callback' and not self._hidden:
407 elif info and info.kind == 'silent_exec_callback' and not self._hidden:
405 self._handle_exec_callback(msg)
408 self._handle_exec_callback(msg)
406 else:
409 else:
407 super(FrontendWidget, self)._handle_execute_reply(msg)
410 super(FrontendWidget, self)._handle_execute_reply(msg)
408
411
409 def _handle_input_request(self, msg):
412 def _handle_input_request(self, msg):
410 """ Handle requests for raw_input.
413 """ Handle requests for raw_input.
411 """
414 """
412 self.log.debug("input: %s", msg.get('content', ''))
415 self.log.debug("input: %s", msg.get('content', ''))
413 if self._hidden:
416 if self._hidden:
414 raise RuntimeError('Request for raw input during hidden execution.')
417 raise RuntimeError('Request for raw input during hidden execution.')
415
418
416 # Make sure that all output from the SUB channel has been processed
419 # Make sure that all output from the SUB channel has been processed
417 # before entering readline mode.
420 # before entering readline mode.
418 self.kernel_manager.sub_channel.flush()
421 self.kernel_manager.sub_channel.flush()
419
422
420 def callback(line):
423 def callback(line):
421 self.kernel_manager.stdin_channel.input(line)
424 self.kernel_manager.stdin_channel.input(line)
422 if self._reading:
425 if self._reading:
423 self.log.debug("Got second input request, assuming first was interrupted.")
426 self.log.debug("Got second input request, assuming first was interrupted.")
424 self._reading = False
427 self._reading = False
425 self._readline(msg['content']['prompt'], callback=callback)
428 self._readline(msg['content']['prompt'], callback=callback)
426
429
427 def _handle_kernel_died(self, since_last_heartbeat):
430 def _handle_kernel_died(self, since_last_heartbeat):
428 """ Handle the kernel's death by asking if the user wants to restart.
431 """ Handle the kernel's death by asking if the user wants to restart.
429 """
432 """
430 self.log.debug("kernel died: %s", since_last_heartbeat)
433 self.log.debug("kernel died: %s", since_last_heartbeat)
431 if self.custom_restart:
434 if self.custom_restart:
432 self.custom_restart_kernel_died.emit(since_last_heartbeat)
435 self.custom_restart_kernel_died.emit(since_last_heartbeat)
433 else:
436 else:
434 message = 'The kernel heartbeat has been inactive for %.2f ' \
437 message = 'The kernel heartbeat has been inactive for %.2f ' \
435 'seconds. Do you want to restart the kernel? You may ' \
438 'seconds. Do you want to restart the kernel? You may ' \
436 'first want to check the network connection.' % \
439 'first want to check the network connection.' % \
437 since_last_heartbeat
440 since_last_heartbeat
438 self.restart_kernel(message, now=True)
441 self.restart_kernel(message, now=True)
439
442
440 def _handle_object_info_reply(self, rep):
443 def _handle_object_info_reply(self, rep):
441 """ Handle replies for call tips.
444 """ Handle replies for call tips.
442 """
445 """
443 self.log.debug("oinfo: %s", rep.get('content', ''))
446 self.log.debug("oinfo: %s", rep.get('content', ''))
444 cursor = self._get_cursor()
447 cursor = self._get_cursor()
445 info = self._request_info.get('call_tip')
448 info = self._request_info.get('call_tip')
446 if info and info.id == rep['parent_header']['msg_id'] and \
449 if info and info.id == rep['parent_header']['msg_id'] and \
447 info.pos == cursor.position():
450 info.pos == cursor.position():
448 # Get the information for a call tip. For now we format the call
451 # Get the information for a call tip. For now we format the call
449 # line as string, later we can pass False to format_call and
452 # line as string, later we can pass False to format_call and
450 # syntax-highlight it ourselves for nicer formatting in the
453 # syntax-highlight it ourselves for nicer formatting in the
451 # calltip.
454 # calltip.
452 content = rep['content']
455 content = rep['content']
453 # if this is from pykernel, 'docstring' will be the only key
456 # if this is from pykernel, 'docstring' will be the only key
454 if content.get('ismagic', False):
457 if content.get('ismagic', False):
455 # Don't generate a call-tip for magics. Ideally, we should
458 # Don't generate a call-tip for magics. Ideally, we should
456 # generate a tooltip, but not on ( like we do for actual
459 # generate a tooltip, but not on ( like we do for actual
457 # callables.
460 # callables.
458 call_info, doc = None, None
461 call_info, doc = None, None
459 else:
462 else:
460 call_info, doc = call_tip(content, format_call=True)
463 call_info, doc = call_tip(content, format_call=True)
461 if call_info or doc:
464 if call_info or doc:
462 self._call_tip_widget.show_call_info(call_info, doc)
465 self._call_tip_widget.show_call_info(call_info, doc)
463
466
464 def _handle_pyout(self, msg):
467 def _handle_pyout(self, msg):
465 """ Handle display hook output.
468 """ Handle display hook output.
466 """
469 """
467 self.log.debug("pyout: %s", msg.get('content', ''))
470 self.log.debug("pyout: %s", msg.get('content', ''))
468 if not self._hidden and self._is_from_this_session(msg):
471 if not self._hidden and self._is_from_this_session(msg):
469 text = msg['content']['data']
472 text = msg['content']['data']
470 self._append_plain_text(text + '\n', before_prompt=True)
473 self._append_plain_text(text + '\n', before_prompt=True)
471
474
472 def _handle_stream(self, msg):
475 def _handle_stream(self, msg):
473 """ Handle stdout, stderr, and stdin.
476 """ Handle stdout, stderr, and stdin.
474 """
477 """
475 self.log.debug("stream: %s", msg.get('content', ''))
478 self.log.debug("stream: %s", msg.get('content', ''))
476 if not self._hidden and self._is_from_this_session(msg):
479 if not self._hidden and self._is_from_this_session(msg):
477 # Most consoles treat tabs as being 8 space characters. Convert tabs
480 # Most consoles treat tabs as being 8 space characters. Convert tabs
478 # to spaces so that output looks as expected regardless of this
481 # to spaces so that output looks as expected regardless of this
479 # widget's tab width.
482 # widget's tab width.
480 text = msg['content']['data'].expandtabs(8)
483 text = msg['content']['data'].expandtabs(8)
481
484
482 self._append_plain_text(text, before_prompt=True)
485 self._append_plain_text(text, before_prompt=True)
483 self._control.moveCursor(QtGui.QTextCursor.End)
486 self._control.moveCursor(QtGui.QTextCursor.End)
484
487
485 def _handle_shutdown_reply(self, msg):
488 def _handle_shutdown_reply(self, msg):
486 """ Handle shutdown signal, only if from other console.
489 """ Handle shutdown signal, only if from other console.
487 """
490 """
488 self.log.debug("shutdown: %s", msg.get('content', ''))
491 self.log.debug("shutdown: %s", msg.get('content', ''))
489 if not self._hidden and not self._is_from_this_session(msg):
492 if not self._hidden and not self._is_from_this_session(msg):
490 if self._local_kernel:
493 if self._local_kernel:
491 if not msg['content']['restart']:
494 if not msg['content']['restart']:
492 self.exit_requested.emit(self)
495 self.exit_requested.emit(self)
493 else:
496 else:
494 # we just got notified of a restart!
497 # we just got notified of a restart!
495 time.sleep(0.25) # wait 1/4 sec to reset
498 time.sleep(0.25) # wait 1/4 sec to reset
496 # lest the request for a new prompt
499 # lest the request for a new prompt
497 # goes to the old kernel
500 # goes to the old kernel
498 self.reset()
501 self.reset()
499 else: # remote kernel, prompt on Kernel shutdown/reset
502 else: # remote kernel, prompt on Kernel shutdown/reset
500 title = self.window().windowTitle()
503 title = self.window().windowTitle()
501 if not msg['content']['restart']:
504 if not msg['content']['restart']:
502 reply = QtGui.QMessageBox.question(self, title,
505 reply = QtGui.QMessageBox.question(self, title,
503 "Kernel has been shutdown permanently. "
506 "Kernel has been shutdown permanently. "
504 "Close the Console?",
507 "Close the Console?",
505 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
508 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
506 if reply == QtGui.QMessageBox.Yes:
509 if reply == QtGui.QMessageBox.Yes:
507 self.exit_requested.emit(self)
510 self.exit_requested.emit(self)
508 else:
511 else:
509 reply = QtGui.QMessageBox.question(self, title,
512 reply = QtGui.QMessageBox.question(self, title,
510 "Kernel has been reset. Clear the Console?",
513 "Kernel has been reset. Clear the Console?",
511 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
514 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
512 if reply == QtGui.QMessageBox.Yes:
515 if reply == QtGui.QMessageBox.Yes:
513 time.sleep(0.25) # wait 1/4 sec to reset
516 time.sleep(0.25) # wait 1/4 sec to reset
514 # lest the request for a new prompt
517 # lest the request for a new prompt
515 # goes to the old kernel
518 # goes to the old kernel
516 self.reset()
519 self.reset()
517
520
518 def _started_channels(self):
521 def _started_channels(self):
519 """ Called when the KernelManager channels have started listening or
522 """ Called when the KernelManager channels have started listening or
520 when the frontend is assigned an already listening KernelManager.
523 when the frontend is assigned an already listening KernelManager.
521 """
524 """
522 self.reset()
525 self.reset()
523
526
524 #---------------------------------------------------------------------------
527 #---------------------------------------------------------------------------
525 # 'FrontendWidget' public interface
528 # 'FrontendWidget' public interface
526 #---------------------------------------------------------------------------
529 #---------------------------------------------------------------------------
527
530
528 def copy_raw(self):
531 def copy_raw(self):
529 """ Copy the currently selected text to the clipboard without attempting
532 """ Copy the currently selected text to the clipboard without attempting
530 to remove prompts or otherwise alter the text.
533 to remove prompts or otherwise alter the text.
531 """
534 """
532 self._control.copy()
535 self._control.copy()
533
536
534 def execute_file(self, path, hidden=False):
537 def execute_file(self, path, hidden=False):
535 """ Attempts to execute file with 'path'. If 'hidden', no output is
538 """ Attempts to execute file with 'path'. If 'hidden', no output is
536 shown.
539 shown.
537 """
540 """
538 self.execute('execfile(%r)' % path, hidden=hidden)
541 self.execute('execfile(%r)' % path, hidden=hidden)
539
542
540 def interrupt_kernel(self):
543 def interrupt_kernel(self):
541 """ Attempts to interrupt the running kernel.
544 """ Attempts to interrupt the running kernel.
542
545
543 Also unsets _reading flag, to avoid runtime errors
546 Also unsets _reading flag, to avoid runtime errors
544 if raw_input is called again.
547 if raw_input is called again.
545 """
548 """
546 if self.custom_interrupt:
549 if self.custom_interrupt:
547 self._reading = False
550 self._reading = False
548 self.custom_interrupt_requested.emit()
551 self.custom_interrupt_requested.emit()
549 elif self.kernel_manager.has_kernel:
552 elif self.kernel_manager.has_kernel:
550 self._reading = False
553 self._reading = False
551 self.kernel_manager.interrupt_kernel()
554 self.kernel_manager.interrupt_kernel()
552 else:
555 else:
553 self._append_plain_text('Kernel process is either remote or '
556 self._append_plain_text('Kernel process is either remote or '
554 'unspecified. Cannot interrupt.\n')
557 'unspecified. Cannot interrupt.\n')
555
558
556 def reset(self):
559 def reset(self):
557 """ Resets the widget to its initial state. Similar to ``clear``, but
560 """ Resets the widget to its initial state. Similar to ``clear``, but
558 also re-writes the banner and aborts execution if necessary.
561 also re-writes the banner and aborts execution if necessary.
559 """
562 """
560 if self._executing:
563 if self._executing:
561 self._executing = False
564 self._executing = False
562 self._request_info['execute'] = None
565 self._request_info['execute'] = {}
563 self._reading = False
566 self._reading = False
564 self._highlighter.highlighting_on = False
567 self._highlighter.highlighting_on = False
565
568
566 self._control.clear()
569 self._control.clear()
567 self._append_plain_text(self.banner)
570 self._append_plain_text(self.banner)
568 # update output marker for stdout/stderr, so that startup
571 # update output marker for stdout/stderr, so that startup
569 # messages appear after banner:
572 # messages appear after banner:
570 self._append_before_prompt_pos = self._get_cursor().position()
573 self._append_before_prompt_pos = self._get_cursor().position()
571 self._show_interpreter_prompt()
574 self._show_interpreter_prompt()
572
575
573 def restart_kernel(self, message, now=False):
576 def restart_kernel(self, message, now=False):
574 """ Attempts to restart the running kernel.
577 """ Attempts to restart the running kernel.
575 """
578 """
576 # FIXME: now should be configurable via a checkbox in the dialog. Right
579 # FIXME: now should be configurable via a checkbox in the dialog. Right
577 # now at least the heartbeat path sets it to True and the manual restart
580 # now at least the heartbeat path sets it to True and the manual restart
578 # to False. But those should just be the pre-selected states of a
581 # to False. But those should just be the pre-selected states of a
579 # checkbox that the user could override if so desired. But I don't know
582 # checkbox that the user could override if so desired. But I don't know
580 # enough Qt to go implementing the checkbox now.
583 # enough Qt to go implementing the checkbox now.
581
584
582 if self.custom_restart:
585 if self.custom_restart:
583 self.custom_restart_requested.emit()
586 self.custom_restart_requested.emit()
584
587
585 elif self.kernel_manager.has_kernel:
588 elif self.kernel_manager.has_kernel:
586 # Pause the heart beat channel to prevent further warnings.
589 # Pause the heart beat channel to prevent further warnings.
587 self.kernel_manager.hb_channel.pause()
590 self.kernel_manager.hb_channel.pause()
588
591
589 # Prompt the user to restart the kernel. Un-pause the heartbeat if
592 # Prompt the user to restart the kernel. Un-pause the heartbeat if
590 # they decline. (If they accept, the heartbeat will be un-paused
593 # they decline. (If they accept, the heartbeat will be un-paused
591 # automatically when the kernel is restarted.)
594 # automatically when the kernel is restarted.)
592 buttons = QtGui.QMessageBox.Yes | QtGui.QMessageBox.No
595 buttons = QtGui.QMessageBox.Yes | QtGui.QMessageBox.No
593 result = QtGui.QMessageBox.question(self, 'Restart kernel?',
596 result = QtGui.QMessageBox.question(self, 'Restart kernel?',
594 message, buttons)
597 message, buttons)
595 if result == QtGui.QMessageBox.Yes:
598 if result == QtGui.QMessageBox.Yes:
596 try:
599 try:
597 self.kernel_manager.restart_kernel(now=now)
600 self.kernel_manager.restart_kernel(now=now)
598 except RuntimeError:
601 except RuntimeError:
599 self._append_plain_text('Kernel started externally. '
602 self._append_plain_text('Kernel started externally. '
600 'Cannot restart.\n')
603 'Cannot restart.\n')
601 else:
604 else:
602 self.reset()
605 self.reset()
603 else:
606 else:
604 self.kernel_manager.hb_channel.unpause()
607 self.kernel_manager.hb_channel.unpause()
605
608
606 else:
609 else:
607 self._append_plain_text('Kernel process is either remote or '
610 self._append_plain_text('Kernel process is either remote or '
608 'unspecified. Cannot restart.\n')
611 'unspecified. Cannot restart.\n')
609
612
610 #---------------------------------------------------------------------------
613 #---------------------------------------------------------------------------
611 # 'FrontendWidget' protected interface
614 # 'FrontendWidget' protected interface
612 #---------------------------------------------------------------------------
615 #---------------------------------------------------------------------------
613
616
614 def _call_tip(self):
617 def _call_tip(self):
615 """ Shows a call tip, if appropriate, at the current cursor location.
618 """ Shows a call tip, if appropriate, at the current cursor location.
616 """
619 """
617 # Decide if it makes sense to show a call tip
620 # Decide if it makes sense to show a call tip
618 if not self.enable_calltips:
621 if not self.enable_calltips:
619 return False
622 return False
620 cursor = self._get_cursor()
623 cursor = self._get_cursor()
621 cursor.movePosition(QtGui.QTextCursor.Left)
624 cursor.movePosition(QtGui.QTextCursor.Left)
622 if cursor.document().characterAt(cursor.position()) != '(':
625 if cursor.document().characterAt(cursor.position()) != '(':
623 return False
626 return False
624 context = self._get_context(cursor)
627 context = self._get_context(cursor)
625 if not context:
628 if not context:
626 return False
629 return False
627
630
628 # Send the metadata request to the kernel
631 # Send the metadata request to the kernel
629 name = '.'.join(context)
632 name = '.'.join(context)
630 msg_id = self.kernel_manager.shell_channel.object_info(name)
633 msg_id = self.kernel_manager.shell_channel.object_info(name)
631 pos = self._get_cursor().position()
634 pos = self._get_cursor().position()
632 self._request_info['call_tip'] = self._CallTipRequest(msg_id, pos)
635 self._request_info['call_tip'] = self._CallTipRequest(msg_id, pos)
633 return True
636 return True
634
637
635 def _complete(self):
638 def _complete(self):
636 """ Performs completion at the current cursor location.
639 """ Performs completion at the current cursor location.
637 """
640 """
638 context = self._get_context()
641 context = self._get_context()
639 if context:
642 if context:
640 # Send the completion request to the kernel
643 # Send the completion request to the kernel
641 msg_id = self.kernel_manager.shell_channel.complete(
644 msg_id = self.kernel_manager.shell_channel.complete(
642 '.'.join(context), # text
645 '.'.join(context), # text
643 self._get_input_buffer_cursor_line(), # line
646 self._get_input_buffer_cursor_line(), # line
644 self._get_input_buffer_cursor_column(), # cursor_pos
647 self._get_input_buffer_cursor_column(), # cursor_pos
645 self.input_buffer) # block
648 self.input_buffer) # block
646 pos = self._get_cursor().position()
649 pos = self._get_cursor().position()
647 info = self._CompletionRequest(msg_id, pos)
650 info = self._CompletionRequest(msg_id, pos)
648 self._request_info['complete'] = info
651 self._request_info['complete'] = info
649
652
650 def _get_context(self, cursor=None):
653 def _get_context(self, cursor=None):
651 """ Gets the context for the specified cursor (or the current cursor
654 """ Gets the context for the specified cursor (or the current cursor
652 if none is specified).
655 if none is specified).
653 """
656 """
654 if cursor is None:
657 if cursor is None:
655 cursor = self._get_cursor()
658 cursor = self._get_cursor()
656 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
659 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
657 QtGui.QTextCursor.KeepAnchor)
660 QtGui.QTextCursor.KeepAnchor)
658 text = cursor.selection().toPlainText()
661 text = cursor.selection().toPlainText()
659 return self._completion_lexer.get_context(text)
662 return self._completion_lexer.get_context(text)
660
663
661 def _process_execute_abort(self, msg):
664 def _process_execute_abort(self, msg):
662 """ Process a reply for an aborted execution request.
665 """ Process a reply for an aborted execution request.
663 """
666 """
664 self._append_plain_text("ERROR: execution aborted\n")
667 self._append_plain_text("ERROR: execution aborted\n")
665
668
666 def _process_execute_error(self, msg):
669 def _process_execute_error(self, msg):
667 """ Process a reply for an execution request that resulted in an error.
670 """ Process a reply for an execution request that resulted in an error.
668 """
671 """
669 content = msg['content']
672 content = msg['content']
670 # If a SystemExit is passed along, this means exit() was called - also
673 # If a SystemExit is passed along, this means exit() was called - also
671 # all the ipython %exit magic syntax of '-k' to be used to keep
674 # all the ipython %exit magic syntax of '-k' to be used to keep
672 # the kernel running
675 # the kernel running
673 if content['ename']=='SystemExit':
676 if content['ename']=='SystemExit':
674 keepkernel = content['evalue']=='-k' or content['evalue']=='True'
677 keepkernel = content['evalue']=='-k' or content['evalue']=='True'
675 self._keep_kernel_on_exit = keepkernel
678 self._keep_kernel_on_exit = keepkernel
676 self.exit_requested.emit(self)
679 self.exit_requested.emit(self)
677 else:
680 else:
678 traceback = ''.join(content['traceback'])
681 traceback = ''.join(content['traceback'])
679 self._append_plain_text(traceback)
682 self._append_plain_text(traceback)
680
683
681 def _process_execute_ok(self, msg):
684 def _process_execute_ok(self, msg):
682 """ Process a reply for a successful execution equest.
685 """ Process a reply for a successful execution equest.
683 """
686 """
684 payload = msg['content']['payload']
687 payload = msg['content']['payload']
685 for item in payload:
688 for item in payload:
686 if not self._process_execute_payload(item):
689 if not self._process_execute_payload(item):
687 warning = 'Warning: received unknown payload of type %s'
690 warning = 'Warning: received unknown payload of type %s'
688 print(warning % repr(item['source']))
691 print(warning % repr(item['source']))
689
692
690 def _process_execute_payload(self, item):
693 def _process_execute_payload(self, item):
691 """ Process a single payload item from the list of payload items in an
694 """ Process a single payload item from the list of payload items in an
692 execution reply. Returns whether the payload was handled.
695 execution reply. Returns whether the payload was handled.
693 """
696 """
694 # The basic FrontendWidget doesn't handle payloads, as they are a
697 # The basic FrontendWidget doesn't handle payloads, as they are a
695 # mechanism for going beyond the standard Python interpreter model.
698 # mechanism for going beyond the standard Python interpreter model.
696 return False
699 return False
697
700
698 def _show_interpreter_prompt(self):
701 def _show_interpreter_prompt(self):
699 """ Shows a prompt for the interpreter.
702 """ Shows a prompt for the interpreter.
700 """
703 """
701 self._show_prompt('>>> ')
704 self._show_prompt('>>> ')
702
705
703 def _show_interpreter_prompt_for_reply(self, msg):
706 def _show_interpreter_prompt_for_reply(self, msg):
704 """ Shows a prompt for the interpreter given an 'execute_reply' message.
707 """ Shows a prompt for the interpreter given an 'execute_reply' message.
705 """
708 """
706 self._show_interpreter_prompt()
709 self._show_interpreter_prompt()
707
710
708 #------ Signal handlers ----------------------------------------------------
711 #------ Signal handlers ----------------------------------------------------
709
712
710 def _document_contents_change(self, position, removed, added):
713 def _document_contents_change(self, position, removed, added):
711 """ Called whenever the document's content changes. Display a call tip
714 """ Called whenever the document's content changes. Display a call tip
712 if appropriate.
715 if appropriate.
713 """
716 """
714 # Calculate where the cursor should be *after* the change:
717 # Calculate where the cursor should be *after* the change:
715 position += added
718 position += added
716
719
717 document = self._control.document()
720 document = self._control.document()
718 if position == self._get_cursor().position():
721 if position == self._get_cursor().position():
719 self._call_tip()
722 self._call_tip()
720
723
721 #------ Trait default initializers -----------------------------------------
724 #------ Trait default initializers -----------------------------------------
722
725
723 def _banner_default(self):
726 def _banner_default(self):
724 """ Returns the standard Python banner.
727 """ Returns the standard Python banner.
725 """
728 """
726 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
729 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
727 '"license" for more information.'
730 '"license" for more information.'
728 return banner % (sys.version, sys.platform)
731 return banner % (sys.version, sys.platform)
@@ -1,283 +1,285 b''
1 # System library imports
1 # System library imports
2 from IPython.external.qt import QtGui
2 from IPython.external.qt import QtGui
3
3
4 # Local imports
4 # Local imports
5 from IPython.utils.traitlets import Bool
5 from IPython.utils.traitlets import Bool
6 from console_widget import ConsoleWidget
6 from console_widget import ConsoleWidget
7
7
8
8
9 class HistoryConsoleWidget(ConsoleWidget):
9 class HistoryConsoleWidget(ConsoleWidget):
10 """ A ConsoleWidget that keeps a history of the commands that have been
10 """ A ConsoleWidget that keeps a history of the commands that have been
11 executed and provides a readline-esque interface to this history.
11 executed and provides a readline-esque interface to this history.
12 """
12 """
13
13
14 #------ Configuration ------------------------------------------------------
14 #------ Configuration ------------------------------------------------------
15
15
16 # If enabled, the input buffer will become "locked" to history movement when
16 # If enabled, the input buffer will become "locked" to history movement when
17 # an edit is made to a multi-line input buffer. To override the lock, use
17 # an edit is made to a multi-line input buffer. To override the lock, use
18 # Shift in conjunction with the standard history cycling keys.
18 # Shift in conjunction with the standard history cycling keys.
19 history_lock = Bool(False, config=True)
19 history_lock = Bool(False, config=True)
20
20
21 #---------------------------------------------------------------------------
21 #---------------------------------------------------------------------------
22 # 'object' interface
22 # 'object' interface
23 #---------------------------------------------------------------------------
23 #---------------------------------------------------------------------------
24
24
25 def __init__(self, *args, **kw):
25 def __init__(self, *args, **kw):
26 super(HistoryConsoleWidget, self).__init__(*args, **kw)
26 super(HistoryConsoleWidget, self).__init__(*args, **kw)
27
27
28 # HistoryConsoleWidget protected variables.
28 # HistoryConsoleWidget protected variables.
29 self._history = []
29 self._history = []
30 self._history_edits = {}
30 self._history_edits = {}
31 self._history_index = 0
31 self._history_index = 0
32 self._history_prefix = ''
32 self._history_prefix = ''
33
33
34 #---------------------------------------------------------------------------
34 #---------------------------------------------------------------------------
35 # 'ConsoleWidget' public interface
35 # 'ConsoleWidget' public interface
36 #---------------------------------------------------------------------------
36 #---------------------------------------------------------------------------
37
37
38 def execute(self, source=None, hidden=False, interactive=False):
38 def execute(self, source=None, hidden=False, interactive=False):
39 """ Reimplemented to the store history.
39 """ Reimplemented to the store history.
40 """
40 """
41 if not hidden:
41 if not hidden:
42 history = self.input_buffer if source is None else source
42 history = self.input_buffer if source is None else source
43
43
44 executed = super(HistoryConsoleWidget, self).execute(
44 executed = super(HistoryConsoleWidget, self).execute(
45 source, hidden, interactive)
45 source, hidden, interactive)
46
46
47 if executed and not hidden:
47 if executed and not hidden:
48 # Save the command unless it was an empty string or was identical
48 # Save the command unless it was an empty string or was identical
49 # to the previous command.
49 # to the previous command.
50 history = history.rstrip()
50 history = history.rstrip()
51 if history and (not self._history or self._history[-1] != history):
51 if history and (not self._history or self._history[-1] != history):
52 self._history.append(history)
52 self._history.append(history)
53
53
54 # Emulate readline: reset all history edits.
54 # Emulate readline: reset all history edits.
55 self._history_edits = {}
55 self._history_edits = {}
56
56
57 # Move the history index to the most recent item.
57 # Move the history index to the most recent item.
58 self._history_index = len(self._history)
58 self._history_index = len(self._history)
59
59
60 return executed
60 return executed
61
61
62 #---------------------------------------------------------------------------
62 #---------------------------------------------------------------------------
63 # 'ConsoleWidget' abstract interface
63 # 'ConsoleWidget' abstract interface
64 #---------------------------------------------------------------------------
64 #---------------------------------------------------------------------------
65
65
66 def _up_pressed(self, shift_modifier):
66 def _up_pressed(self, shift_modifier):
67 """ Called when the up key is pressed. Returns whether to continue
67 """ Called when the up key is pressed. Returns whether to continue
68 processing the event.
68 processing the event.
69 """
69 """
70 prompt_cursor = self._get_prompt_cursor()
70 prompt_cursor = self._get_prompt_cursor()
71 if self._get_cursor().blockNumber() == prompt_cursor.blockNumber():
71 if self._get_cursor().blockNumber() == prompt_cursor.blockNumber():
72 # Bail out if we're locked.
72 # Bail out if we're locked.
73 if self._history_locked() and not shift_modifier:
73 if self._history_locked() and not shift_modifier:
74 return False
74 return False
75
75
76 # Set a search prefix based on the cursor position.
76 # Set a search prefix based on the cursor position.
77 col = self._get_input_buffer_cursor_column()
77 col = self._get_input_buffer_cursor_column()
78 input_buffer = self.input_buffer
78 input_buffer = self.input_buffer
79 if self._history_index == len(self._history) or \
79 if self._history_index == len(self._history) or \
80 (self._history_prefix and col != len(self._history_prefix)):
80 (self._history_prefix and col != len(self._history_prefix)):
81 self._history_index = len(self._history)
81 self._history_index = len(self._history)
82 self._history_prefix = input_buffer[:col]
82 self._history_prefix = input_buffer[:col]
83
83
84 # Perform the search.
84 # Perform the search.
85 self.history_previous(self._history_prefix,
85 self.history_previous(self._history_prefix,
86 as_prefix=not shift_modifier)
86 as_prefix=not shift_modifier)
87
87
88 # Go to the first line of the prompt for seemless history scrolling.
88 # Go to the first line of the prompt for seemless history scrolling.
89 # Emulate readline: keep the cursor position fixed for a prefix
89 # Emulate readline: keep the cursor position fixed for a prefix
90 # search.
90 # search.
91 cursor = self._get_prompt_cursor()
91 cursor = self._get_prompt_cursor()
92 if self._history_prefix:
92 if self._history_prefix:
93 cursor.movePosition(QtGui.QTextCursor.Right,
93 cursor.movePosition(QtGui.QTextCursor.Right,
94 n=len(self._history_prefix))
94 n=len(self._history_prefix))
95 else:
95 else:
96 cursor.movePosition(QtGui.QTextCursor.EndOfLine)
96 cursor.movePosition(QtGui.QTextCursor.EndOfLine)
97 self._set_cursor(cursor)
97 self._set_cursor(cursor)
98
98
99 return False
99 return False
100
100
101 return True
101 return True
102
102
103 def _down_pressed(self, shift_modifier):
103 def _down_pressed(self, shift_modifier):
104 """ Called when the down key is pressed. Returns whether to continue
104 """ Called when the down key is pressed. Returns whether to continue
105 processing the event.
105 processing the event.
106 """
106 """
107 end_cursor = self._get_end_cursor()
107 end_cursor = self._get_end_cursor()
108 if self._get_cursor().blockNumber() == end_cursor.blockNumber():
108 if self._get_cursor().blockNumber() == end_cursor.blockNumber():
109 # Bail out if we're locked.
109 # Bail out if we're locked.
110 if self._history_locked() and not shift_modifier:
110 if self._history_locked() and not shift_modifier:
111 return False
111 return False
112
112
113 # Perform the search.
113 # Perform the search.
114 replaced = self.history_next(self._history_prefix,
114 replaced = self.history_next(self._history_prefix,
115 as_prefix=not shift_modifier)
115 as_prefix=not shift_modifier)
116
116
117 # Emulate readline: keep the cursor position fixed for a prefix
117 # Emulate readline: keep the cursor position fixed for a prefix
118 # search. (We don't need to move the cursor to the end of the buffer
118 # search. (We don't need to move the cursor to the end of the buffer
119 # in the other case because this happens automatically when the
119 # in the other case because this happens automatically when the
120 # input buffer is set.)
120 # input buffer is set.)
121 if self._history_prefix and replaced:
121 if self._history_prefix and replaced:
122 cursor = self._get_prompt_cursor()
122 cursor = self._get_prompt_cursor()
123 cursor.movePosition(QtGui.QTextCursor.Right,
123 cursor.movePosition(QtGui.QTextCursor.Right,
124 n=len(self._history_prefix))
124 n=len(self._history_prefix))
125 self._set_cursor(cursor)
125 self._set_cursor(cursor)
126
126
127 return False
127 return False
128
128
129 return True
129 return True
130
130
131 #---------------------------------------------------------------------------
131 #---------------------------------------------------------------------------
132 # 'HistoryConsoleWidget' public interface
132 # 'HistoryConsoleWidget' public interface
133 #---------------------------------------------------------------------------
133 #---------------------------------------------------------------------------
134
134
135 def history_previous(self, substring='', as_prefix=True):
135 def history_previous(self, substring='', as_prefix=True):
136 """ If possible, set the input buffer to a previous history item.
136 """ If possible, set the input buffer to a previous history item.
137
137
138 Parameters:
138 Parameters:
139 -----------
139 -----------
140 substring : str, optional
140 substring : str, optional
141 If specified, search for an item with this substring.
141 If specified, search for an item with this substring.
142 as_prefix : bool, optional
142 as_prefix : bool, optional
143 If True, the substring must match at the beginning (default).
143 If True, the substring must match at the beginning (default).
144
144
145 Returns:
145 Returns:
146 --------
146 --------
147 Whether the input buffer was changed.
147 Whether the input buffer was changed.
148 """
148 """
149 index = self._history_index
149 index = self._history_index
150 replace = False
150 replace = False
151 while index > 0:
151 while index > 0:
152 index -= 1
152 index -= 1
153 history = self._get_edited_history(index)
153 history = self._get_edited_history(index)
154 if (as_prefix and history.startswith(substring)) \
154 if (as_prefix and history.startswith(substring)) \
155 or (not as_prefix and substring in history):
155 or (not as_prefix and substring in history):
156 replace = True
156 replace = True
157 break
157 break
158
158
159 if replace:
159 if replace:
160 self._store_edits()
160 self._store_edits()
161 self._history_index = index
161 self._history_index = index
162 self.input_buffer = history
162 self.input_buffer = history
163
163
164 return replace
164 return replace
165
165
166 def history_next(self, substring='', as_prefix=True):
166 def history_next(self, substring='', as_prefix=True):
167 """ If possible, set the input buffer to a subsequent history item.
167 """ If possible, set the input buffer to a subsequent history item.
168
168
169 Parameters:
169 Parameters:
170 -----------
170 -----------
171 substring : str, optional
171 substring : str, optional
172 If specified, search for an item with this substring.
172 If specified, search for an item with this substring.
173 as_prefix : bool, optional
173 as_prefix : bool, optional
174 If True, the substring must match at the beginning (default).
174 If True, the substring must match at the beginning (default).
175
175
176 Returns:
176 Returns:
177 --------
177 --------
178 Whether the input buffer was changed.
178 Whether the input buffer was changed.
179 """
179 """
180 index = self._history_index
180 index = self._history_index
181 replace = False
181 replace = False
182 while self._history_index < len(self._history):
182 while self._history_index < len(self._history):
183 index += 1
183 index += 1
184 history = self._get_edited_history(index)
184 history = self._get_edited_history(index)
185 if (as_prefix and history.startswith(substring)) \
185 if (as_prefix and history.startswith(substring)) \
186 or (not as_prefix and substring in history):
186 or (not as_prefix and substring in history):
187 replace = True
187 replace = True
188 break
188 break
189
189
190 if replace:
190 if replace:
191 self._store_edits()
191 self._store_edits()
192 self._history_index = index
192 self._history_index = index
193 self.input_buffer = history
193 self.input_buffer = history
194
194
195 return replace
195 return replace
196
196
197 def history_tail(self, n=10):
197 def history_tail(self, n=10):
198 """ Get the local history list.
198 """ Get the local history list.
199
199
200 Parameters:
200 Parameters:
201 -----------
201 -----------
202 n : int
202 n : int
203 The (maximum) number of history items to get.
203 The (maximum) number of history items to get.
204 """
204 """
205 return self._history[-n:]
205 return self._history[-n:]
206
206
207 def _request_update_session_history_length(self):
207 def _request_update_session_history_length(self):
208 msg_id = self.kernel_manager.shell_channel.execute('',
208 msg_id = self.kernel_manager.shell_channel.execute('',
209 silent=True,
209 silent=True,
210 user_expressions={
210 user_expressions={
211 'hlen':'len(get_ipython().history_manager.input_hist_raw)',
211 'hlen':'len(get_ipython().history_manager.input_hist_raw)',
212 }
212 }
213 )
213 )
214 self._request_info['execute'] = self._ExecutionRequest(msg_id, 'save_magic')
214 self._request_info['execute'][msg_id] = self._ExecutionRequest(msg_id, 'save_magic')
215
215
216 def _handle_execute_reply(self, msg):
216 def _handle_execute_reply(self, msg):
217 """ Handles replies for code execution, here only session history length
217 """ Handles replies for code execution, here only session history length
218 """
218 """
219 info = self._request_info.get('execute')
219 info_list = self._request_info.get('execute')
220 if info and info.id == msg['parent_header']['msg_id'] and \
220 msg_id = msg['parent_header']['msg_id']
221 info.kind == 'save_magic' and not self._hidden:
221 if msg_id in info_list:
222 content = msg['content']
222 info = info_list.pop(msg_id)
223 status = content['status']
223 if info.kind == 'save_magic' and not self._hidden:
224 if status == 'ok':
224 content = msg['content']
225 self._max_session_history=(int(content['user_expressions']['hlen']))
225 status = content['status']
226 if status == 'ok':
227 self._max_session_history=(int(content['user_expressions']['hlen']))
226
228
227 def save_magic(self):
229 def save_magic(self):
228 # update the session history length
230 # update the session history length
229 self._request_update_session_history_length()
231 self._request_update_session_history_length()
230
232
231 file_name,extFilter = QtGui.QFileDialog.getSaveFileName(self,
233 file_name,extFilter = QtGui.QFileDialog.getSaveFileName(self,
232 "Enter A filename",
234 "Enter A filename",
233 filter='Python File (*.py);; All files (*.*)'
235 filter='Python File (*.py);; All files (*.*)'
234 )
236 )
235
237
236 # let's the user search/type for a file name, while the history length
238 # let's the user search/type for a file name, while the history length
237 # is fetched
239 # is fetched
238
240
239 if file_name:
241 if file_name:
240 hist_range, ok = QtGui.QInputDialog.getText(self,
242 hist_range, ok = QtGui.QInputDialog.getText(self,
241 'Please enter an interval of command to save',
243 'Please enter an interval of command to save',
242 'Saving commands:',
244 'Saving commands:',
243 text=str('1-'+str(self._max_session_history))
245 text=str('1-'+str(self._max_session_history))
244 )
246 )
245 if ok:
247 if ok:
246 self.execute("%save"+" "+file_name+" "+str(hist_range))
248 self.execute("%save"+" "+file_name+" "+str(hist_range))
247
249
248 #---------------------------------------------------------------------------
250 #---------------------------------------------------------------------------
249 # 'HistoryConsoleWidget' protected interface
251 # 'HistoryConsoleWidget' protected interface
250 #---------------------------------------------------------------------------
252 #---------------------------------------------------------------------------
251
253
252 def _history_locked(self):
254 def _history_locked(self):
253 """ Returns whether history movement is locked.
255 """ Returns whether history movement is locked.
254 """
256 """
255 return (self.history_lock and
257 return (self.history_lock and
256 (self._get_edited_history(self._history_index) !=
258 (self._get_edited_history(self._history_index) !=
257 self.input_buffer) and
259 self.input_buffer) and
258 (self._get_prompt_cursor().blockNumber() !=
260 (self._get_prompt_cursor().blockNumber() !=
259 self._get_end_cursor().blockNumber()))
261 self._get_end_cursor().blockNumber()))
260
262
261 def _get_edited_history(self, index):
263 def _get_edited_history(self, index):
262 """ Retrieves a history item, possibly with temporary edits.
264 """ Retrieves a history item, possibly with temporary edits.
263 """
265 """
264 if index in self._history_edits:
266 if index in self._history_edits:
265 return self._history_edits[index]
267 return self._history_edits[index]
266 elif index == len(self._history):
268 elif index == len(self._history):
267 return unicode()
269 return unicode()
268 return self._history[index]
270 return self._history[index]
269
271
270 def _set_history(self, history):
272 def _set_history(self, history):
271 """ Replace the current history with a sequence of history items.
273 """ Replace the current history with a sequence of history items.
272 """
274 """
273 self._history = list(history)
275 self._history = list(history)
274 self._history_edits = {}
276 self._history_edits = {}
275 self._history_index = len(self._history)
277 self._history_index = len(self._history)
276
278
277 def _store_edits(self):
279 def _store_edits(self):
278 """ If there are edits to the current input buffer, store them.
280 """ If there are edits to the current input buffer, store them.
279 """
281 """
280 current = self.input_buffer
282 current = self.input_buffer
281 if self._history_index == len(self._history) or \
283 if self._history_index == len(self._history) or \
282 self._history[self._history_index] != current:
284 self._history[self._history_index] != current:
283 self._history_edits[self._history_index] = current
285 self._history_edits[self._history_index] = current
@@ -1,562 +1,565 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 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 import time
15 import time
16 from textwrap import dedent
16 from textwrap import dedent
17
17
18 # System library imports
18 # System library imports
19 from IPython.external.qt import QtCore, QtGui
19 from IPython.external.qt import QtCore, QtGui
20
20
21 # Local imports
21 # Local imports
22 from IPython.core.inputsplitter import IPythonInputSplitter, \
22 from IPython.core.inputsplitter import IPythonInputSplitter, \
23 transform_ipy_prompt
23 transform_ipy_prompt
24 from IPython.utils.traitlets import Bool, Unicode
24 from IPython.utils.traitlets import Bool, Unicode
25 from frontend_widget import FrontendWidget
25 from frontend_widget import FrontendWidget
26 import styles
26 import styles
27
27
28 #-----------------------------------------------------------------------------
28 #-----------------------------------------------------------------------------
29 # Constants
29 # Constants
30 #-----------------------------------------------------------------------------
30 #-----------------------------------------------------------------------------
31
31
32 # Default strings to build and display input and output prompts (and separators
32 # Default strings to build and display input and output prompts (and separators
33 # in between)
33 # in between)
34 default_in_prompt = 'In [<span class="in-prompt-number">%i</span>]: '
34 default_in_prompt = 'In [<span class="in-prompt-number">%i</span>]: '
35 default_out_prompt = 'Out[<span class="out-prompt-number">%i</span>]: '
35 default_out_prompt = 'Out[<span class="out-prompt-number">%i</span>]: '
36 default_input_sep = '\n'
36 default_input_sep = '\n'
37 default_output_sep = ''
37 default_output_sep = ''
38 default_output_sep2 = ''
38 default_output_sep2 = ''
39
39
40 # Base path for most payload sources.
40 # Base path for most payload sources.
41 zmq_shell_source = 'IPython.zmq.zmqshell.ZMQInteractiveShell'
41 zmq_shell_source = 'IPython.zmq.zmqshell.ZMQInteractiveShell'
42
42
43 if sys.platform.startswith('win'):
43 if sys.platform.startswith('win'):
44 default_editor = 'notepad'
44 default_editor = 'notepad'
45 else:
45 else:
46 default_editor = ''
46 default_editor = ''
47
47
48 #-----------------------------------------------------------------------------
48 #-----------------------------------------------------------------------------
49 # IPythonWidget class
49 # IPythonWidget class
50 #-----------------------------------------------------------------------------
50 #-----------------------------------------------------------------------------
51
51
52 class IPythonWidget(FrontendWidget):
52 class IPythonWidget(FrontendWidget):
53 """ A FrontendWidget for an IPython kernel.
53 """ A FrontendWidget for an IPython kernel.
54 """
54 """
55
55
56 # If set, the 'custom_edit_requested(str, int)' signal will be emitted when
56 # 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'
57 # an editor is needed for a file. This overrides 'editor' and 'editor_line'
58 # settings.
58 # settings.
59 custom_edit = Bool(False)
59 custom_edit = Bool(False)
60 custom_edit_requested = QtCore.Signal(object, object)
60 custom_edit_requested = QtCore.Signal(object, object)
61
61
62 editor = Unicode(default_editor, config=True,
62 editor = Unicode(default_editor, config=True,
63 help="""
63 help="""
64 A command for invoking a system text editor. If the string contains a
64 A command for invoking a system text editor. If the string contains a
65 {filename} format specifier, it will be used. Otherwise, the filename
65 {filename} format specifier, it will be used. Otherwise, the filename
66 will be appended to the end the command.
66 will be appended to the end the command.
67 """)
67 """)
68
68
69 editor_line = Unicode(config=True,
69 editor_line = Unicode(config=True,
70 help="""
70 help="""
71 The editor command to use when a specific line number is requested. The
71 The editor command to use when a specific line number is requested. The
72 string should contain two format specifiers: {line} and {filename}. If
72 string should contain two format specifiers: {line} and {filename}. If
73 this parameter is not specified, the line number option to the %edit
73 this parameter is not specified, the line number option to the %edit
74 magic will be ignored.
74 magic will be ignored.
75 """)
75 """)
76
76
77 style_sheet = Unicode(config=True,
77 style_sheet = Unicode(config=True,
78 help="""
78 help="""
79 A CSS stylesheet. The stylesheet can contain classes for:
79 A CSS stylesheet. The stylesheet can contain classes for:
80 1. Qt: QPlainTextEdit, QFrame, QWidget, etc
80 1. Qt: QPlainTextEdit, QFrame, QWidget, etc
81 2. Pygments: .c, .k, .o, etc. (see PygmentsHighlighter)
81 2. Pygments: .c, .k, .o, etc. (see PygmentsHighlighter)
82 3. IPython: .error, .in-prompt, .out-prompt, etc
82 3. IPython: .error, .in-prompt, .out-prompt, etc
83 """)
83 """)
84
84
85 syntax_style = Unicode(config=True,
85 syntax_style = Unicode(config=True,
86 help="""
86 help="""
87 If not empty, use this Pygments style for syntax highlighting.
87 If not empty, use this Pygments style for syntax highlighting.
88 Otherwise, the style sheet is queried for Pygments style
88 Otherwise, the style sheet is queried for Pygments style
89 information.
89 information.
90 """)
90 """)
91
91
92 # Prompts.
92 # Prompts.
93 in_prompt = Unicode(default_in_prompt, config=True)
93 in_prompt = Unicode(default_in_prompt, config=True)
94 out_prompt = Unicode(default_out_prompt, config=True)
94 out_prompt = Unicode(default_out_prompt, config=True)
95 input_sep = Unicode(default_input_sep, config=True)
95 input_sep = Unicode(default_input_sep, config=True)
96 output_sep = Unicode(default_output_sep, config=True)
96 output_sep = Unicode(default_output_sep, config=True)
97 output_sep2 = Unicode(default_output_sep2, config=True)
97 output_sep2 = Unicode(default_output_sep2, config=True)
98
98
99 # FrontendWidget protected class variables.
99 # FrontendWidget protected class variables.
100 _input_splitter_class = IPythonInputSplitter
100 _input_splitter_class = IPythonInputSplitter
101
101
102 # IPythonWidget protected class variables.
102 # IPythonWidget protected class variables.
103 _PromptBlock = namedtuple('_PromptBlock', ['block', 'length', 'number'])
103 _PromptBlock = namedtuple('_PromptBlock', ['block', 'length', 'number'])
104 _payload_source_edit = zmq_shell_source + '.edit_magic'
104 _payload_source_edit = zmq_shell_source + '.edit_magic'
105 _payload_source_exit = zmq_shell_source + '.ask_exit'
105 _payload_source_exit = zmq_shell_source + '.ask_exit'
106 _payload_source_next_input = zmq_shell_source + '.set_next_input'
106 _payload_source_next_input = zmq_shell_source + '.set_next_input'
107 _payload_source_page = 'IPython.zmq.page.page'
107 _payload_source_page = 'IPython.zmq.page.page'
108 _retrying_history_request = False
108 _retrying_history_request = 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(IPythonWidget, self).__init__(*args, **kw)
115 super(IPythonWidget, self).__init__(*args, **kw)
116
116
117 # IPythonWidget protected variables.
117 # IPythonWidget protected variables.
118 self._payload_handlers = {
118 self._payload_handlers = {
119 self._payload_source_edit : self._handle_payload_edit,
119 self._payload_source_edit : self._handle_payload_edit,
120 self._payload_source_exit : self._handle_payload_exit,
120 self._payload_source_exit : self._handle_payload_exit,
121 self._payload_source_page : self._handle_payload_page,
121 self._payload_source_page : self._handle_payload_page,
122 self._payload_source_next_input : self._handle_payload_next_input }
122 self._payload_source_next_input : self._handle_payload_next_input }
123 self._previous_prompt_obj = None
123 self._previous_prompt_obj = None
124 self._keep_kernel_on_exit = None
124 self._keep_kernel_on_exit = None
125
125
126 # Initialize widget styling.
126 # Initialize widget styling.
127 if self.style_sheet:
127 if self.style_sheet:
128 self._style_sheet_changed()
128 self._style_sheet_changed()
129 self._syntax_style_changed()
129 self._syntax_style_changed()
130 else:
130 else:
131 self.set_default_style()
131 self.set_default_style()
132
132
133 #---------------------------------------------------------------------------
133 #---------------------------------------------------------------------------
134 # 'BaseFrontendMixin' abstract interface
134 # 'BaseFrontendMixin' abstract interface
135 #---------------------------------------------------------------------------
135 #---------------------------------------------------------------------------
136
136
137 def _handle_complete_reply(self, rep):
137 def _handle_complete_reply(self, rep):
138 """ Reimplemented to support IPython's improved completion machinery.
138 """ Reimplemented to support IPython's improved completion machinery.
139 """
139 """
140 self.log.debug("complete: %s", rep.get('content', ''))
140 self.log.debug("complete: %s", rep.get('content', ''))
141 cursor = self._get_cursor()
141 cursor = self._get_cursor()
142 info = self._request_info.get('complete')
142 info = self._request_info.get('complete')
143 if info and info.id == rep['parent_header']['msg_id'] and \
143 if info and info.id == rep['parent_header']['msg_id'] and \
144 info.pos == cursor.position():
144 info.pos == cursor.position():
145 matches = rep['content']['matches']
145 matches = rep['content']['matches']
146 text = rep['content']['matched_text']
146 text = rep['content']['matched_text']
147 offset = len(text)
147 offset = len(text)
148
148
149 # Clean up matches with period and path separators if the matched
149 # Clean up matches with period and path separators if the matched
150 # text has not been transformed. This is done by truncating all
150 # text has not been transformed. This is done by truncating all
151 # but the last component and then suitably decreasing the offset
151 # but the last component and then suitably decreasing the offset
152 # between the current cursor position and the start of completion.
152 # between the current cursor position and the start of completion.
153 if len(matches) > 1 and matches[0][:offset] == text:
153 if len(matches) > 1 and matches[0][:offset] == text:
154 parts = re.split(r'[./\\]', text)
154 parts = re.split(r'[./\\]', text)
155 sep_count = len(parts) - 1
155 sep_count = len(parts) - 1
156 if sep_count:
156 if sep_count:
157 chop_length = sum(map(len, parts[:sep_count])) + sep_count
157 chop_length = sum(map(len, parts[:sep_count])) + sep_count
158 matches = [ match[chop_length:] for match in matches ]
158 matches = [ match[chop_length:] for match in matches ]
159 offset -= chop_length
159 offset -= chop_length
160
160
161 # Move the cursor to the start of the match and complete.
161 # Move the cursor to the start of the match and complete.
162 cursor.movePosition(QtGui.QTextCursor.Left, n=offset)
162 cursor.movePosition(QtGui.QTextCursor.Left, n=offset)
163 self._complete_with_items(cursor, matches)
163 self._complete_with_items(cursor, matches)
164
164
165 def _handle_execute_reply(self, msg):
165 def _handle_execute_reply(self, msg):
166 """ Reimplemented to support prompt requests.
166 """ Reimplemented to support prompt requests.
167 """
167 """
168 info = self._request_info.get('execute')
168 info_list = self._request_info.get('execute')
169 if info and info.id == msg['parent_header']['msg_id']:
169 msg_id = msg['parent_header']['msg_id']
170 if msg_id in info_list:
171 info = info_list[msg_id]
170 if info.kind == 'prompt':
172 if info.kind == 'prompt':
171 number = msg['content']['execution_count'] + 1
173 number = msg['content']['execution_count'] + 1
172 self._show_interpreter_prompt(number)
174 self._show_interpreter_prompt(number)
175 info_list.pop(msg_id)
173 else:
176 else:
174 super(IPythonWidget, self)._handle_execute_reply(msg)
177 super(IPythonWidget, self)._handle_execute_reply(msg)
175
178
176 def _handle_history_reply(self, msg):
179 def _handle_history_reply(self, msg):
177 """ Implemented to handle history tail replies, which are only supported
180 """ Implemented to handle history tail replies, which are only supported
178 by the IPython kernel.
181 by the IPython kernel.
179 """
182 """
180 self.log.debug("history: %s", msg.get('content', ''))
183 self.log.debug("history: %s", msg.get('content', ''))
181 content = msg['content']
184 content = msg['content']
182 if 'history' not in content:
185 if 'history' not in content:
183 self.log.error("History request failed: %r"%content)
186 self.log.error("History request failed: %r"%content)
184 if content.get('status', '') == 'aborted' and \
187 if content.get('status', '') == 'aborted' and \
185 not self._retrying_history_request:
188 not self._retrying_history_request:
186 # a *different* action caused this request to be aborted, so
189 # a *different* action caused this request to be aborted, so
187 # we should try again.
190 # we should try again.
188 self.log.error("Retrying aborted history request")
191 self.log.error("Retrying aborted history request")
189 # prevent multiple retries of aborted requests:
192 # prevent multiple retries of aborted requests:
190 self._retrying_history_request = True
193 self._retrying_history_request = True
191 # wait out the kernel's queue flush, which is currently timed at 0.1s
194 # wait out the kernel's queue flush, which is currently timed at 0.1s
192 time.sleep(0.25)
195 time.sleep(0.25)
193 self.kernel_manager.shell_channel.history(hist_access_type='tail',n=1000)
196 self.kernel_manager.shell_channel.history(hist_access_type='tail',n=1000)
194 else:
197 else:
195 self._retrying_history_request = False
198 self._retrying_history_request = False
196 return
199 return
197 # reset retry flag
200 # reset retry flag
198 self._retrying_history_request = False
201 self._retrying_history_request = False
199 history_items = content['history']
202 history_items = content['history']
200 items = []
203 items = []
201 last_cell = u""
204 last_cell = u""
202 for _, _, cell in history_items:
205 for _, _, cell in history_items:
203 cell = cell.rstrip()
206 cell = cell.rstrip()
204 if cell != last_cell:
207 if cell != last_cell:
205 items.append(cell)
208 items.append(cell)
206 last_cell = cell
209 last_cell = cell
207 self._set_history(items)
210 self._set_history(items)
208
211
209 def _handle_pyout(self, msg):
212 def _handle_pyout(self, msg):
210 """ Reimplemented for IPython-style "display hook".
213 """ Reimplemented for IPython-style "display hook".
211 """
214 """
212 self.log.debug("pyout: %s", msg.get('content', ''))
215 self.log.debug("pyout: %s", msg.get('content', ''))
213 if not self._hidden and self._is_from_this_session(msg):
216 if not self._hidden and self._is_from_this_session(msg):
214 content = msg['content']
217 content = msg['content']
215 prompt_number = content['execution_count']
218 prompt_number = content['execution_count']
216 data = content['data']
219 data = content['data']
217 if data.has_key('text/html'):
220 if data.has_key('text/html'):
218 self._append_plain_text(self.output_sep, True)
221 self._append_plain_text(self.output_sep, True)
219 self._append_html(self._make_out_prompt(prompt_number), True)
222 self._append_html(self._make_out_prompt(prompt_number), True)
220 html = data['text/html']
223 html = data['text/html']
221 self._append_plain_text('\n', True)
224 self._append_plain_text('\n', True)
222 self._append_html(html + self.output_sep2, True)
225 self._append_html(html + self.output_sep2, True)
223 elif data.has_key('text/plain'):
226 elif data.has_key('text/plain'):
224 self._append_plain_text(self.output_sep, True)
227 self._append_plain_text(self.output_sep, True)
225 self._append_html(self._make_out_prompt(prompt_number), True)
228 self._append_html(self._make_out_prompt(prompt_number), True)
226 text = data['text/plain']
229 text = data['text/plain']
227 # If the repr is multiline, make sure we start on a new line,
230 # If the repr is multiline, make sure we start on a new line,
228 # so that its lines are aligned.
231 # so that its lines are aligned.
229 if "\n" in text and not self.output_sep.endswith("\n"):
232 if "\n" in text and not self.output_sep.endswith("\n"):
230 self._append_plain_text('\n', True)
233 self._append_plain_text('\n', True)
231 self._append_plain_text(text + self.output_sep2, True)
234 self._append_plain_text(text + self.output_sep2, True)
232
235
233 def _handle_display_data(self, msg):
236 def _handle_display_data(self, msg):
234 """ The base handler for the ``display_data`` message.
237 """ The base handler for the ``display_data`` message.
235 """
238 """
236 self.log.debug("display: %s", msg.get('content', ''))
239 self.log.debug("display: %s", msg.get('content', ''))
237 # For now, we don't display data from other frontends, but we
240 # For now, we don't display data from other frontends, but we
238 # eventually will as this allows all frontends to monitor the display
241 # eventually will as this allows all frontends to monitor the display
239 # data. But we need to figure out how to handle this in the GUI.
242 # data. But we need to figure out how to handle this in the GUI.
240 if not self._hidden and self._is_from_this_session(msg):
243 if not self._hidden and self._is_from_this_session(msg):
241 source = msg['content']['source']
244 source = msg['content']['source']
242 data = msg['content']['data']
245 data = msg['content']['data']
243 metadata = msg['content']['metadata']
246 metadata = msg['content']['metadata']
244 # In the regular IPythonWidget, we simply print the plain text
247 # In the regular IPythonWidget, we simply print the plain text
245 # representation.
248 # representation.
246 if data.has_key('text/html'):
249 if data.has_key('text/html'):
247 html = data['text/html']
250 html = data['text/html']
248 self._append_html(html, True)
251 self._append_html(html, True)
249 elif data.has_key('text/plain'):
252 elif data.has_key('text/plain'):
250 text = data['text/plain']
253 text = data['text/plain']
251 self._append_plain_text(text, True)
254 self._append_plain_text(text, True)
252 # This newline seems to be needed for text and html output.
255 # This newline seems to be needed for text and html output.
253 self._append_plain_text(u'\n', True)
256 self._append_plain_text(u'\n', True)
254
257
255 def _started_channels(self):
258 def _started_channels(self):
256 """ Reimplemented to make a history request.
259 """ Reimplemented to make a history request.
257 """
260 """
258 super(IPythonWidget, self)._started_channels()
261 super(IPythonWidget, self)._started_channels()
259 self.kernel_manager.shell_channel.history(hist_access_type='tail',
262 self.kernel_manager.shell_channel.history(hist_access_type='tail',
260 n=1000)
263 n=1000)
261 #---------------------------------------------------------------------------
264 #---------------------------------------------------------------------------
262 # 'ConsoleWidget' public interface
265 # 'ConsoleWidget' public interface
263 #---------------------------------------------------------------------------
266 #---------------------------------------------------------------------------
264
267
265 def copy(self):
268 def copy(self):
266 """ Copy the currently selected text to the clipboard, removing prompts
269 """ Copy the currently selected text to the clipboard, removing prompts
267 if possible.
270 if possible.
268 """
271 """
269 text = self._control.textCursor().selection().toPlainText()
272 text = self._control.textCursor().selection().toPlainText()
270 if text:
273 if text:
271 lines = map(transform_ipy_prompt, text.splitlines())
274 lines = map(transform_ipy_prompt, text.splitlines())
272 text = '\n'.join(lines)
275 text = '\n'.join(lines)
273 QtGui.QApplication.clipboard().setText(text)
276 QtGui.QApplication.clipboard().setText(text)
274
277
275 #---------------------------------------------------------------------------
278 #---------------------------------------------------------------------------
276 # 'FrontendWidget' public interface
279 # 'FrontendWidget' public interface
277 #---------------------------------------------------------------------------
280 #---------------------------------------------------------------------------
278
281
279 def execute_file(self, path, hidden=False):
282 def execute_file(self, path, hidden=False):
280 """ Reimplemented to use the 'run' magic.
283 """ Reimplemented to use the 'run' magic.
281 """
284 """
282 # Use forward slashes on Windows to avoid escaping each separator.
285 # Use forward slashes on Windows to avoid escaping each separator.
283 if sys.platform == 'win32':
286 if sys.platform == 'win32':
284 path = os.path.normpath(path).replace('\\', '/')
287 path = os.path.normpath(path).replace('\\', '/')
285
288
286 # Perhaps we should not be using %run directly, but while we
289 # Perhaps we should not be using %run directly, but while we
287 # are, it is necessary to quote filenames containing spaces or quotes.
290 # are, it is necessary to quote filenames containing spaces or quotes.
288 # Escaping quotes in filename in %run seems tricky and inconsistent,
291 # Escaping quotes in filename in %run seems tricky and inconsistent,
289 # so not trying it at present.
292 # so not trying it at present.
290 if '"' in path:
293 if '"' in path:
291 if "'" in path:
294 if "'" in path:
292 raise ValueError("Can't run filename containing both single "
295 raise ValueError("Can't run filename containing both single "
293 "and double quotes: %s" % path)
296 "and double quotes: %s" % path)
294 path = "'%s'" % path
297 path = "'%s'" % path
295 elif ' ' in path or "'" in path:
298 elif ' ' in path or "'" in path:
296 path = '"%s"' % path
299 path = '"%s"' % path
297
300
298 self.execute('%%run %s' % path, hidden=hidden)
301 self.execute('%%run %s' % path, hidden=hidden)
299
302
300 #---------------------------------------------------------------------------
303 #---------------------------------------------------------------------------
301 # 'FrontendWidget' protected interface
304 # 'FrontendWidget' protected interface
302 #---------------------------------------------------------------------------
305 #---------------------------------------------------------------------------
303
306
304 def _complete(self):
307 def _complete(self):
305 """ Reimplemented to support IPython's improved completion machinery.
308 """ Reimplemented to support IPython's improved completion machinery.
306 """
309 """
307 # We let the kernel split the input line, so we *always* send an empty
310 # We let the kernel split the input line, so we *always* send an empty
308 # text field. Readline-based frontends do get a real text field which
311 # text field. Readline-based frontends do get a real text field which
309 # they can use.
312 # they can use.
310 text = ''
313 text = ''
311
314
312 # Send the completion request to the kernel
315 # Send the completion request to the kernel
313 msg_id = self.kernel_manager.shell_channel.complete(
316 msg_id = self.kernel_manager.shell_channel.complete(
314 text, # text
317 text, # text
315 self._get_input_buffer_cursor_line(), # line
318 self._get_input_buffer_cursor_line(), # line
316 self._get_input_buffer_cursor_column(), # cursor_pos
319 self._get_input_buffer_cursor_column(), # cursor_pos
317 self.input_buffer) # block
320 self.input_buffer) # block
318 pos = self._get_cursor().position()
321 pos = self._get_cursor().position()
319 info = self._CompletionRequest(msg_id, pos)
322 info = self._CompletionRequest(msg_id, pos)
320 self._request_info['complete'] = info
323 self._request_info['complete'] = info
321
324
322 def _process_execute_error(self, msg):
325 def _process_execute_error(self, msg):
323 """ Reimplemented for IPython-style traceback formatting.
326 """ Reimplemented for IPython-style traceback formatting.
324 """
327 """
325 content = msg['content']
328 content = msg['content']
326 traceback = '\n'.join(content['traceback']) + '\n'
329 traceback = '\n'.join(content['traceback']) + '\n'
327 if False:
330 if False:
328 # FIXME: For now, tracebacks come as plain text, so we can't use
331 # FIXME: For now, tracebacks come as plain text, so we can't use
329 # the html renderer yet. Once we refactor ultratb to produce
332 # the html renderer yet. Once we refactor ultratb to produce
330 # properly styled tracebacks, this branch should be the default
333 # properly styled tracebacks, this branch should be the default
331 traceback = traceback.replace(' ', '&nbsp;')
334 traceback = traceback.replace(' ', '&nbsp;')
332 traceback = traceback.replace('\n', '<br/>')
335 traceback = traceback.replace('\n', '<br/>')
333
336
334 ename = content['ename']
337 ename = content['ename']
335 ename_styled = '<span class="error">%s</span>' % ename
338 ename_styled = '<span class="error">%s</span>' % ename
336 traceback = traceback.replace(ename, ename_styled)
339 traceback = traceback.replace(ename, ename_styled)
337
340
338 self._append_html(traceback)
341 self._append_html(traceback)
339 else:
342 else:
340 # This is the fallback for now, using plain text with ansi escapes
343 # This is the fallback for now, using plain text with ansi escapes
341 self._append_plain_text(traceback)
344 self._append_plain_text(traceback)
342
345
343 def _process_execute_payload(self, item):
346 def _process_execute_payload(self, item):
344 """ Reimplemented to dispatch payloads to handler methods.
347 """ Reimplemented to dispatch payloads to handler methods.
345 """
348 """
346 handler = self._payload_handlers.get(item['source'])
349 handler = self._payload_handlers.get(item['source'])
347 if handler is None:
350 if handler is None:
348 # We have no handler for this type of payload, simply ignore it
351 # We have no handler for this type of payload, simply ignore it
349 return False
352 return False
350 else:
353 else:
351 handler(item)
354 handler(item)
352 return True
355 return True
353
356
354 def _show_interpreter_prompt(self, number=None):
357 def _show_interpreter_prompt(self, number=None):
355 """ Reimplemented for IPython-style prompts.
358 """ Reimplemented for IPython-style prompts.
356 """
359 """
357 # If a number was not specified, make a prompt number request.
360 # If a number was not specified, make a prompt number request.
358 if number is None:
361 if number is None:
359 msg_id = self.kernel_manager.shell_channel.execute('', silent=True)
362 msg_id = self.kernel_manager.shell_channel.execute('', silent=True)
360 info = self._ExecutionRequest(msg_id, 'prompt')
363 info = self._ExecutionRequest(msg_id, 'prompt')
361 self._request_info['execute'] = info
364 self._request_info['execute'][msg_id] = info
362 return
365 return
363
366
364 # Show a new prompt and save information about it so that it can be
367 # Show a new prompt and save information about it so that it can be
365 # updated later if the prompt number turns out to be wrong.
368 # updated later if the prompt number turns out to be wrong.
366 self._prompt_sep = self.input_sep
369 self._prompt_sep = self.input_sep
367 self._show_prompt(self._make_in_prompt(number), html=True)
370 self._show_prompt(self._make_in_prompt(number), html=True)
368 block = self._control.document().lastBlock()
371 block = self._control.document().lastBlock()
369 length = len(self._prompt)
372 length = len(self._prompt)
370 self._previous_prompt_obj = self._PromptBlock(block, length, number)
373 self._previous_prompt_obj = self._PromptBlock(block, length, number)
371
374
372 # Update continuation prompt to reflect (possibly) new prompt length.
375 # Update continuation prompt to reflect (possibly) new prompt length.
373 self._set_continuation_prompt(
376 self._set_continuation_prompt(
374 self._make_continuation_prompt(self._prompt), html=True)
377 self._make_continuation_prompt(self._prompt), html=True)
375
378
376 def _show_interpreter_prompt_for_reply(self, msg):
379 def _show_interpreter_prompt_for_reply(self, msg):
377 """ Reimplemented for IPython-style prompts.
380 """ Reimplemented for IPython-style prompts.
378 """
381 """
379 # Update the old prompt number if necessary.
382 # Update the old prompt number if necessary.
380 content = msg['content']
383 content = msg['content']
381 # abort replies do not have any keys:
384 # abort replies do not have any keys:
382 if content['status'] == 'aborted':
385 if content['status'] == 'aborted':
383 if self._previous_prompt_obj:
386 if self._previous_prompt_obj:
384 previous_prompt_number = self._previous_prompt_obj.number
387 previous_prompt_number = self._previous_prompt_obj.number
385 else:
388 else:
386 previous_prompt_number = 0
389 previous_prompt_number = 0
387 else:
390 else:
388 previous_prompt_number = content['execution_count']
391 previous_prompt_number = content['execution_count']
389 if self._previous_prompt_obj and \
392 if self._previous_prompt_obj and \
390 self._previous_prompt_obj.number != previous_prompt_number:
393 self._previous_prompt_obj.number != previous_prompt_number:
391 block = self._previous_prompt_obj.block
394 block = self._previous_prompt_obj.block
392
395
393 # Make sure the prompt block has not been erased.
396 # Make sure the prompt block has not been erased.
394 if block.isValid() and block.text():
397 if block.isValid() and block.text():
395
398
396 # Remove the old prompt and insert a new prompt.
399 # Remove the old prompt and insert a new prompt.
397 cursor = QtGui.QTextCursor(block)
400 cursor = QtGui.QTextCursor(block)
398 cursor.movePosition(QtGui.QTextCursor.Right,
401 cursor.movePosition(QtGui.QTextCursor.Right,
399 QtGui.QTextCursor.KeepAnchor,
402 QtGui.QTextCursor.KeepAnchor,
400 self._previous_prompt_obj.length)
403 self._previous_prompt_obj.length)
401 prompt = self._make_in_prompt(previous_prompt_number)
404 prompt = self._make_in_prompt(previous_prompt_number)
402 self._prompt = self._insert_html_fetching_plain_text(
405 self._prompt = self._insert_html_fetching_plain_text(
403 cursor, prompt)
406 cursor, prompt)
404
407
405 # When the HTML is inserted, Qt blows away the syntax
408 # When the HTML is inserted, Qt blows away the syntax
406 # highlighting for the line, so we need to rehighlight it.
409 # highlighting for the line, so we need to rehighlight it.
407 self._highlighter.rehighlightBlock(cursor.block())
410 self._highlighter.rehighlightBlock(cursor.block())
408
411
409 self._previous_prompt_obj = None
412 self._previous_prompt_obj = None
410
413
411 # Show a new prompt with the kernel's estimated prompt number.
414 # Show a new prompt with the kernel's estimated prompt number.
412 self._show_interpreter_prompt(previous_prompt_number + 1)
415 self._show_interpreter_prompt(previous_prompt_number + 1)
413
416
414 #---------------------------------------------------------------------------
417 #---------------------------------------------------------------------------
415 # 'IPythonWidget' interface
418 # 'IPythonWidget' interface
416 #---------------------------------------------------------------------------
419 #---------------------------------------------------------------------------
417
420
418 def set_default_style(self, colors='lightbg'):
421 def set_default_style(self, colors='lightbg'):
419 """ Sets the widget style to the class defaults.
422 """ Sets the widget style to the class defaults.
420
423
421 Parameters:
424 Parameters:
422 -----------
425 -----------
423 colors : str, optional (default lightbg)
426 colors : str, optional (default lightbg)
424 Whether to use the default IPython light background or dark
427 Whether to use the default IPython light background or dark
425 background or B&W style.
428 background or B&W style.
426 """
429 """
427 colors = colors.lower()
430 colors = colors.lower()
428 if colors=='lightbg':
431 if colors=='lightbg':
429 self.style_sheet = styles.default_light_style_sheet
432 self.style_sheet = styles.default_light_style_sheet
430 self.syntax_style = styles.default_light_syntax_style
433 self.syntax_style = styles.default_light_syntax_style
431 elif colors=='linux':
434 elif colors=='linux':
432 self.style_sheet = styles.default_dark_style_sheet
435 self.style_sheet = styles.default_dark_style_sheet
433 self.syntax_style = styles.default_dark_syntax_style
436 self.syntax_style = styles.default_dark_syntax_style
434 elif colors=='nocolor':
437 elif colors=='nocolor':
435 self.style_sheet = styles.default_bw_style_sheet
438 self.style_sheet = styles.default_bw_style_sheet
436 self.syntax_style = styles.default_bw_syntax_style
439 self.syntax_style = styles.default_bw_syntax_style
437 else:
440 else:
438 raise KeyError("No such color scheme: %s"%colors)
441 raise KeyError("No such color scheme: %s"%colors)
439
442
440 #---------------------------------------------------------------------------
443 #---------------------------------------------------------------------------
441 # 'IPythonWidget' protected interface
444 # 'IPythonWidget' protected interface
442 #---------------------------------------------------------------------------
445 #---------------------------------------------------------------------------
443
446
444 def _edit(self, filename, line=None):
447 def _edit(self, filename, line=None):
445 """ Opens a Python script for editing.
448 """ Opens a Python script for editing.
446
449
447 Parameters:
450 Parameters:
448 -----------
451 -----------
449 filename : str
452 filename : str
450 A path to a local system file.
453 A path to a local system file.
451
454
452 line : int, optional
455 line : int, optional
453 A line of interest in the file.
456 A line of interest in the file.
454 """
457 """
455 if self.custom_edit:
458 if self.custom_edit:
456 self.custom_edit_requested.emit(filename, line)
459 self.custom_edit_requested.emit(filename, line)
457 elif not self.editor:
460 elif not self.editor:
458 self._append_plain_text('No default editor available.\n'
461 self._append_plain_text('No default editor available.\n'
459 'Specify a GUI text editor in the `IPythonWidget.editor` '
462 'Specify a GUI text editor in the `IPythonWidget.editor` '
460 'configurable to enable the %edit magic')
463 'configurable to enable the %edit magic')
461 else:
464 else:
462 try:
465 try:
463 filename = '"%s"' % filename
466 filename = '"%s"' % filename
464 if line and self.editor_line:
467 if line and self.editor_line:
465 command = self.editor_line.format(filename=filename,
468 command = self.editor_line.format(filename=filename,
466 line=line)
469 line=line)
467 else:
470 else:
468 try:
471 try:
469 command = self.editor.format()
472 command = self.editor.format()
470 except KeyError:
473 except KeyError:
471 command = self.editor.format(filename=filename)
474 command = self.editor.format(filename=filename)
472 else:
475 else:
473 command += ' ' + filename
476 command += ' ' + filename
474 except KeyError:
477 except KeyError:
475 self._append_plain_text('Invalid editor command.\n')
478 self._append_plain_text('Invalid editor command.\n')
476 else:
479 else:
477 try:
480 try:
478 Popen(command, shell=True)
481 Popen(command, shell=True)
479 except OSError:
482 except OSError:
480 msg = 'Opening editor with command "%s" failed.\n'
483 msg = 'Opening editor with command "%s" failed.\n'
481 self._append_plain_text(msg % command)
484 self._append_plain_text(msg % command)
482
485
483 def _make_in_prompt(self, number):
486 def _make_in_prompt(self, number):
484 """ Given a prompt number, returns an HTML In prompt.
487 """ Given a prompt number, returns an HTML In prompt.
485 """
488 """
486 try:
489 try:
487 body = self.in_prompt % number
490 body = self.in_prompt % number
488 except TypeError:
491 except TypeError:
489 # allow in_prompt to leave out number, e.g. '>>> '
492 # allow in_prompt to leave out number, e.g. '>>> '
490 body = self.in_prompt
493 body = self.in_prompt
491 return '<span class="in-prompt">%s</span>' % body
494 return '<span class="in-prompt">%s</span>' % body
492
495
493 def _make_continuation_prompt(self, prompt):
496 def _make_continuation_prompt(self, prompt):
494 """ Given a plain text version of an In prompt, returns an HTML
497 """ Given a plain text version of an In prompt, returns an HTML
495 continuation prompt.
498 continuation prompt.
496 """
499 """
497 end_chars = '...: '
500 end_chars = '...: '
498 space_count = len(prompt.lstrip('\n')) - len(end_chars)
501 space_count = len(prompt.lstrip('\n')) - len(end_chars)
499 body = '&nbsp;' * space_count + end_chars
502 body = '&nbsp;' * space_count + end_chars
500 return '<span class="in-prompt">%s</span>' % body
503 return '<span class="in-prompt">%s</span>' % body
501
504
502 def _make_out_prompt(self, number):
505 def _make_out_prompt(self, number):
503 """ Given a prompt number, returns an HTML Out prompt.
506 """ Given a prompt number, returns an HTML Out prompt.
504 """
507 """
505 body = self.out_prompt % number
508 body = self.out_prompt % number
506 return '<span class="out-prompt">%s</span>' % body
509 return '<span class="out-prompt">%s</span>' % body
507
510
508 #------ Payload handlers --------------------------------------------------
511 #------ Payload handlers --------------------------------------------------
509
512
510 # Payload handlers with a generic interface: each takes the opaque payload
513 # Payload handlers with a generic interface: each takes the opaque payload
511 # dict, unpacks it and calls the underlying functions with the necessary
514 # dict, unpacks it and calls the underlying functions with the necessary
512 # arguments.
515 # arguments.
513
516
514 def _handle_payload_edit(self, item):
517 def _handle_payload_edit(self, item):
515 self._edit(item['filename'], item['line_number'])
518 self._edit(item['filename'], item['line_number'])
516
519
517 def _handle_payload_exit(self, item):
520 def _handle_payload_exit(self, item):
518 self._keep_kernel_on_exit = item['keepkernel']
521 self._keep_kernel_on_exit = item['keepkernel']
519 self.exit_requested.emit(self)
522 self.exit_requested.emit(self)
520
523
521 def _handle_payload_next_input(self, item):
524 def _handle_payload_next_input(self, item):
522 self.input_buffer = dedent(item['text'].rstrip())
525 self.input_buffer = dedent(item['text'].rstrip())
523
526
524 def _handle_payload_page(self, item):
527 def _handle_payload_page(self, item):
525 # Since the plain text widget supports only a very small subset of HTML
528 # Since the plain text widget supports only a very small subset of HTML
526 # and we have no control over the HTML source, we only page HTML
529 # and we have no control over the HTML source, we only page HTML
527 # payloads in the rich text widget.
530 # payloads in the rich text widget.
528 if item['html'] and self.kind == 'rich':
531 if item['html'] and self.kind == 'rich':
529 self._page(item['html'], html=True)
532 self._page(item['html'], html=True)
530 else:
533 else:
531 self._page(item['text'], html=False)
534 self._page(item['text'], html=False)
532
535
533 #------ Trait change handlers --------------------------------------------
536 #------ Trait change handlers --------------------------------------------
534
537
535 def _style_sheet_changed(self):
538 def _style_sheet_changed(self):
536 """ Set the style sheets of the underlying widgets.
539 """ Set the style sheets of the underlying widgets.
537 """
540 """
538 self.setStyleSheet(self.style_sheet)
541 self.setStyleSheet(self.style_sheet)
539 self._control.document().setDefaultStyleSheet(self.style_sheet)
542 self._control.document().setDefaultStyleSheet(self.style_sheet)
540 if self._page_control:
543 if self._page_control:
541 self._page_control.document().setDefaultStyleSheet(self.style_sheet)
544 self._page_control.document().setDefaultStyleSheet(self.style_sheet)
542
545
543 bg_color = self._control.palette().window().color()
546 bg_color = self._control.palette().window().color()
544 self._ansi_processor.set_background_color(bg_color)
547 self._ansi_processor.set_background_color(bg_color)
545
548
546
549
547 def _syntax_style_changed(self):
550 def _syntax_style_changed(self):
548 """ Set the style for the syntax highlighter.
551 """ Set the style for the syntax highlighter.
549 """
552 """
550 if self._highlighter is None:
553 if self._highlighter is None:
551 # ignore premature calls
554 # ignore premature calls
552 return
555 return
553 if self.syntax_style:
556 if self.syntax_style:
554 self._highlighter.set_style(self.syntax_style)
557 self._highlighter.set_style(self.syntax_style)
555 else:
558 else:
556 self._highlighter.set_style_sheet(self.style_sheet)
559 self._highlighter.set_style_sheet(self.style_sheet)
557
560
558 #------ Trait default initializers -----------------------------------------
561 #------ Trait default initializers -----------------------------------------
559
562
560 def _banner_default(self):
563 def _banner_default(self):
561 from IPython.core.usage import default_gui_banner
564 from IPython.core.usage import default_gui_banner
562 return default_gui_banner
565 return default_gui_banner
@@ -1,906 +1,909 b''
1 """The Qt MainWindow for the QtConsole
1 """The Qt MainWindow for the QtConsole
2
2
3 This is a tabbed pseudo-terminal of IPython sessions, with a menu bar for
3 This is a tabbed pseudo-terminal of IPython sessions, with a menu bar for
4 common actions.
4 common actions.
5
5
6 Authors:
6 Authors:
7
7
8 * Evan Patterson
8 * Evan Patterson
9 * Min RK
9 * Min RK
10 * Erik Tollerud
10 * Erik Tollerud
11 * Fernando Perez
11 * Fernando Perez
12 * Bussonnier Matthias
12 * Bussonnier Matthias
13 * Thomas Kluyver
13 * Thomas Kluyver
14
14
15 """
15 """
16
16
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18 # Imports
18 # Imports
19 #-----------------------------------------------------------------------------
19 #-----------------------------------------------------------------------------
20
20
21 # stdlib imports
21 # stdlib imports
22 import sys
22 import sys
23 import re
23 import re
24 import webbrowser
24 import webbrowser
25 from threading import Thread
25 from threading import Thread
26
26
27 # System library imports
27 # System library imports
28 from IPython.external.qt import QtGui,QtCore
28 from IPython.external.qt import QtGui,QtCore
29
29
30 def background(f):
30 def background(f):
31 """call a function in a simple thread, to prevent blocking"""
31 """call a function in a simple thread, to prevent blocking"""
32 t = Thread(target=f)
32 t = Thread(target=f)
33 t.start()
33 t.start()
34 return t
34 return t
35
35
36 #-----------------------------------------------------------------------------
36 #-----------------------------------------------------------------------------
37 # Classes
37 # Classes
38 #-----------------------------------------------------------------------------
38 #-----------------------------------------------------------------------------
39
39
40 class MainWindow(QtGui.QMainWindow):
40 class MainWindow(QtGui.QMainWindow):
41
41
42 #---------------------------------------------------------------------------
42 #---------------------------------------------------------------------------
43 # 'object' interface
43 # 'object' interface
44 #---------------------------------------------------------------------------
44 #---------------------------------------------------------------------------
45
45
46 def __init__(self, app,
46 def __init__(self, app,
47 confirm_exit=True,
47 confirm_exit=True,
48 new_frontend_factory=None, slave_frontend_factory=None,
48 new_frontend_factory=None, slave_frontend_factory=None,
49 ):
49 ):
50 """ Create a tabbed MainWindow for managing IPython FrontendWidgets
50 """ Create a tabbed MainWindow for managing IPython FrontendWidgets
51
51
52 Parameters
52 Parameters
53 ----------
53 ----------
54
54
55 app : reference to QApplication parent
55 app : reference to QApplication parent
56 confirm_exit : bool, optional
56 confirm_exit : bool, optional
57 Whether we should prompt on close of tabs
57 Whether we should prompt on close of tabs
58 new_frontend_factory : callable
58 new_frontend_factory : callable
59 A callable that returns a new IPythonWidget instance, attached to
59 A callable that returns a new IPythonWidget instance, attached to
60 its own running kernel.
60 its own running kernel.
61 slave_frontend_factory : callable
61 slave_frontend_factory : callable
62 A callable that takes an existing IPythonWidget, and returns a new
62 A callable that takes an existing IPythonWidget, and returns a new
63 IPythonWidget instance, attached to the same kernel.
63 IPythonWidget instance, attached to the same kernel.
64 """
64 """
65
65
66 super(MainWindow, self).__init__()
66 super(MainWindow, self).__init__()
67 self._kernel_counter = 0
67 self._kernel_counter = 0
68 self._app = app
68 self._app = app
69 self.confirm_exit = confirm_exit
69 self.confirm_exit = confirm_exit
70 self.new_frontend_factory = new_frontend_factory
70 self.new_frontend_factory = new_frontend_factory
71 self.slave_frontend_factory = slave_frontend_factory
71 self.slave_frontend_factory = slave_frontend_factory
72
72
73 self.tab_widget = QtGui.QTabWidget(self)
73 self.tab_widget = QtGui.QTabWidget(self)
74 self.tab_widget.setDocumentMode(True)
74 self.tab_widget.setDocumentMode(True)
75 self.tab_widget.setTabsClosable(True)
75 self.tab_widget.setTabsClosable(True)
76 self.tab_widget.tabCloseRequested[int].connect(self.close_tab)
76 self.tab_widget.tabCloseRequested[int].connect(self.close_tab)
77
77
78 self.setCentralWidget(self.tab_widget)
78 self.setCentralWidget(self.tab_widget)
79 # hide tab bar at first, since we have no tabs:
79 # hide tab bar at first, since we have no tabs:
80 self.tab_widget.tabBar().setVisible(False)
80 self.tab_widget.tabBar().setVisible(False)
81 # prevent focus in tab bar
81 # prevent focus in tab bar
82 self.tab_widget.setFocusPolicy(QtCore.Qt.NoFocus)
82 self.tab_widget.setFocusPolicy(QtCore.Qt.NoFocus)
83
83
84 def update_tab_bar_visibility(self):
84 def update_tab_bar_visibility(self):
85 """ update visibility of the tabBar depending of the number of tab
85 """ update visibility of the tabBar depending of the number of tab
86
86
87 0 or 1 tab, tabBar hidden
87 0 or 1 tab, tabBar hidden
88 2+ tabs, tabBar visible
88 2+ tabs, tabBar visible
89
89
90 send a self.close if number of tab ==0
90 send a self.close if number of tab ==0
91
91
92 need to be called explicitely, or be connected to tabInserted/tabRemoved
92 need to be called explicitely, or be connected to tabInserted/tabRemoved
93 """
93 """
94 if self.tab_widget.count() <= 1:
94 if self.tab_widget.count() <= 1:
95 self.tab_widget.tabBar().setVisible(False)
95 self.tab_widget.tabBar().setVisible(False)
96 else:
96 else:
97 self.tab_widget.tabBar().setVisible(True)
97 self.tab_widget.tabBar().setVisible(True)
98 if self.tab_widget.count()==0 :
98 if self.tab_widget.count()==0 :
99 self.close()
99 self.close()
100
100
101 @property
101 @property
102 def next_kernel_id(self):
102 def next_kernel_id(self):
103 """constantly increasing counter for kernel IDs"""
103 """constantly increasing counter for kernel IDs"""
104 c = self._kernel_counter
104 c = self._kernel_counter
105 self._kernel_counter += 1
105 self._kernel_counter += 1
106 return c
106 return c
107
107
108 @property
108 @property
109 def active_frontend(self):
109 def active_frontend(self):
110 return self.tab_widget.currentWidget()
110 return self.tab_widget.currentWidget()
111
111
112 def create_tab_with_new_frontend(self):
112 def create_tab_with_new_frontend(self):
113 """create a new frontend and attach it to a new tab"""
113 """create a new frontend and attach it to a new tab"""
114 widget = self.new_frontend_factory()
114 widget = self.new_frontend_factory()
115 self.add_tab_with_frontend(widget)
115 self.add_tab_with_frontend(widget)
116
116
117 def create_tab_with_current_kernel(self):
117 def create_tab_with_current_kernel(self):
118 """create a new frontend attached to the same kernel as the current tab"""
118 """create a new frontend attached to the same kernel as the current tab"""
119 current_widget = self.tab_widget.currentWidget()
119 current_widget = self.tab_widget.currentWidget()
120 current_widget_index = self.tab_widget.indexOf(current_widget)
120 current_widget_index = self.tab_widget.indexOf(current_widget)
121 current_widget_name = self.tab_widget.tabText(current_widget_index)
121 current_widget_name = self.tab_widget.tabText(current_widget_index)
122 widget = self.slave_frontend_factory(current_widget)
122 widget = self.slave_frontend_factory(current_widget)
123 if 'slave' in current_widget_name:
123 if 'slave' in current_widget_name:
124 # don't keep stacking slaves
124 # don't keep stacking slaves
125 name = current_widget_name
125 name = current_widget_name
126 else:
126 else:
127 name = '(%s) slave' % current_widget_name
127 name = '(%s) slave' % current_widget_name
128 self.add_tab_with_frontend(widget,name=name)
128 self.add_tab_with_frontend(widget,name=name)
129
129
130 def close_tab(self,current_tab):
130 def close_tab(self,current_tab):
131 """ Called when you need to try to close a tab.
131 """ Called when you need to try to close a tab.
132
132
133 It takes the number of the tab to be closed as argument, or a referece
133 It takes the number of the tab to be closed as argument, or a referece
134 to the wiget insite this tab
134 to the wiget insite this tab
135 """
135 """
136
136
137 # let's be sure "tab" and "closing widget are respectivey the index of the tab to close
137 # let's be sure "tab" and "closing widget are respectivey the index of the tab to close
138 # and a reference to the trontend to close
138 # and a reference to the trontend to close
139 if type(current_tab) is not int :
139 if type(current_tab) is not int :
140 current_tab = self.tab_widget.indexOf(current_tab)
140 current_tab = self.tab_widget.indexOf(current_tab)
141 closing_widget=self.tab_widget.widget(current_tab)
141 closing_widget=self.tab_widget.widget(current_tab)
142
142
143
143
144 # when trying to be closed, widget might re-send a request to be closed again, but will
144 # when trying to be closed, widget might re-send a request to be closed again, but will
145 # be deleted when event will be processed. So need to check that widget still exist and
145 # be deleted when event will be processed. So need to check that widget still exist and
146 # skip if not. One example of this is when 'exit' is send in a slave tab. 'exit' will be
146 # skip if not. One example of this is when 'exit' is send in a slave tab. 'exit' will be
147 # re-send by this fonction on the master widget, which ask all slaves widget to exit
147 # re-send by this fonction on the master widget, which ask all slaves widget to exit
148 if closing_widget==None:
148 if closing_widget==None:
149 return
149 return
150
150
151 #get a list of all slave widgets on the same kernel.
151 #get a list of all slave widgets on the same kernel.
152 slave_tabs = self.find_slave_widgets(closing_widget)
152 slave_tabs = self.find_slave_widgets(closing_widget)
153
153
154 keepkernel = None #Use the prompt by default
154 keepkernel = None #Use the prompt by default
155 if hasattr(closing_widget,'_keep_kernel_on_exit'): #set by exit magic
155 if hasattr(closing_widget,'_keep_kernel_on_exit'): #set by exit magic
156 keepkernel = closing_widget._keep_kernel_on_exit
156 keepkernel = closing_widget._keep_kernel_on_exit
157 # If signal sent by exit magic (_keep_kernel_on_exit, exist and not None)
157 # If signal sent by exit magic (_keep_kernel_on_exit, exist and not None)
158 # we set local slave tabs._hidden to True to avoid prompting for kernel
158 # we set local slave tabs._hidden to True to avoid prompting for kernel
159 # restart when they get the signal. and then "forward" the 'exit'
159 # restart when they get the signal. and then "forward" the 'exit'
160 # to the main window
160 # to the main window
161 if keepkernel is not None:
161 if keepkernel is not None:
162 for tab in slave_tabs:
162 for tab in slave_tabs:
163 tab._hidden = True
163 tab._hidden = True
164 if closing_widget in slave_tabs:
164 if closing_widget in slave_tabs:
165 try :
165 try :
166 self.find_master_tab(closing_widget).execute('exit')
166 self.find_master_tab(closing_widget).execute('exit')
167 except AttributeError:
167 except AttributeError:
168 self.log.info("Master already closed or not local, closing only current tab")
168 self.log.info("Master already closed or not local, closing only current tab")
169 self.tab_widget.removeTab(current_tab)
169 self.tab_widget.removeTab(current_tab)
170 self.update_tab_bar_visibility()
170 self.update_tab_bar_visibility()
171 return
171 return
172
172
173 kernel_manager = closing_widget.kernel_manager
173 kernel_manager = closing_widget.kernel_manager
174
174
175 if keepkernel is None and not closing_widget._confirm_exit:
175 if keepkernel is None and not closing_widget._confirm_exit:
176 # don't prompt, just terminate the kernel if we own it
176 # don't prompt, just terminate the kernel if we own it
177 # or leave it alone if we don't
177 # or leave it alone if we don't
178 keepkernel = closing_widget._existing
178 keepkernel = closing_widget._existing
179 if keepkernel is None: #show prompt
179 if keepkernel is None: #show prompt
180 if kernel_manager and kernel_manager.channels_running:
180 if kernel_manager and kernel_manager.channels_running:
181 title = self.window().windowTitle()
181 title = self.window().windowTitle()
182 cancel = QtGui.QMessageBox.Cancel
182 cancel = QtGui.QMessageBox.Cancel
183 okay = QtGui.QMessageBox.Ok
183 okay = QtGui.QMessageBox.Ok
184 if closing_widget._may_close:
184 if closing_widget._may_close:
185 msg = "You are closing the tab : "+'"'+self.tab_widget.tabText(current_tab)+'"'
185 msg = "You are closing the tab : "+'"'+self.tab_widget.tabText(current_tab)+'"'
186 info = "Would you like to quit the Kernel and close all attached Consoles as well?"
186 info = "Would you like to quit the Kernel and close all attached Consoles as well?"
187 justthis = QtGui.QPushButton("&No, just this Tab", self)
187 justthis = QtGui.QPushButton("&No, just this Tab", self)
188 justthis.setShortcut('N')
188 justthis.setShortcut('N')
189 closeall = QtGui.QPushButton("&Yes, close all", self)
189 closeall = QtGui.QPushButton("&Yes, close all", self)
190 closeall.setShortcut('Y')
190 closeall.setShortcut('Y')
191 box = QtGui.QMessageBox(QtGui.QMessageBox.Question,
191 box = QtGui.QMessageBox(QtGui.QMessageBox.Question,
192 title, msg)
192 title, msg)
193 box.setInformativeText(info)
193 box.setInformativeText(info)
194 box.addButton(cancel)
194 box.addButton(cancel)
195 box.addButton(justthis, QtGui.QMessageBox.NoRole)
195 box.addButton(justthis, QtGui.QMessageBox.NoRole)
196 box.addButton(closeall, QtGui.QMessageBox.YesRole)
196 box.addButton(closeall, QtGui.QMessageBox.YesRole)
197 box.setDefaultButton(closeall)
197 box.setDefaultButton(closeall)
198 box.setEscapeButton(cancel)
198 box.setEscapeButton(cancel)
199 pixmap = QtGui.QPixmap(self._app.icon.pixmap(QtCore.QSize(64,64)))
199 pixmap = QtGui.QPixmap(self._app.icon.pixmap(QtCore.QSize(64,64)))
200 box.setIconPixmap(pixmap)
200 box.setIconPixmap(pixmap)
201 reply = box.exec_()
201 reply = box.exec_()
202 if reply == 1: # close All
202 if reply == 1: # close All
203 for slave in slave_tabs:
203 for slave in slave_tabs:
204 background(slave.kernel_manager.stop_channels)
204 background(slave.kernel_manager.stop_channels)
205 self.tab_widget.removeTab(self.tab_widget.indexOf(slave))
205 self.tab_widget.removeTab(self.tab_widget.indexOf(slave))
206 closing_widget.execute("exit")
206 closing_widget.execute("exit")
207 self.tab_widget.removeTab(current_tab)
207 self.tab_widget.removeTab(current_tab)
208 background(kernel_manager.stop_channels)
208 background(kernel_manager.stop_channels)
209 elif reply == 0: # close Console
209 elif reply == 0: # close Console
210 if not closing_widget._existing:
210 if not closing_widget._existing:
211 # Have kernel: don't quit, just close the tab
211 # Have kernel: don't quit, just close the tab
212 closing_widget.execute("exit True")
212 closing_widget.execute("exit True")
213 self.tab_widget.removeTab(current_tab)
213 self.tab_widget.removeTab(current_tab)
214 background(kernel_manager.stop_channels)
214 background(kernel_manager.stop_channels)
215 else:
215 else:
216 reply = QtGui.QMessageBox.question(self, title,
216 reply = QtGui.QMessageBox.question(self, title,
217 "Are you sure you want to close this Console?"+
217 "Are you sure you want to close this Console?"+
218 "\nThe Kernel and other Consoles will remain active.",
218 "\nThe Kernel and other Consoles will remain active.",
219 okay|cancel,
219 okay|cancel,
220 defaultButton=okay
220 defaultButton=okay
221 )
221 )
222 if reply == okay:
222 if reply == okay:
223 self.tab_widget.removeTab(current_tab)
223 self.tab_widget.removeTab(current_tab)
224 elif keepkernel: #close console but leave kernel running (no prompt)
224 elif keepkernel: #close console but leave kernel running (no prompt)
225 self.tab_widget.removeTab(current_tab)
225 self.tab_widget.removeTab(current_tab)
226 background(kernel_manager.stop_channels)
226 background(kernel_manager.stop_channels)
227 else: #close console and kernel (no prompt)
227 else: #close console and kernel (no prompt)
228 self.tab_widget.removeTab(current_tab)
228 self.tab_widget.removeTab(current_tab)
229 if kernel_manager and kernel_manager.channels_running:
229 if kernel_manager and kernel_manager.channels_running:
230 for slave in slave_tabs:
230 for slave in slave_tabs:
231 background(slave.kernel_manager.stop_channels)
231 background(slave.kernel_manager.stop_channels)
232 self.tab_widget.removeTab(self.tab_widget.indexOf(slave))
232 self.tab_widget.removeTab(self.tab_widget.indexOf(slave))
233 kernel_manager.shutdown_kernel()
233 kernel_manager.shutdown_kernel()
234 background(kernel_manager.stop_channels)
234 background(kernel_manager.stop_channels)
235
235
236 self.update_tab_bar_visibility()
236 self.update_tab_bar_visibility()
237
237
238 def add_tab_with_frontend(self,frontend,name=None):
238 def add_tab_with_frontend(self,frontend,name=None):
239 """ insert a tab with a given frontend in the tab bar, and give it a name
239 """ insert a tab with a given frontend in the tab bar, and give it a name
240
240
241 """
241 """
242 if not name:
242 if not name:
243 name = 'kernel %i' % self.next_kernel_id
243 name = 'kernel %i' % self.next_kernel_id
244 self.tab_widget.addTab(frontend,name)
244 self.tab_widget.addTab(frontend,name)
245 self.update_tab_bar_visibility()
245 self.update_tab_bar_visibility()
246 self.make_frontend_visible(frontend)
246 self.make_frontend_visible(frontend)
247 frontend.exit_requested.connect(self.close_tab)
247 frontend.exit_requested.connect(self.close_tab)
248
248
249 def next_tab(self):
249 def next_tab(self):
250 self.tab_widget.setCurrentIndex((self.tab_widget.currentIndex()+1))
250 self.tab_widget.setCurrentIndex((self.tab_widget.currentIndex()+1))
251
251
252 def prev_tab(self):
252 def prev_tab(self):
253 self.tab_widget.setCurrentIndex((self.tab_widget.currentIndex()-1))
253 self.tab_widget.setCurrentIndex((self.tab_widget.currentIndex()-1))
254
254
255 def make_frontend_visible(self,frontend):
255 def make_frontend_visible(self,frontend):
256 widget_index=self.tab_widget.indexOf(frontend)
256 widget_index=self.tab_widget.indexOf(frontend)
257 if widget_index > 0 :
257 if widget_index > 0 :
258 self.tab_widget.setCurrentIndex(widget_index)
258 self.tab_widget.setCurrentIndex(widget_index)
259
259
260 def find_master_tab(self,tab,as_list=False):
260 def find_master_tab(self,tab,as_list=False):
261 """
261 """
262 Try to return the frontend that own the kernel attached to the given widget/tab.
262 Try to return the frontend that own the kernel attached to the given widget/tab.
263
263
264 Only find frontend owed by the current application. Selection
264 Only find frontend owed by the current application. Selection
265 based on port of the kernel, might be inacurate if several kernel
265 based on port of the kernel, might be inacurate if several kernel
266 on different ip use same port number.
266 on different ip use same port number.
267
267
268 This fonction does the conversion tabNumber/widget if needed.
268 This fonction does the conversion tabNumber/widget if needed.
269 Might return None if no master widget (non local kernel)
269 Might return None if no master widget (non local kernel)
270 Will crash IPython if more than 1 masterWidget
270 Will crash IPython if more than 1 masterWidget
271
271
272 When asList set to True, always return a list of widget(s) owning
272 When asList set to True, always return a list of widget(s) owning
273 the kernel. The list might be empty or containing several Widget.
273 the kernel. The list might be empty or containing several Widget.
274 """
274 """
275
275
276 #convert from/to int/richIpythonWidget if needed
276 #convert from/to int/richIpythonWidget if needed
277 if isinstance(tab, int):
277 if isinstance(tab, int):
278 tab = self.tab_widget.widget(tab)
278 tab = self.tab_widget.widget(tab)
279 km=tab.kernel_manager
279 km=tab.kernel_manager
280
280
281 #build list of all widgets
281 #build list of all widgets
282 widget_list = [self.tab_widget.widget(i) for i in range(self.tab_widget.count())]
282 widget_list = [self.tab_widget.widget(i) for i in range(self.tab_widget.count())]
283
283
284 # widget that are candidate to be the owner of the kernel does have all the same port of the curent widget
284 # widget that are candidate to be the owner of the kernel does have all the same port of the curent widget
285 # And should have a _may_close attribute
285 # And should have a _may_close attribute
286 filtered_widget_list = [ widget for widget in widget_list if
286 filtered_widget_list = [ widget for widget in widget_list if
287 widget.kernel_manager.connection_file == km.connection_file and
287 widget.kernel_manager.connection_file == km.connection_file and
288 hasattr(widget,'_may_close') ]
288 hasattr(widget,'_may_close') ]
289 # the master widget is the one that may close the kernel
289 # the master widget is the one that may close the kernel
290 master_widget= [ widget for widget in filtered_widget_list if widget._may_close]
290 master_widget= [ widget for widget in filtered_widget_list if widget._may_close]
291 if as_list:
291 if as_list:
292 return master_widget
292 return master_widget
293 assert(len(master_widget)<=1 )
293 assert(len(master_widget)<=1 )
294 if len(master_widget)==0:
294 if len(master_widget)==0:
295 return None
295 return None
296
296
297 return master_widget[0]
297 return master_widget[0]
298
298
299 def find_slave_widgets(self,tab):
299 def find_slave_widgets(self,tab):
300 """return all the frontends that do not own the kernel attached to the given widget/tab.
300 """return all the frontends that do not own the kernel attached to the given widget/tab.
301
301
302 Only find frontends owned by the current application. Selection
302 Only find frontends owned by the current application. Selection
303 based on connection file of the kernel.
303 based on connection file of the kernel.
304
304
305 This function does the conversion tabNumber/widget if needed.
305 This function does the conversion tabNumber/widget if needed.
306 """
306 """
307 #convert from/to int/richIpythonWidget if needed
307 #convert from/to int/richIpythonWidget if needed
308 if isinstance(tab, int):
308 if isinstance(tab, int):
309 tab = self.tab_widget.widget(tab)
309 tab = self.tab_widget.widget(tab)
310 km=tab.kernel_manager
310 km=tab.kernel_manager
311
311
312 #build list of all widgets
312 #build list of all widgets
313 widget_list = [self.tab_widget.widget(i) for i in range(self.tab_widget.count())]
313 widget_list = [self.tab_widget.widget(i) for i in range(self.tab_widget.count())]
314
314
315 # widget that are candidate not to be the owner of the kernel does have all the same port of the curent widget
315 # widget that are candidate not to be the owner of the kernel does have all the same port of the curent widget
316 filtered_widget_list = ( widget for widget in widget_list if
316 filtered_widget_list = ( widget for widget in widget_list if
317 widget.kernel_manager.connection_file == km.connection_file)
317 widget.kernel_manager.connection_file == km.connection_file)
318 # Get a list of all widget owning the same kernel and removed it from
318 # Get a list of all widget owning the same kernel and removed it from
319 # the previous cadidate. (better using sets ?)
319 # the previous cadidate. (better using sets ?)
320 master_widget_list = self.find_master_tab(tab, as_list=True)
320 master_widget_list = self.find_master_tab(tab, as_list=True)
321 slave_list = [widget for widget in filtered_widget_list if widget not in master_widget_list]
321 slave_list = [widget for widget in filtered_widget_list if widget not in master_widget_list]
322
322
323 return slave_list
323 return slave_list
324
324
325 # Populate the menu bar with common actions and shortcuts
325 # Populate the menu bar with common actions and shortcuts
326 def add_menu_action(self, menu, action, defer_shortcut=False):
326 def add_menu_action(self, menu, action, defer_shortcut=False):
327 """Add action to menu as well as self
327 """Add action to menu as well as self
328
328
329 So that when the menu bar is invisible, its actions are still available.
329 So that when the menu bar is invisible, its actions are still available.
330
330
331 If defer_shortcut is True, set the shortcut context to widget-only,
331 If defer_shortcut is True, set the shortcut context to widget-only,
332 where it will avoid conflict with shortcuts already bound to the
332 where it will avoid conflict with shortcuts already bound to the
333 widgets themselves.
333 widgets themselves.
334 """
334 """
335 menu.addAction(action)
335 menu.addAction(action)
336 self.addAction(action)
336 self.addAction(action)
337
337
338 if defer_shortcut:
338 if defer_shortcut:
339 action.setShortcutContext(QtCore.Qt.WidgetShortcut)
339 action.setShortcutContext(QtCore.Qt.WidgetShortcut)
340
340
341 def init_menu_bar(self):
341 def init_menu_bar(self):
342 #create menu in the order they should appear in the menu bar
342 #create menu in the order they should appear in the menu bar
343 self.init_file_menu()
343 self.init_file_menu()
344 self.init_edit_menu()
344 self.init_edit_menu()
345 self.init_view_menu()
345 self.init_view_menu()
346 self.init_kernel_menu()
346 self.init_kernel_menu()
347 self.init_magic_menu()
347 self.init_magic_menu()
348 self.init_window_menu()
348 self.init_window_menu()
349 self.init_help_menu()
349 self.init_help_menu()
350
350
351 def init_file_menu(self):
351 def init_file_menu(self):
352 self.file_menu = self.menuBar().addMenu("&File")
352 self.file_menu = self.menuBar().addMenu("&File")
353
353
354 self.new_kernel_tab_act = QtGui.QAction("New Tab with &New kernel",
354 self.new_kernel_tab_act = QtGui.QAction("New Tab with &New kernel",
355 self,
355 self,
356 shortcut="Ctrl+T",
356 shortcut="Ctrl+T",
357 triggered=self.create_tab_with_new_frontend)
357 triggered=self.create_tab_with_new_frontend)
358 self.add_menu_action(self.file_menu, self.new_kernel_tab_act)
358 self.add_menu_action(self.file_menu, self.new_kernel_tab_act)
359
359
360 self.slave_kernel_tab_act = QtGui.QAction("New Tab with Sa&me kernel",
360 self.slave_kernel_tab_act = QtGui.QAction("New Tab with Sa&me kernel",
361 self,
361 self,
362 shortcut="Ctrl+Shift+T",
362 shortcut="Ctrl+Shift+T",
363 triggered=self.create_tab_with_current_kernel)
363 triggered=self.create_tab_with_current_kernel)
364 self.add_menu_action(self.file_menu, self.slave_kernel_tab_act)
364 self.add_menu_action(self.file_menu, self.slave_kernel_tab_act)
365
365
366 self.file_menu.addSeparator()
366 self.file_menu.addSeparator()
367
367
368 self.close_action=QtGui.QAction("&Close Tab",
368 self.close_action=QtGui.QAction("&Close Tab",
369 self,
369 self,
370 shortcut=QtGui.QKeySequence.Close,
370 shortcut=QtGui.QKeySequence.Close,
371 triggered=self.close_active_frontend
371 triggered=self.close_active_frontend
372 )
372 )
373 self.add_menu_action(self.file_menu, self.close_action)
373 self.add_menu_action(self.file_menu, self.close_action)
374
374
375 self.export_action=QtGui.QAction("&Save to HTML/XHTML",
375 self.export_action=QtGui.QAction("&Save to HTML/XHTML",
376 self,
376 self,
377 shortcut=QtGui.QKeySequence.Save,
377 shortcut=QtGui.QKeySequence.Save,
378 triggered=self.export_action_active_frontend
378 triggered=self.export_action_active_frontend
379 )
379 )
380 self.add_menu_action(self.file_menu, self.export_action, True)
380 self.add_menu_action(self.file_menu, self.export_action, True)
381
381
382 self.file_menu.addSeparator()
382 self.file_menu.addSeparator()
383
383
384 printkey = QtGui.QKeySequence(QtGui.QKeySequence.Print)
384 printkey = QtGui.QKeySequence(QtGui.QKeySequence.Print)
385 if printkey.matches("Ctrl+P") and sys.platform != 'darwin':
385 if printkey.matches("Ctrl+P") and sys.platform != 'darwin':
386 # Only override the default if there is a collision.
386 # Only override the default if there is a collision.
387 # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
387 # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
388 printkey = "Ctrl+Shift+P"
388 printkey = "Ctrl+Shift+P"
389 self.print_action = QtGui.QAction("&Print",
389 self.print_action = QtGui.QAction("&Print",
390 self,
390 self,
391 shortcut=printkey,
391 shortcut=printkey,
392 triggered=self.print_action_active_frontend)
392 triggered=self.print_action_active_frontend)
393 self.add_menu_action(self.file_menu, self.print_action, True)
393 self.add_menu_action(self.file_menu, self.print_action, True)
394
394
395 if sys.platform != 'darwin':
395 if sys.platform != 'darwin':
396 # OSX always has Quit in the Application menu, only add it
396 # OSX always has Quit in the Application menu, only add it
397 # to the File menu elsewhere.
397 # to the File menu elsewhere.
398
398
399 self.file_menu.addSeparator()
399 self.file_menu.addSeparator()
400
400
401 self.quit_action = QtGui.QAction("&Quit",
401 self.quit_action = QtGui.QAction("&Quit",
402 self,
402 self,
403 shortcut=QtGui.QKeySequence.Quit,
403 shortcut=QtGui.QKeySequence.Quit,
404 triggered=self.close,
404 triggered=self.close,
405 )
405 )
406 self.add_menu_action(self.file_menu, self.quit_action)
406 self.add_menu_action(self.file_menu, self.quit_action)
407
407
408
408
409 def init_edit_menu(self):
409 def init_edit_menu(self):
410 self.edit_menu = self.menuBar().addMenu("&Edit")
410 self.edit_menu = self.menuBar().addMenu("&Edit")
411
411
412 self.undo_action = QtGui.QAction("&Undo",
412 self.undo_action = QtGui.QAction("&Undo",
413 self,
413 self,
414 shortcut=QtGui.QKeySequence.Undo,
414 shortcut=QtGui.QKeySequence.Undo,
415 statusTip="Undo last action if possible",
415 statusTip="Undo last action if possible",
416 triggered=self.undo_active_frontend
416 triggered=self.undo_active_frontend
417 )
417 )
418 self.add_menu_action(self.edit_menu, self.undo_action)
418 self.add_menu_action(self.edit_menu, self.undo_action)
419
419
420 self.redo_action = QtGui.QAction("&Redo",
420 self.redo_action = QtGui.QAction("&Redo",
421 self,
421 self,
422 shortcut=QtGui.QKeySequence.Redo,
422 shortcut=QtGui.QKeySequence.Redo,
423 statusTip="Redo last action if possible",
423 statusTip="Redo last action if possible",
424 triggered=self.redo_active_frontend)
424 triggered=self.redo_active_frontend)
425 self.add_menu_action(self.edit_menu, self.redo_action)
425 self.add_menu_action(self.edit_menu, self.redo_action)
426
426
427 self.edit_menu.addSeparator()
427 self.edit_menu.addSeparator()
428
428
429 self.cut_action = QtGui.QAction("&Cut",
429 self.cut_action = QtGui.QAction("&Cut",
430 self,
430 self,
431 shortcut=QtGui.QKeySequence.Cut,
431 shortcut=QtGui.QKeySequence.Cut,
432 triggered=self.cut_active_frontend
432 triggered=self.cut_active_frontend
433 )
433 )
434 self.add_menu_action(self.edit_menu, self.cut_action, True)
434 self.add_menu_action(self.edit_menu, self.cut_action, True)
435
435
436 self.copy_action = QtGui.QAction("&Copy",
436 self.copy_action = QtGui.QAction("&Copy",
437 self,
437 self,
438 shortcut=QtGui.QKeySequence.Copy,
438 shortcut=QtGui.QKeySequence.Copy,
439 triggered=self.copy_active_frontend
439 triggered=self.copy_active_frontend
440 )
440 )
441 self.add_menu_action(self.edit_menu, self.copy_action, True)
441 self.add_menu_action(self.edit_menu, self.copy_action, True)
442
442
443 self.copy_raw_action = QtGui.QAction("Copy (&Raw Text)",
443 self.copy_raw_action = QtGui.QAction("Copy (&Raw Text)",
444 self,
444 self,
445 shortcut="Ctrl+Shift+C",
445 shortcut="Ctrl+Shift+C",
446 triggered=self.copy_raw_active_frontend
446 triggered=self.copy_raw_active_frontend
447 )
447 )
448 self.add_menu_action(self.edit_menu, self.copy_raw_action, True)
448 self.add_menu_action(self.edit_menu, self.copy_raw_action, True)
449
449
450 self.paste_action = QtGui.QAction("&Paste",
450 self.paste_action = QtGui.QAction("&Paste",
451 self,
451 self,
452 shortcut=QtGui.QKeySequence.Paste,
452 shortcut=QtGui.QKeySequence.Paste,
453 triggered=self.paste_active_frontend
453 triggered=self.paste_active_frontend
454 )
454 )
455 self.add_menu_action(self.edit_menu, self.paste_action, True)
455 self.add_menu_action(self.edit_menu, self.paste_action, True)
456
456
457 self.edit_menu.addSeparator()
457 self.edit_menu.addSeparator()
458
458
459 selectall = QtGui.QKeySequence(QtGui.QKeySequence.SelectAll)
459 selectall = QtGui.QKeySequence(QtGui.QKeySequence.SelectAll)
460 if selectall.matches("Ctrl+A") and sys.platform != 'darwin':
460 if selectall.matches("Ctrl+A") and sys.platform != 'darwin':
461 # Only override the default if there is a collision.
461 # Only override the default if there is a collision.
462 # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
462 # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
463 selectall = "Ctrl+Shift+A"
463 selectall = "Ctrl+Shift+A"
464 self.select_all_action = QtGui.QAction("Select &All",
464 self.select_all_action = QtGui.QAction("Select &All",
465 self,
465 self,
466 shortcut=selectall,
466 shortcut=selectall,
467 triggered=self.select_all_active_frontend
467 triggered=self.select_all_active_frontend
468 )
468 )
469 self.add_menu_action(self.edit_menu, self.select_all_action, True)
469 self.add_menu_action(self.edit_menu, self.select_all_action, True)
470
470
471
471
472 def init_view_menu(self):
472 def init_view_menu(self):
473 self.view_menu = self.menuBar().addMenu("&View")
473 self.view_menu = self.menuBar().addMenu("&View")
474
474
475 if sys.platform != 'darwin':
475 if sys.platform != 'darwin':
476 # disable on OSX, where there is always a menu bar
476 # disable on OSX, where there is always a menu bar
477 self.toggle_menu_bar_act = QtGui.QAction("Toggle &Menu Bar",
477 self.toggle_menu_bar_act = QtGui.QAction("Toggle &Menu Bar",
478 self,
478 self,
479 shortcut="Ctrl+Shift+M",
479 shortcut="Ctrl+Shift+M",
480 statusTip="Toggle visibility of menubar",
480 statusTip="Toggle visibility of menubar",
481 triggered=self.toggle_menu_bar)
481 triggered=self.toggle_menu_bar)
482 self.add_menu_action(self.view_menu, self.toggle_menu_bar_act)
482 self.add_menu_action(self.view_menu, self.toggle_menu_bar_act)
483
483
484 fs_key = "Ctrl+Meta+F" if sys.platform == 'darwin' else "F11"
484 fs_key = "Ctrl+Meta+F" if sys.platform == 'darwin' else "F11"
485 self.full_screen_act = QtGui.QAction("&Full Screen",
485 self.full_screen_act = QtGui.QAction("&Full Screen",
486 self,
486 self,
487 shortcut=fs_key,
487 shortcut=fs_key,
488 statusTip="Toggle between Fullscreen and Normal Size",
488 statusTip="Toggle between Fullscreen and Normal Size",
489 triggered=self.toggleFullScreen)
489 triggered=self.toggleFullScreen)
490 self.add_menu_action(self.view_menu, self.full_screen_act)
490 self.add_menu_action(self.view_menu, self.full_screen_act)
491
491
492 self.view_menu.addSeparator()
492 self.view_menu.addSeparator()
493
493
494 self.increase_font_size = QtGui.QAction("Zoom &In",
494 self.increase_font_size = QtGui.QAction("Zoom &In",
495 self,
495 self,
496 shortcut=QtGui.QKeySequence.ZoomIn,
496 shortcut=QtGui.QKeySequence.ZoomIn,
497 triggered=self.increase_font_size_active_frontend
497 triggered=self.increase_font_size_active_frontend
498 )
498 )
499 self.add_menu_action(self.view_menu, self.increase_font_size, True)
499 self.add_menu_action(self.view_menu, self.increase_font_size, True)
500
500
501 self.decrease_font_size = QtGui.QAction("Zoom &Out",
501 self.decrease_font_size = QtGui.QAction("Zoom &Out",
502 self,
502 self,
503 shortcut=QtGui.QKeySequence.ZoomOut,
503 shortcut=QtGui.QKeySequence.ZoomOut,
504 triggered=self.decrease_font_size_active_frontend
504 triggered=self.decrease_font_size_active_frontend
505 )
505 )
506 self.add_menu_action(self.view_menu, self.decrease_font_size, True)
506 self.add_menu_action(self.view_menu, self.decrease_font_size, True)
507
507
508 self.reset_font_size = QtGui.QAction("Zoom &Reset",
508 self.reset_font_size = QtGui.QAction("Zoom &Reset",
509 self,
509 self,
510 shortcut="Ctrl+0",
510 shortcut="Ctrl+0",
511 triggered=self.reset_font_size_active_frontend
511 triggered=self.reset_font_size_active_frontend
512 )
512 )
513 self.add_menu_action(self.view_menu, self.reset_font_size, True)
513 self.add_menu_action(self.view_menu, self.reset_font_size, True)
514
514
515 self.view_menu.addSeparator()
515 self.view_menu.addSeparator()
516
516
517 self.clear_action = QtGui.QAction("&Clear Screen",
517 self.clear_action = QtGui.QAction("&Clear Screen",
518 self,
518 self,
519 shortcut='Ctrl+L',
519 shortcut='Ctrl+L',
520 statusTip="Clear the console",
520 statusTip="Clear the console",
521 triggered=self.clear_magic_active_frontend)
521 triggered=self.clear_magic_active_frontend)
522 self.add_menu_action(self.view_menu, self.clear_action)
522 self.add_menu_action(self.view_menu, self.clear_action)
523
523
524 def init_kernel_menu(self):
524 def init_kernel_menu(self):
525 self.kernel_menu = self.menuBar().addMenu("&Kernel")
525 self.kernel_menu = self.menuBar().addMenu("&Kernel")
526 # Qt on OSX maps Ctrl to Cmd, and Meta to Ctrl
526 # Qt on OSX maps Ctrl to Cmd, and Meta to Ctrl
527 # keep the signal shortcuts to ctrl, rather than
527 # keep the signal shortcuts to ctrl, rather than
528 # platform-default like we do elsewhere.
528 # platform-default like we do elsewhere.
529
529
530 ctrl = "Meta" if sys.platform == 'darwin' else "Ctrl"
530 ctrl = "Meta" if sys.platform == 'darwin' else "Ctrl"
531
531
532 self.interrupt_kernel_action = QtGui.QAction("Interrupt current Kernel",
532 self.interrupt_kernel_action = QtGui.QAction("Interrupt current Kernel",
533 self,
533 self,
534 triggered=self.interrupt_kernel_active_frontend,
534 triggered=self.interrupt_kernel_active_frontend,
535 shortcut=ctrl+"+C",
535 shortcut=ctrl+"+C",
536 )
536 )
537 self.add_menu_action(self.kernel_menu, self.interrupt_kernel_action)
537 self.add_menu_action(self.kernel_menu, self.interrupt_kernel_action)
538
538
539 self.restart_kernel_action = QtGui.QAction("Restart current Kernel",
539 self.restart_kernel_action = QtGui.QAction("Restart current Kernel",
540 self,
540 self,
541 triggered=self.restart_kernel_active_frontend,
541 triggered=self.restart_kernel_active_frontend,
542 shortcut=ctrl+"+.",
542 shortcut=ctrl+"+.",
543 )
543 )
544 self.add_menu_action(self.kernel_menu, self.restart_kernel_action)
544 self.add_menu_action(self.kernel_menu, self.restart_kernel_action)
545
545
546 self.kernel_menu.addSeparator()
546 self.kernel_menu.addSeparator()
547
547
548 def _make_dynamic_magic(self,magic):
548 def _make_dynamic_magic(self,magic):
549 """Return a function `fun` that will execute `magic` on active frontend.
549 """Return a function `fun` that will execute `magic` on active frontend.
550
550
551 Parameters
551 Parameters
552 ----------
552 ----------
553 magic : string
553 magic : string
554 string that will be executed as is when the returned function is called
554 string that will be executed as is when the returned function is called
555
555
556 Returns
556 Returns
557 -------
557 -------
558 fun : function
558 fun : function
559 function with no parameters, when called will execute `magic` on the
559 function with no parameters, when called will execute `magic` on the
560 current active frontend at call time
560 current active frontend at call time
561
561
562 See Also
562 See Also
563 --------
563 --------
564 populate_all_magic_menu : generate the "All Magics..." menu
564 populate_all_magic_menu : generate the "All Magics..." menu
565
565
566 Notes
566 Notes
567 -----
567 -----
568 `fun` execute `magic` an active frontend at the moment it is triggerd,
568 `fun` execute `magic` an active frontend at the moment it is triggerd,
569 not the active frontend at the moment it has been created.
569 not the active frontend at the moment it has been created.
570
570
571 This function is mostly used to create the "All Magics..." Menu at run time.
571 This function is mostly used to create the "All Magics..." Menu at run time.
572 """
572 """
573 # need to level nested function to be sure to past magic
573 # need to level nested function to be sure to past magic
574 # on active frontend **at run time**.
574 # on active frontend **at run time**.
575 def inner_dynamic_magic():
575 def inner_dynamic_magic():
576 self.active_frontend.execute(magic)
576 self.active_frontend.execute(magic)
577 inner_dynamic_magic.__name__ = "dynamics_magic_s"
577 inner_dynamic_magic.__name__ = "dynamics_magic_s"
578 return inner_dynamic_magic
578 return inner_dynamic_magic
579
579
580 def populate_all_magic_menu(self, listofmagic=None):
580 def populate_all_magic_menu(self, listofmagic=None):
581 """Clean "All Magics..." menu and repopulate it with `listofmagic`
581 """Clean "All Magics..." menu and repopulate it with `listofmagic`
582
582
583 Parameters
583 Parameters
584 ----------
584 ----------
585 listofmagic : string,
585 listofmagic : string,
586 repr() of a list of strings, send back by the kernel
586 repr() of a list of strings, send back by the kernel
587
587
588 Notes
588 Notes
589 -----
589 -----
590 `listofmagic`is a repr() of list because it is fed with the result of
590 `listofmagic`is a repr() of list because it is fed with the result of
591 a 'user_expression'
591 a 'user_expression'
592 """
592 """
593 alm_magic_menu = self.all_magic_menu
593 alm_magic_menu = self.all_magic_menu
594 alm_magic_menu.clear()
594 alm_magic_menu.clear()
595
595
596 # list of protected magic that don't like to be called without argument
596 # list of protected magic that don't like to be called without argument
597 # append '?' to the end to print the docstring when called from the menu
597 # append '?' to the end to print the docstring when called from the menu
598 protected_magic = set(["more","less","load_ext","pycat","loadpy","save"])
598 protected_magic = set(["more","less","load_ext","pycat","loadpy","save"])
599 magics=re.findall('\w+', listofmagic)
599 magics=re.findall('\w+', listofmagic)
600 for magic in magics:
600 for magic in magics:
601 if magic in protected_magic:
601 if magic in protected_magic:
602 pmagic = '%s%s%s'%('%',magic,'?')
602 pmagic = '%s%s%s'%('%',magic,'?')
603 else:
603 else:
604 pmagic = '%s%s'%('%',magic)
604 pmagic = '%s%s'%('%',magic)
605 xaction = QtGui.QAction(pmagic,
605 xaction = QtGui.QAction(pmagic,
606 self,
606 self,
607 triggered=self._make_dynamic_magic(pmagic)
607 triggered=self._make_dynamic_magic(pmagic)
608 )
608 )
609 alm_magic_menu.addAction(xaction)
609 alm_magic_menu.addAction(xaction)
610
610
611 def update_all_magic_menu(self):
611 def update_all_magic_menu(self):
612 """ Update the list on magic in the "All Magics..." Menu
612 """ Update the list on magic in the "All Magics..." Menu
613
613
614 Request the kernel with the list of availlable magic and populate the
614 Request the kernel with the list of availlable magic and populate the
615 menu with the list received back
615 menu with the list received back
616
616
617 """
617 """
618 # first define a callback which will get the list of all magic and put it in the menu.
618 # first define a callback which will get the list of all magic and put it in the menu.
619 self.active_frontend._silent_exec_callback('get_ipython().lsmagic()', self.populate_all_magic_menu)
619 self.active_frontend._silent_exec_callback('get_ipython().lsmagic()', self.populate_all_magic_menu)
620
620
621 def init_magic_menu(self):
621 def init_magic_menu(self):
622 self.magic_menu = self.menuBar().addMenu("&Magic")
622 self.magic_menu = self.menuBar().addMenu("&Magic")
623 self.all_magic_menu = self.magic_menu.addMenu("&All Magics")
623 self.all_magic_menu = self.magic_menu.addMenu("&All Magics")
624
624
625 # This action should usually not appear as it will be cleared when menu
625 # This action should usually not appear as it will be cleared when menu
626 # is updated at first kernel response. Though, it is necessary when
626 # is updated at first kernel response. Though, it is necessary when
627 # connecting through X-forwarding, as in this case, the menu is not
627 # connecting through X-forwarding, as in this case, the menu is not
628 # auto updated, SO DO NOT DELETE.
628 # auto updated, SO DO NOT DELETE.
629 self.pop = QtGui.QAction("&Update All Magic Menu ",
629 self.pop = QtGui.QAction("&Update All Magic Menu ",
630 self, triggered=self.update_all_magic_menu)
630 self, triggered=self.update_all_magic_menu)
631 self.add_menu_action(self.all_magic_menu, self.pop)
631 self.add_menu_action(self.all_magic_menu, self.pop)
632 # we need to populate the 'Magic Menu' once the kernel has answer at
633 # least once let's do it immedialy, but it's assured to works
634 self.pop.trigger()
632
635
633 self.reset_action = QtGui.QAction("&Reset",
636 self.reset_action = QtGui.QAction("&Reset",
634 self,
637 self,
635 statusTip="Clear all varible from workspace",
638 statusTip="Clear all varible from workspace",
636 triggered=self.reset_magic_active_frontend)
639 triggered=self.reset_magic_active_frontend)
637 self.add_menu_action(self.magic_menu, self.reset_action)
640 self.add_menu_action(self.magic_menu, self.reset_action)
638
641
639 self.history_action = QtGui.QAction("&History",
642 self.history_action = QtGui.QAction("&History",
640 self,
643 self,
641 statusTip="show command history",
644 statusTip="show command history",
642 triggered=self.history_magic_active_frontend)
645 triggered=self.history_magic_active_frontend)
643 self.add_menu_action(self.magic_menu, self.history_action)
646 self.add_menu_action(self.magic_menu, self.history_action)
644
647
645 self.save_action = QtGui.QAction("E&xport History ",
648 self.save_action = QtGui.QAction("E&xport History ",
646 self,
649 self,
647 statusTip="Export History as Python File",
650 statusTip="Export History as Python File",
648 triggered=self.save_magic_active_frontend)
651 triggered=self.save_magic_active_frontend)
649 self.add_menu_action(self.magic_menu, self.save_action)
652 self.add_menu_action(self.magic_menu, self.save_action)
650
653
651 self.who_action = QtGui.QAction("&Who",
654 self.who_action = QtGui.QAction("&Who",
652 self,
655 self,
653 statusTip="List interactive variable",
656 statusTip="List interactive variable",
654 triggered=self.who_magic_active_frontend)
657 triggered=self.who_magic_active_frontend)
655 self.add_menu_action(self.magic_menu, self.who_action)
658 self.add_menu_action(self.magic_menu, self.who_action)
656
659
657 self.who_ls_action = QtGui.QAction("Wh&o ls",
660 self.who_ls_action = QtGui.QAction("Wh&o ls",
658 self,
661 self,
659 statusTip="Return a list of interactive variable",
662 statusTip="Return a list of interactive variable",
660 triggered=self.who_ls_magic_active_frontend)
663 triggered=self.who_ls_magic_active_frontend)
661 self.add_menu_action(self.magic_menu, self.who_ls_action)
664 self.add_menu_action(self.magic_menu, self.who_ls_action)
662
665
663 self.whos_action = QtGui.QAction("Who&s",
666 self.whos_action = QtGui.QAction("Who&s",
664 self,
667 self,
665 statusTip="List interactive variable with detail",
668 statusTip="List interactive variable with detail",
666 triggered=self.whos_magic_active_frontend)
669 triggered=self.whos_magic_active_frontend)
667 self.add_menu_action(self.magic_menu, self.whos_action)
670 self.add_menu_action(self.magic_menu, self.whos_action)
668
671
669 def init_window_menu(self):
672 def init_window_menu(self):
670 self.window_menu = self.menuBar().addMenu("&Window")
673 self.window_menu = self.menuBar().addMenu("&Window")
671 if sys.platform == 'darwin':
674 if sys.platform == 'darwin':
672 # add min/maximize actions to OSX, which lacks default bindings.
675 # add min/maximize actions to OSX, which lacks default bindings.
673 self.minimizeAct = QtGui.QAction("Mini&mize",
676 self.minimizeAct = QtGui.QAction("Mini&mize",
674 self,
677 self,
675 shortcut="Ctrl+m",
678 shortcut="Ctrl+m",
676 statusTip="Minimize the window/Restore Normal Size",
679 statusTip="Minimize the window/Restore Normal Size",
677 triggered=self.toggleMinimized)
680 triggered=self.toggleMinimized)
678 # maximize is called 'Zoom' on OSX for some reason
681 # maximize is called 'Zoom' on OSX for some reason
679 self.maximizeAct = QtGui.QAction("&Zoom",
682 self.maximizeAct = QtGui.QAction("&Zoom",
680 self,
683 self,
681 shortcut="Ctrl+Shift+M",
684 shortcut="Ctrl+Shift+M",
682 statusTip="Maximize the window/Restore Normal Size",
685 statusTip="Maximize the window/Restore Normal Size",
683 triggered=self.toggleMaximized)
686 triggered=self.toggleMaximized)
684
687
685 self.add_menu_action(self.window_menu, self.minimizeAct)
688 self.add_menu_action(self.window_menu, self.minimizeAct)
686 self.add_menu_action(self.window_menu, self.maximizeAct)
689 self.add_menu_action(self.window_menu, self.maximizeAct)
687 self.window_menu.addSeparator()
690 self.window_menu.addSeparator()
688
691
689 prev_key = "Ctrl+Shift+Left" if sys.platform == 'darwin' else "Ctrl+PgUp"
692 prev_key = "Ctrl+Shift+Left" if sys.platform == 'darwin' else "Ctrl+PgUp"
690 self.prev_tab_act = QtGui.QAction("Pre&vious Tab",
693 self.prev_tab_act = QtGui.QAction("Pre&vious Tab",
691 self,
694 self,
692 shortcut=prev_key,
695 shortcut=prev_key,
693 statusTip="Select previous tab",
696 statusTip="Select previous tab",
694 triggered=self.prev_tab)
697 triggered=self.prev_tab)
695 self.add_menu_action(self.window_menu, self.prev_tab_act)
698 self.add_menu_action(self.window_menu, self.prev_tab_act)
696
699
697 next_key = "Ctrl+Shift+Right" if sys.platform == 'darwin' else "Ctrl+PgDown"
700 next_key = "Ctrl+Shift+Right" if sys.platform == 'darwin' else "Ctrl+PgDown"
698 self.next_tab_act = QtGui.QAction("Ne&xt Tab",
701 self.next_tab_act = QtGui.QAction("Ne&xt Tab",
699 self,
702 self,
700 shortcut=next_key,
703 shortcut=next_key,
701 statusTip="Select next tab",
704 statusTip="Select next tab",
702 triggered=self.next_tab)
705 triggered=self.next_tab)
703 self.add_menu_action(self.window_menu, self.next_tab_act)
706 self.add_menu_action(self.window_menu, self.next_tab_act)
704
707
705 def init_help_menu(self):
708 def init_help_menu(self):
706 # please keep the Help menu in Mac Os even if empty. It will
709 # please keep the Help menu in Mac Os even if empty. It will
707 # automatically contain a search field to search inside menus and
710 # automatically contain a search field to search inside menus and
708 # please keep it spelled in English, as long as Qt Doesn't support
711 # please keep it spelled in English, as long as Qt Doesn't support
709 # a QAction.MenuRole like HelpMenuRole otherwise it will loose
712 # a QAction.MenuRole like HelpMenuRole otherwise it will loose
710 # this search field fonctionality
713 # this search field fonctionality
711
714
712 self.help_menu = self.menuBar().addMenu("&Help")
715 self.help_menu = self.menuBar().addMenu("&Help")
713
716
714
717
715 # Help Menu
718 # Help Menu
716
719
717 self.intro_active_frontend_action = QtGui.QAction("&Intro to IPython",
720 self.intro_active_frontend_action = QtGui.QAction("&Intro to IPython",
718 self,
721 self,
719 triggered=self.intro_active_frontend
722 triggered=self.intro_active_frontend
720 )
723 )
721 self.add_menu_action(self.help_menu, self.intro_active_frontend_action)
724 self.add_menu_action(self.help_menu, self.intro_active_frontend_action)
722
725
723 self.quickref_active_frontend_action = QtGui.QAction("IPython &Cheat Sheet",
726 self.quickref_active_frontend_action = QtGui.QAction("IPython &Cheat Sheet",
724 self,
727 self,
725 triggered=self.quickref_active_frontend
728 triggered=self.quickref_active_frontend
726 )
729 )
727 self.add_menu_action(self.help_menu, self.quickref_active_frontend_action)
730 self.add_menu_action(self.help_menu, self.quickref_active_frontend_action)
728
731
729 self.guiref_active_frontend_action = QtGui.QAction("&Qt Console",
732 self.guiref_active_frontend_action = QtGui.QAction("&Qt Console",
730 self,
733 self,
731 triggered=self.guiref_active_frontend
734 triggered=self.guiref_active_frontend
732 )
735 )
733 self.add_menu_action(self.help_menu, self.guiref_active_frontend_action)
736 self.add_menu_action(self.help_menu, self.guiref_active_frontend_action)
734
737
735 self.onlineHelpAct = QtGui.QAction("Open Online &Help",
738 self.onlineHelpAct = QtGui.QAction("Open Online &Help",
736 self,
739 self,
737 triggered=self._open_online_help)
740 triggered=self._open_online_help)
738 self.add_menu_action(self.help_menu, self.onlineHelpAct)
741 self.add_menu_action(self.help_menu, self.onlineHelpAct)
739
742
740 # minimize/maximize/fullscreen actions:
743 # minimize/maximize/fullscreen actions:
741
744
742 def toggle_menu_bar(self):
745 def toggle_menu_bar(self):
743 menu_bar = self.menuBar()
746 menu_bar = self.menuBar()
744 if menu_bar.isVisible():
747 if menu_bar.isVisible():
745 menu_bar.setVisible(False)
748 menu_bar.setVisible(False)
746 else:
749 else:
747 menu_bar.setVisible(True)
750 menu_bar.setVisible(True)
748
751
749 def toggleMinimized(self):
752 def toggleMinimized(self):
750 if not self.isMinimized():
753 if not self.isMinimized():
751 self.showMinimized()
754 self.showMinimized()
752 else:
755 else:
753 self.showNormal()
756 self.showNormal()
754
757
755 def _open_online_help(self):
758 def _open_online_help(self):
756 filename="http://ipython.org/ipython-doc/stable/index.html"
759 filename="http://ipython.org/ipython-doc/stable/index.html"
757 webbrowser.open(filename, new=1, autoraise=True)
760 webbrowser.open(filename, new=1, autoraise=True)
758
761
759 def toggleMaximized(self):
762 def toggleMaximized(self):
760 if not self.isMaximized():
763 if not self.isMaximized():
761 self.showMaximized()
764 self.showMaximized()
762 else:
765 else:
763 self.showNormal()
766 self.showNormal()
764
767
765 # Min/Max imizing while in full screen give a bug
768 # Min/Max imizing while in full screen give a bug
766 # when going out of full screen, at least on OSX
769 # when going out of full screen, at least on OSX
767 def toggleFullScreen(self):
770 def toggleFullScreen(self):
768 if not self.isFullScreen():
771 if not self.isFullScreen():
769 self.showFullScreen()
772 self.showFullScreen()
770 if sys.platform == 'darwin':
773 if sys.platform == 'darwin':
771 self.maximizeAct.setEnabled(False)
774 self.maximizeAct.setEnabled(False)
772 self.minimizeAct.setEnabled(False)
775 self.minimizeAct.setEnabled(False)
773 else:
776 else:
774 self.showNormal()
777 self.showNormal()
775 if sys.platform == 'darwin':
778 if sys.platform == 'darwin':
776 self.maximizeAct.setEnabled(True)
779 self.maximizeAct.setEnabled(True)
777 self.minimizeAct.setEnabled(True)
780 self.minimizeAct.setEnabled(True)
778
781
779 def close_active_frontend(self):
782 def close_active_frontend(self):
780 self.close_tab(self.active_frontend)
783 self.close_tab(self.active_frontend)
781
784
782 def restart_kernel_active_frontend(self):
785 def restart_kernel_active_frontend(self):
783 self.active_frontend.request_restart_kernel()
786 self.active_frontend.request_restart_kernel()
784
787
785 def interrupt_kernel_active_frontend(self):
788 def interrupt_kernel_active_frontend(self):
786 self.active_frontend.request_interrupt_kernel()
789 self.active_frontend.request_interrupt_kernel()
787
790
788 def cut_active_frontend(self):
791 def cut_active_frontend(self):
789 widget = self.active_frontend
792 widget = self.active_frontend
790 if widget.can_cut():
793 if widget.can_cut():
791 widget.cut()
794 widget.cut()
792
795
793 def copy_active_frontend(self):
796 def copy_active_frontend(self):
794 widget = self.active_frontend
797 widget = self.active_frontend
795 if widget.can_copy():
798 if widget.can_copy():
796 widget.copy()
799 widget.copy()
797
800
798 def copy_raw_active_frontend(self):
801 def copy_raw_active_frontend(self):
799 self.active_frontend._copy_raw_action.trigger()
802 self.active_frontend._copy_raw_action.trigger()
800
803
801 def paste_active_frontend(self):
804 def paste_active_frontend(self):
802 widget = self.active_frontend
805 widget = self.active_frontend
803 if widget.can_paste():
806 if widget.can_paste():
804 widget.paste()
807 widget.paste()
805
808
806 def undo_active_frontend(self):
809 def undo_active_frontend(self):
807 self.active_frontend.undo()
810 self.active_frontend.undo()
808
811
809 def redo_active_frontend(self):
812 def redo_active_frontend(self):
810 self.active_frontend.redo()
813 self.active_frontend.redo()
811
814
812 def reset_magic_active_frontend(self):
815 def reset_magic_active_frontend(self):
813 self.active_frontend.execute("%reset")
816 self.active_frontend.execute("%reset")
814
817
815 def history_magic_active_frontend(self):
818 def history_magic_active_frontend(self):
816 self.active_frontend.execute("%history")
819 self.active_frontend.execute("%history")
817
820
818 def save_magic_active_frontend(self):
821 def save_magic_active_frontend(self):
819 self.active_frontend.save_magic()
822 self.active_frontend.save_magic()
820
823
821 def clear_magic_active_frontend(self):
824 def clear_magic_active_frontend(self):
822 self.active_frontend.execute("%clear")
825 self.active_frontend.execute("%clear")
823
826
824 def who_magic_active_frontend(self):
827 def who_magic_active_frontend(self):
825 self.active_frontend.execute("%who")
828 self.active_frontend.execute("%who")
826
829
827 def who_ls_magic_active_frontend(self):
830 def who_ls_magic_active_frontend(self):
828 self.active_frontend.execute("%who_ls")
831 self.active_frontend.execute("%who_ls")
829
832
830 def whos_magic_active_frontend(self):
833 def whos_magic_active_frontend(self):
831 self.active_frontend.execute("%whos")
834 self.active_frontend.execute("%whos")
832
835
833 def print_action_active_frontend(self):
836 def print_action_active_frontend(self):
834 self.active_frontend.print_action.trigger()
837 self.active_frontend.print_action.trigger()
835
838
836 def export_action_active_frontend(self):
839 def export_action_active_frontend(self):
837 self.active_frontend.export_action.trigger()
840 self.active_frontend.export_action.trigger()
838
841
839 def select_all_active_frontend(self):
842 def select_all_active_frontend(self):
840 self.active_frontend.select_all_action.trigger()
843 self.active_frontend.select_all_action.trigger()
841
844
842 def increase_font_size_active_frontend(self):
845 def increase_font_size_active_frontend(self):
843 self.active_frontend.increase_font_size.trigger()
846 self.active_frontend.increase_font_size.trigger()
844
847
845 def decrease_font_size_active_frontend(self):
848 def decrease_font_size_active_frontend(self):
846 self.active_frontend.decrease_font_size.trigger()
849 self.active_frontend.decrease_font_size.trigger()
847
850
848 def reset_font_size_active_frontend(self):
851 def reset_font_size_active_frontend(self):
849 self.active_frontend.reset_font_size.trigger()
852 self.active_frontend.reset_font_size.trigger()
850
853
851 def guiref_active_frontend(self):
854 def guiref_active_frontend(self):
852 self.active_frontend.execute("%guiref")
855 self.active_frontend.execute("%guiref")
853
856
854 def intro_active_frontend(self):
857 def intro_active_frontend(self):
855 self.active_frontend.execute("?")
858 self.active_frontend.execute("?")
856
859
857 def quickref_active_frontend(self):
860 def quickref_active_frontend(self):
858 self.active_frontend.execute("%quickref")
861 self.active_frontend.execute("%quickref")
859 #---------------------------------------------------------------------------
862 #---------------------------------------------------------------------------
860 # QWidget interface
863 # QWidget interface
861 #---------------------------------------------------------------------------
864 #---------------------------------------------------------------------------
862
865
863 def closeEvent(self, event):
866 def closeEvent(self, event):
864 """ Forward the close event to every tabs contained by the windows
867 """ Forward the close event to every tabs contained by the windows
865 """
868 """
866 if self.tab_widget.count() == 0:
869 if self.tab_widget.count() == 0:
867 # no tabs, just close
870 # no tabs, just close
868 event.accept()
871 event.accept()
869 return
872 return
870 # Do Not loop on the widget count as it change while closing
873 # Do Not loop on the widget count as it change while closing
871 title = self.window().windowTitle()
874 title = self.window().windowTitle()
872 cancel = QtGui.QMessageBox.Cancel
875 cancel = QtGui.QMessageBox.Cancel
873 okay = QtGui.QMessageBox.Ok
876 okay = QtGui.QMessageBox.Ok
874
877
875 if self.confirm_exit:
878 if self.confirm_exit:
876 if self.tab_widget.count() > 1:
879 if self.tab_widget.count() > 1:
877 msg = "Close all tabs, stop all kernels, and Quit?"
880 msg = "Close all tabs, stop all kernels, and Quit?"
878 else:
881 else:
879 msg = "Close console, stop kernel, and Quit?"
882 msg = "Close console, stop kernel, and Quit?"
880 info = "Kernels not started here (e.g. notebooks) will be left alone."
883 info = "Kernels not started here (e.g. notebooks) will be left alone."
881 closeall = QtGui.QPushButton("&Yes, quit everything", self)
884 closeall = QtGui.QPushButton("&Yes, quit everything", self)
882 closeall.setShortcut('Y')
885 closeall.setShortcut('Y')
883 box = QtGui.QMessageBox(QtGui.QMessageBox.Question,
886 box = QtGui.QMessageBox(QtGui.QMessageBox.Question,
884 title, msg)
887 title, msg)
885 box.setInformativeText(info)
888 box.setInformativeText(info)
886 box.addButton(cancel)
889 box.addButton(cancel)
887 box.addButton(closeall, QtGui.QMessageBox.YesRole)
890 box.addButton(closeall, QtGui.QMessageBox.YesRole)
888 box.setDefaultButton(closeall)
891 box.setDefaultButton(closeall)
889 box.setEscapeButton(cancel)
892 box.setEscapeButton(cancel)
890 pixmap = QtGui.QPixmap(self._app.icon.pixmap(QtCore.QSize(64,64)))
893 pixmap = QtGui.QPixmap(self._app.icon.pixmap(QtCore.QSize(64,64)))
891 box.setIconPixmap(pixmap)
894 box.setIconPixmap(pixmap)
892 reply = box.exec_()
895 reply = box.exec_()
893 else:
896 else:
894 reply = okay
897 reply = okay
895
898
896 if reply == cancel:
899 if reply == cancel:
897 event.ignore()
900 event.ignore()
898 return
901 return
899 if reply == okay:
902 if reply == okay:
900 while self.tab_widget.count() >= 1:
903 while self.tab_widget.count() >= 1:
901 # prevent further confirmations:
904 # prevent further confirmations:
902 widget = self.active_frontend
905 widget = self.active_frontend
903 widget._confirm_exit = False
906 widget._confirm_exit = False
904 self.close_tab(widget)
907 self.close_tab(widget)
905 event.accept()
908 event.accept()
906
909
@@ -1,552 +1,549 b''
1 """ A minimal application using the Qt console-style IPython frontend.
1 """ A minimal application using the Qt console-style IPython frontend.
2
2
3 This is not a complete console app, as subprocess will not be able to receive
3 This is not a complete console app, as subprocess will not be able to receive
4 input, there is no real readline support, among other limitations.
4 input, there is no real readline support, among other limitations.
5
5
6 Authors:
6 Authors:
7
7
8 * Evan Patterson
8 * Evan Patterson
9 * Min RK
9 * Min RK
10 * Erik Tollerud
10 * Erik Tollerud
11 * Fernando Perez
11 * Fernando Perez
12 * Bussonnier Matthias
12 * Bussonnier Matthias
13 * Thomas Kluyver
13 * Thomas Kluyver
14
14
15 """
15 """
16
16
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18 # Imports
18 # Imports
19 #-----------------------------------------------------------------------------
19 #-----------------------------------------------------------------------------
20
20
21 # stdlib imports
21 # stdlib imports
22 import json
22 import json
23 import os
23 import os
24 import signal
24 import signal
25 import sys
25 import sys
26 import uuid
26 import uuid
27
27
28 # System library imports
28 # System library imports
29 from IPython.external.qt import QtGui
29 from IPython.external.qt import QtGui
30
30
31 # Local imports
31 # Local imports
32 from IPython.config.application import boolean_flag, catch_config_error
32 from IPython.config.application import boolean_flag, catch_config_error
33 from IPython.core.application import BaseIPythonApplication
33 from IPython.core.application import BaseIPythonApplication
34 from IPython.core.profiledir import ProfileDir
34 from IPython.core.profiledir import ProfileDir
35 from IPython.lib.kernel import tunnel_to_kernel, find_connection_file
35 from IPython.lib.kernel import tunnel_to_kernel, find_connection_file
36 from IPython.frontend.qt.console.frontend_widget import FrontendWidget
36 from IPython.frontend.qt.console.frontend_widget import FrontendWidget
37 from IPython.frontend.qt.console.ipython_widget import IPythonWidget
37 from IPython.frontend.qt.console.ipython_widget import IPythonWidget
38 from IPython.frontend.qt.console.rich_ipython_widget import RichIPythonWidget
38 from IPython.frontend.qt.console.rich_ipython_widget import RichIPythonWidget
39 from IPython.frontend.qt.console import styles
39 from IPython.frontend.qt.console import styles
40 from IPython.frontend.qt.console.mainwindow import MainWindow
40 from IPython.frontend.qt.console.mainwindow import MainWindow
41 from IPython.frontend.qt.kernelmanager import QtKernelManager
41 from IPython.frontend.qt.kernelmanager import QtKernelManager
42 from IPython.utils.path import filefind
42 from IPython.utils.path import filefind
43 from IPython.utils.py3compat import str_to_bytes
43 from IPython.utils.py3compat import str_to_bytes
44 from IPython.utils.traitlets import (
44 from IPython.utils.traitlets import (
45 Dict, List, Unicode, Integer, CaselessStrEnum, CBool, Any
45 Dict, List, Unicode, Integer, CaselessStrEnum, CBool, Any
46 )
46 )
47 from IPython.zmq.ipkernel import (
47 from IPython.zmq.ipkernel import (
48 flags as ipkernel_flags,
48 flags as ipkernel_flags,
49 aliases as ipkernel_aliases,
49 aliases as ipkernel_aliases,
50 IPKernelApp
50 IPKernelApp
51 )
51 )
52 from IPython.zmq.session import Session, default_secure
52 from IPython.zmq.session import Session, default_secure
53 from IPython.zmq.zmqshell import ZMQInteractiveShell
53 from IPython.zmq.zmqshell import ZMQInteractiveShell
54
54
55 #-----------------------------------------------------------------------------
55 #-----------------------------------------------------------------------------
56 # Network Constants
56 # Network Constants
57 #-----------------------------------------------------------------------------
57 #-----------------------------------------------------------------------------
58
58
59 from IPython.utils.localinterfaces import LOCALHOST, LOCAL_IPS
59 from IPython.utils.localinterfaces import LOCALHOST, LOCAL_IPS
60
60
61 #-----------------------------------------------------------------------------
61 #-----------------------------------------------------------------------------
62 # Globals
62 # Globals
63 #-----------------------------------------------------------------------------
63 #-----------------------------------------------------------------------------
64
64
65 _examples = """
65 _examples = """
66 ipython qtconsole # start the qtconsole
66 ipython qtconsole # start the qtconsole
67 ipython qtconsole --pylab=inline # start with pylab in inline plotting mode
67 ipython qtconsole --pylab=inline # start with pylab in inline plotting mode
68 """
68 """
69
69
70 #-----------------------------------------------------------------------------
70 #-----------------------------------------------------------------------------
71 # Aliases and Flags
71 # Aliases and Flags
72 #-----------------------------------------------------------------------------
72 #-----------------------------------------------------------------------------
73
73
74 flags = dict(ipkernel_flags)
74 flags = dict(ipkernel_flags)
75 qt_flags = {
75 qt_flags = {
76 'existing' : ({'IPythonQtConsoleApp' : {'existing' : 'kernel*.json'}},
76 'existing' : ({'IPythonQtConsoleApp' : {'existing' : 'kernel*.json'}},
77 "Connect to an existing kernel. If no argument specified, guess most recent"),
77 "Connect to an existing kernel. If no argument specified, guess most recent"),
78 'pure' : ({'IPythonQtConsoleApp' : {'pure' : True}},
78 'pure' : ({'IPythonQtConsoleApp' : {'pure' : True}},
79 "Use a pure Python kernel instead of an IPython kernel."),
79 "Use a pure Python kernel instead of an IPython kernel."),
80 'plain' : ({'ConsoleWidget' : {'kind' : 'plain'}},
80 'plain' : ({'ConsoleWidget' : {'kind' : 'plain'}},
81 "Disable rich text support."),
81 "Disable rich text support."),
82 }
82 }
83 qt_flags.update(boolean_flag(
83 qt_flags.update(boolean_flag(
84 'gui-completion', 'ConsoleWidget.gui_completion',
84 'gui-completion', 'ConsoleWidget.gui_completion',
85 "use a GUI widget for tab completion",
85 "use a GUI widget for tab completion",
86 "use plaintext output for completion"
86 "use plaintext output for completion"
87 ))
87 ))
88 qt_flags.update(boolean_flag(
88 qt_flags.update(boolean_flag(
89 'confirm-exit', 'IPythonQtConsoleApp.confirm_exit',
89 'confirm-exit', 'IPythonQtConsoleApp.confirm_exit',
90 """Set to display confirmation dialog on exit. You can always use 'exit' or 'quit',
90 """Set to display confirmation dialog on exit. You can always use 'exit' or 'quit',
91 to force a direct exit without any confirmation.
91 to force a direct exit without any confirmation.
92 """,
92 """,
93 """Don't prompt the user when exiting. This will terminate the kernel
93 """Don't prompt the user when exiting. This will terminate the kernel
94 if it is owned by the frontend, and leave it alive if it is external.
94 if it is owned by the frontend, and leave it alive if it is external.
95 """
95 """
96 ))
96 ))
97 flags.update(qt_flags)
97 flags.update(qt_flags)
98
98
99 aliases = dict(ipkernel_aliases)
99 aliases = dict(ipkernel_aliases)
100
100
101 qt_aliases = dict(
101 qt_aliases = dict(
102 hb = 'IPythonQtConsoleApp.hb_port',
102 hb = 'IPythonQtConsoleApp.hb_port',
103 shell = 'IPythonQtConsoleApp.shell_port',
103 shell = 'IPythonQtConsoleApp.shell_port',
104 iopub = 'IPythonQtConsoleApp.iopub_port',
104 iopub = 'IPythonQtConsoleApp.iopub_port',
105 stdin = 'IPythonQtConsoleApp.stdin_port',
105 stdin = 'IPythonQtConsoleApp.stdin_port',
106 ip = 'IPythonQtConsoleApp.ip',
106 ip = 'IPythonQtConsoleApp.ip',
107 existing = 'IPythonQtConsoleApp.existing',
107 existing = 'IPythonQtConsoleApp.existing',
108 f = 'IPythonQtConsoleApp.connection_file',
108 f = 'IPythonQtConsoleApp.connection_file',
109
109
110 style = 'IPythonWidget.syntax_style',
110 style = 'IPythonWidget.syntax_style',
111 stylesheet = 'IPythonQtConsoleApp.stylesheet',
111 stylesheet = 'IPythonQtConsoleApp.stylesheet',
112 colors = 'ZMQInteractiveShell.colors',
112 colors = 'ZMQInteractiveShell.colors',
113
113
114 editor = 'IPythonWidget.editor',
114 editor = 'IPythonWidget.editor',
115 paging = 'ConsoleWidget.paging',
115 paging = 'ConsoleWidget.paging',
116 ssh = 'IPythonQtConsoleApp.sshserver',
116 ssh = 'IPythonQtConsoleApp.sshserver',
117 )
117 )
118 aliases.update(qt_aliases)
118 aliases.update(qt_aliases)
119
119
120 #-----------------------------------------------------------------------------
120 #-----------------------------------------------------------------------------
121 # Classes
121 # Classes
122 #-----------------------------------------------------------------------------
122 #-----------------------------------------------------------------------------
123
123
124 #-----------------------------------------------------------------------------
124 #-----------------------------------------------------------------------------
125 # IPythonQtConsole
125 # IPythonQtConsole
126 #-----------------------------------------------------------------------------
126 #-----------------------------------------------------------------------------
127
127
128
128
129 class IPythonQtConsoleApp(BaseIPythonApplication):
129 class IPythonQtConsoleApp(BaseIPythonApplication):
130 name = 'ipython-qtconsole'
130 name = 'ipython-qtconsole'
131 default_config_file_name='ipython_config.py'
131 default_config_file_name='ipython_config.py'
132
132
133 description = """
133 description = """
134 The IPython QtConsole.
134 The IPython QtConsole.
135
135
136 This launches a Console-style application using Qt. It is not a full
136 This launches a Console-style application using Qt. It is not a full
137 console, in that launched terminal subprocesses will not be able to accept
137 console, in that launched terminal subprocesses will not be able to accept
138 input.
138 input.
139
139
140 The QtConsole supports various extra features beyond the Terminal IPython
140 The QtConsole supports various extra features beyond the Terminal IPython
141 shell, such as inline plotting with matplotlib, via:
141 shell, such as inline plotting with matplotlib, via:
142
142
143 ipython qtconsole --pylab=inline
143 ipython qtconsole --pylab=inline
144
144
145 as well as saving your session as HTML, and printing the output.
145 as well as saving your session as HTML, and printing the output.
146
146
147 """
147 """
148 examples = _examples
148 examples = _examples
149
149
150 classes = [IPKernelApp, IPythonWidget, ZMQInteractiveShell, ProfileDir, Session]
150 classes = [IPKernelApp, IPythonWidget, ZMQInteractiveShell, ProfileDir, Session]
151 flags = Dict(flags)
151 flags = Dict(flags)
152 aliases = Dict(aliases)
152 aliases = Dict(aliases)
153
153
154 kernel_argv = List(Unicode)
154 kernel_argv = List(Unicode)
155
155
156 # create requested profiles by default, if they don't exist:
156 # create requested profiles by default, if they don't exist:
157 auto_create = CBool(True)
157 auto_create = CBool(True)
158 # connection info:
158 # connection info:
159 ip = Unicode(LOCALHOST, config=True,
159 ip = Unicode(LOCALHOST, config=True,
160 help="""Set the kernel\'s IP address [default localhost].
160 help="""Set the kernel\'s IP address [default localhost].
161 If the IP address is something other than localhost, then
161 If the IP address is something other than localhost, then
162 Consoles on other machines will be able to connect
162 Consoles on other machines will be able to connect
163 to the Kernel, so be careful!"""
163 to the Kernel, so be careful!"""
164 )
164 )
165
165
166 sshserver = Unicode('', config=True,
166 sshserver = Unicode('', config=True,
167 help="""The SSH server to use to connect to the kernel.""")
167 help="""The SSH server to use to connect to the kernel.""")
168 sshkey = Unicode('', config=True,
168 sshkey = Unicode('', config=True,
169 help="""Path to the ssh key to use for logging in to the ssh server.""")
169 help="""Path to the ssh key to use for logging in to the ssh server.""")
170
170
171 hb_port = Integer(0, config=True,
171 hb_port = Integer(0, config=True,
172 help="set the heartbeat port [default: random]")
172 help="set the heartbeat port [default: random]")
173 shell_port = Integer(0, config=True,
173 shell_port = Integer(0, config=True,
174 help="set the shell (XREP) port [default: random]")
174 help="set the shell (XREP) port [default: random]")
175 iopub_port = Integer(0, config=True,
175 iopub_port = Integer(0, config=True,
176 help="set the iopub (PUB) port [default: random]")
176 help="set the iopub (PUB) port [default: random]")
177 stdin_port = Integer(0, config=True,
177 stdin_port = Integer(0, config=True,
178 help="set the stdin (XREQ) port [default: random]")
178 help="set the stdin (XREQ) port [default: random]")
179 connection_file = Unicode('', config=True,
179 connection_file = Unicode('', config=True,
180 help="""JSON file in which to store connection info [default: kernel-<pid>.json]
180 help="""JSON file in which to store connection info [default: kernel-<pid>.json]
181
181
182 This file will contain the IP, ports, and authentication key needed to connect
182 This file will contain the IP, ports, and authentication key needed to connect
183 clients to this kernel. By default, this file will be created in the security-dir
183 clients to this kernel. By default, this file will be created in the security-dir
184 of the current profile, but can be specified by absolute path.
184 of the current profile, but can be specified by absolute path.
185 """)
185 """)
186 def _connection_file_default(self):
186 def _connection_file_default(self):
187 return 'kernel-%i.json' % os.getpid()
187 return 'kernel-%i.json' % os.getpid()
188
188
189 existing = Unicode('', config=True,
189 existing = Unicode('', config=True,
190 help="""Connect to an already running kernel""")
190 help="""Connect to an already running kernel""")
191
191
192 stylesheet = Unicode('', config=True,
192 stylesheet = Unicode('', config=True,
193 help="path to a custom CSS stylesheet")
193 help="path to a custom CSS stylesheet")
194
194
195 pure = CBool(False, config=True,
195 pure = CBool(False, config=True,
196 help="Use a pure Python kernel instead of an IPython kernel.")
196 help="Use a pure Python kernel instead of an IPython kernel.")
197 plain = CBool(False, config=True,
197 plain = CBool(False, config=True,
198 help="Use a plaintext widget instead of rich text (plain can't print/save).")
198 help="Use a plaintext widget instead of rich text (plain can't print/save).")
199
199
200 def _pure_changed(self, name, old, new):
200 def _pure_changed(self, name, old, new):
201 kind = 'plain' if self.plain else 'rich'
201 kind = 'plain' if self.plain else 'rich'
202 self.config.ConsoleWidget.kind = kind
202 self.config.ConsoleWidget.kind = kind
203 if self.pure:
203 if self.pure:
204 self.widget_factory = FrontendWidget
204 self.widget_factory = FrontendWidget
205 elif self.plain:
205 elif self.plain:
206 self.widget_factory = IPythonWidget
206 self.widget_factory = IPythonWidget
207 else:
207 else:
208 self.widget_factory = RichIPythonWidget
208 self.widget_factory = RichIPythonWidget
209
209
210 _plain_changed = _pure_changed
210 _plain_changed = _pure_changed
211
211
212 confirm_exit = CBool(True, config=True,
212 confirm_exit = CBool(True, config=True,
213 help="""
213 help="""
214 Set to display confirmation dialog on exit. You can always use 'exit' or 'quit',
214 Set to display confirmation dialog on exit. You can always use 'exit' or 'quit',
215 to force a direct exit without any confirmation.""",
215 to force a direct exit without any confirmation.""",
216 )
216 )
217
217
218 # the factory for creating a widget
218 # the factory for creating a widget
219 widget_factory = Any(RichIPythonWidget)
219 widget_factory = Any(RichIPythonWidget)
220
220
221 def parse_command_line(self, argv=None):
221 def parse_command_line(self, argv=None):
222 super(IPythonQtConsoleApp, self).parse_command_line(argv)
222 super(IPythonQtConsoleApp, self).parse_command_line(argv)
223 if argv is None:
223 if argv is None:
224 argv = sys.argv[1:]
224 argv = sys.argv[1:]
225 self.kernel_argv = list(argv) # copy
225 self.kernel_argv = list(argv) # copy
226 # kernel should inherit default config file from frontend
226 # kernel should inherit default config file from frontend
227 self.kernel_argv.append("--KernelApp.parent_appname='%s'"%self.name)
227 self.kernel_argv.append("--KernelApp.parent_appname='%s'"%self.name)
228 # Scrub frontend-specific flags
228 # Scrub frontend-specific flags
229 swallow_next = False
229 swallow_next = False
230 was_flag = False
230 was_flag = False
231 # copy again, in case some aliases have the same name as a flag
231 # copy again, in case some aliases have the same name as a flag
232 # argv = list(self.kernel_argv)
232 # argv = list(self.kernel_argv)
233 for a in argv:
233 for a in argv:
234 if swallow_next:
234 if swallow_next:
235 swallow_next = False
235 swallow_next = False
236 # last arg was an alias, remove the next one
236 # last arg was an alias, remove the next one
237 # *unless* the last alias has a no-arg flag version, in which
237 # *unless* the last alias has a no-arg flag version, in which
238 # case, don't swallow the next arg if it's also a flag:
238 # case, don't swallow the next arg if it's also a flag:
239 if not (was_flag and a.startswith('-')):
239 if not (was_flag and a.startswith('-')):
240 self.kernel_argv.remove(a)
240 self.kernel_argv.remove(a)
241 continue
241 continue
242 if a.startswith('-'):
242 if a.startswith('-'):
243 split = a.lstrip('-').split('=')
243 split = a.lstrip('-').split('=')
244 alias = split[0]
244 alias = split[0]
245 if alias in qt_aliases:
245 if alias in qt_aliases:
246 self.kernel_argv.remove(a)
246 self.kernel_argv.remove(a)
247 if len(split) == 1:
247 if len(split) == 1:
248 # alias passed with arg via space
248 # alias passed with arg via space
249 swallow_next = True
249 swallow_next = True
250 # could have been a flag that matches an alias, e.g. `existing`
250 # could have been a flag that matches an alias, e.g. `existing`
251 # in which case, we might not swallow the next arg
251 # in which case, we might not swallow the next arg
252 was_flag = alias in qt_flags
252 was_flag = alias in qt_flags
253 elif alias in qt_flags:
253 elif alias in qt_flags:
254 # strip flag, but don't swallow next, as flags don't take args
254 # strip flag, but don't swallow next, as flags don't take args
255 self.kernel_argv.remove(a)
255 self.kernel_argv.remove(a)
256
256
257 def init_connection_file(self):
257 def init_connection_file(self):
258 """find the connection file, and load the info if found.
258 """find the connection file, and load the info if found.
259
259
260 The current working directory and the current profile's security
260 The current working directory and the current profile's security
261 directory will be searched for the file if it is not given by
261 directory will be searched for the file if it is not given by
262 absolute path.
262 absolute path.
263
263
264 When attempting to connect to an existing kernel and the `--existing`
264 When attempting to connect to an existing kernel and the `--existing`
265 argument does not match an existing file, it will be interpreted as a
265 argument does not match an existing file, it will be interpreted as a
266 fileglob, and the matching file in the current profile's security dir
266 fileglob, and the matching file in the current profile's security dir
267 with the latest access time will be used.
267 with the latest access time will be used.
268 """
268 """
269 if self.existing:
269 if self.existing:
270 try:
270 try:
271 cf = find_connection_file(self.existing)
271 cf = find_connection_file(self.existing)
272 except Exception:
272 except Exception:
273 self.log.critical("Could not find existing kernel connection file %s", self.existing)
273 self.log.critical("Could not find existing kernel connection file %s", self.existing)
274 self.exit(1)
274 self.exit(1)
275 self.log.info("Connecting to existing kernel: %s" % cf)
275 self.log.info("Connecting to existing kernel: %s" % cf)
276 self.connection_file = cf
276 self.connection_file = cf
277 # should load_connection_file only be used for existing?
277 # should load_connection_file only be used for existing?
278 # as it is now, this allows reusing ports if an existing
278 # as it is now, this allows reusing ports if an existing
279 # file is requested
279 # file is requested
280 try:
280 try:
281 self.load_connection_file()
281 self.load_connection_file()
282 except Exception:
282 except Exception:
283 self.log.error("Failed to load connection file: %r", self.connection_file, exc_info=True)
283 self.log.error("Failed to load connection file: %r", self.connection_file, exc_info=True)
284 self.exit(1)
284 self.exit(1)
285
285
286 def load_connection_file(self):
286 def load_connection_file(self):
287 """load ip/port/hmac config from JSON connection file"""
287 """load ip/port/hmac config from JSON connection file"""
288 # this is identical to KernelApp.load_connection_file
288 # this is identical to KernelApp.load_connection_file
289 # perhaps it can be centralized somewhere?
289 # perhaps it can be centralized somewhere?
290 try:
290 try:
291 fname = filefind(self.connection_file, ['.', self.profile_dir.security_dir])
291 fname = filefind(self.connection_file, ['.', self.profile_dir.security_dir])
292 except IOError:
292 except IOError:
293 self.log.debug("Connection File not found: %s", self.connection_file)
293 self.log.debug("Connection File not found: %s", self.connection_file)
294 return
294 return
295 self.log.debug(u"Loading connection file %s", fname)
295 self.log.debug(u"Loading connection file %s", fname)
296 with open(fname) as f:
296 with open(fname) as f:
297 s = f.read()
297 s = f.read()
298 cfg = json.loads(s)
298 cfg = json.loads(s)
299 if self.ip == LOCALHOST and 'ip' in cfg:
299 if self.ip == LOCALHOST and 'ip' in cfg:
300 # not overridden by config or cl_args
300 # not overridden by config or cl_args
301 self.ip = cfg['ip']
301 self.ip = cfg['ip']
302 for channel in ('hb', 'shell', 'iopub', 'stdin'):
302 for channel in ('hb', 'shell', 'iopub', 'stdin'):
303 name = channel + '_port'
303 name = channel + '_port'
304 if getattr(self, name) == 0 and name in cfg:
304 if getattr(self, name) == 0 and name in cfg:
305 # not overridden by config or cl_args
305 # not overridden by config or cl_args
306 setattr(self, name, cfg[name])
306 setattr(self, name, cfg[name])
307 if 'key' in cfg:
307 if 'key' in cfg:
308 self.config.Session.key = str_to_bytes(cfg['key'])
308 self.config.Session.key = str_to_bytes(cfg['key'])
309
309
310 def init_ssh(self):
310 def init_ssh(self):
311 """set up ssh tunnels, if needed."""
311 """set up ssh tunnels, if needed."""
312 if not self.sshserver and not self.sshkey:
312 if not self.sshserver and not self.sshkey:
313 return
313 return
314
314
315 if self.sshkey and not self.sshserver:
315 if self.sshkey and not self.sshserver:
316 # specifying just the key implies that we are connecting directly
316 # specifying just the key implies that we are connecting directly
317 self.sshserver = self.ip
317 self.sshserver = self.ip
318 self.ip = LOCALHOST
318 self.ip = LOCALHOST
319
319
320 # build connection dict for tunnels:
320 # build connection dict for tunnels:
321 info = dict(ip=self.ip,
321 info = dict(ip=self.ip,
322 shell_port=self.shell_port,
322 shell_port=self.shell_port,
323 iopub_port=self.iopub_port,
323 iopub_port=self.iopub_port,
324 stdin_port=self.stdin_port,
324 stdin_port=self.stdin_port,
325 hb_port=self.hb_port
325 hb_port=self.hb_port
326 )
326 )
327
327
328 self.log.info("Forwarding connections to %s via %s"%(self.ip, self.sshserver))
328 self.log.info("Forwarding connections to %s via %s"%(self.ip, self.sshserver))
329
329
330 # tunnels return a new set of ports, which will be on localhost:
330 # tunnels return a new set of ports, which will be on localhost:
331 self.ip = LOCALHOST
331 self.ip = LOCALHOST
332 try:
332 try:
333 newports = tunnel_to_kernel(info, self.sshserver, self.sshkey)
333 newports = tunnel_to_kernel(info, self.sshserver, self.sshkey)
334 except:
334 except:
335 # even catch KeyboardInterrupt
335 # even catch KeyboardInterrupt
336 self.log.error("Could not setup tunnels", exc_info=True)
336 self.log.error("Could not setup tunnels", exc_info=True)
337 self.exit(1)
337 self.exit(1)
338
338
339 self.shell_port, self.iopub_port, self.stdin_port, self.hb_port = newports
339 self.shell_port, self.iopub_port, self.stdin_port, self.hb_port = newports
340
340
341 cf = self.connection_file
341 cf = self.connection_file
342 base,ext = os.path.splitext(cf)
342 base,ext = os.path.splitext(cf)
343 base = os.path.basename(base)
343 base = os.path.basename(base)
344 self.connection_file = os.path.basename(base)+'-ssh'+ext
344 self.connection_file = os.path.basename(base)+'-ssh'+ext
345 self.log.critical("To connect another client via this tunnel, use:")
345 self.log.critical("To connect another client via this tunnel, use:")
346 self.log.critical("--existing %s" % self.connection_file)
346 self.log.critical("--existing %s" % self.connection_file)
347
347
348 def _new_connection_file(self):
348 def _new_connection_file(self):
349 return os.path.join(self.profile_dir.security_dir, 'kernel-%s.json' % uuid.uuid4())
349 return os.path.join(self.profile_dir.security_dir, 'kernel-%s.json' % uuid.uuid4())
350
350
351 def init_kernel_manager(self):
351 def init_kernel_manager(self):
352 # Don't let Qt or ZMQ swallow KeyboardInterupts.
352 # Don't let Qt or ZMQ swallow KeyboardInterupts.
353 signal.signal(signal.SIGINT, signal.SIG_DFL)
353 signal.signal(signal.SIGINT, signal.SIG_DFL)
354 sec = self.profile_dir.security_dir
354 sec = self.profile_dir.security_dir
355 try:
355 try:
356 cf = filefind(self.connection_file, ['.', sec])
356 cf = filefind(self.connection_file, ['.', sec])
357 except IOError:
357 except IOError:
358 # file might not exist
358 # file might not exist
359 if self.connection_file == os.path.basename(self.connection_file):
359 if self.connection_file == os.path.basename(self.connection_file):
360 # just shortname, put it in security dir
360 # just shortname, put it in security dir
361 cf = os.path.join(sec, self.connection_file)
361 cf = os.path.join(sec, self.connection_file)
362 else:
362 else:
363 cf = self.connection_file
363 cf = self.connection_file
364
364
365 # Create a KernelManager and start a kernel.
365 # Create a KernelManager and start a kernel.
366 self.kernel_manager = QtKernelManager(
366 self.kernel_manager = QtKernelManager(
367 ip=self.ip,
367 ip=self.ip,
368 shell_port=self.shell_port,
368 shell_port=self.shell_port,
369 iopub_port=self.iopub_port,
369 iopub_port=self.iopub_port,
370 stdin_port=self.stdin_port,
370 stdin_port=self.stdin_port,
371 hb_port=self.hb_port,
371 hb_port=self.hb_port,
372 connection_file=cf,
372 connection_file=cf,
373 config=self.config,
373 config=self.config,
374 )
374 )
375 # start the kernel
375 # start the kernel
376 if not self.existing:
376 if not self.existing:
377 kwargs = dict(ipython=not self.pure)
377 kwargs = dict(ipython=not self.pure)
378 kwargs['extra_arguments'] = self.kernel_argv
378 kwargs['extra_arguments'] = self.kernel_argv
379 self.kernel_manager.start_kernel(**kwargs)
379 self.kernel_manager.start_kernel(**kwargs)
380 elif self.sshserver:
380 elif self.sshserver:
381 # ssh, write new connection file
381 # ssh, write new connection file
382 self.kernel_manager.write_connection_file()
382 self.kernel_manager.write_connection_file()
383 self.kernel_manager.start_channels()
383 self.kernel_manager.start_channels()
384
384
385 def new_frontend_master(self):
385 def new_frontend_master(self):
386 """ Create and return new frontend attached to new kernel, launched on localhost.
386 """ Create and return new frontend attached to new kernel, launched on localhost.
387 """
387 """
388 ip = self.ip if self.ip in LOCAL_IPS else LOCALHOST
388 ip = self.ip if self.ip in LOCAL_IPS else LOCALHOST
389 kernel_manager = QtKernelManager(
389 kernel_manager = QtKernelManager(
390 ip=ip,
390 ip=ip,
391 connection_file=self._new_connection_file(),
391 connection_file=self._new_connection_file(),
392 config=self.config,
392 config=self.config,
393 )
393 )
394 # start the kernel
394 # start the kernel
395 kwargs = dict(ipython=not self.pure)
395 kwargs = dict(ipython=not self.pure)
396 kwargs['extra_arguments'] = self.kernel_argv
396 kwargs['extra_arguments'] = self.kernel_argv
397 kernel_manager.start_kernel(**kwargs)
397 kernel_manager.start_kernel(**kwargs)
398 kernel_manager.start_channels()
398 kernel_manager.start_channels()
399 widget = self.widget_factory(config=self.config,
399 widget = self.widget_factory(config=self.config,
400 local_kernel=True)
400 local_kernel=True)
401 widget.kernel_manager = kernel_manager
401 widget.kernel_manager = kernel_manager
402 widget._existing = False
402 widget._existing = False
403 widget._may_close = True
403 widget._may_close = True
404 widget._confirm_exit = self.confirm_exit
404 widget._confirm_exit = self.confirm_exit
405 return widget
405 return widget
406
406
407 def new_frontend_slave(self, current_widget):
407 def new_frontend_slave(self, current_widget):
408 """Create and return a new frontend attached to an existing kernel.
408 """Create and return a new frontend attached to an existing kernel.
409
409
410 Parameters
410 Parameters
411 ----------
411 ----------
412 current_widget : IPythonWidget
412 current_widget : IPythonWidget
413 The IPythonWidget whose kernel this frontend is to share
413 The IPythonWidget whose kernel this frontend is to share
414 """
414 """
415 kernel_manager = QtKernelManager(
415 kernel_manager = QtKernelManager(
416 connection_file=current_widget.kernel_manager.connection_file,
416 connection_file=current_widget.kernel_manager.connection_file,
417 config = self.config,
417 config = self.config,
418 )
418 )
419 kernel_manager.load_connection_file()
419 kernel_manager.load_connection_file()
420 kernel_manager.start_channels()
420 kernel_manager.start_channels()
421 widget = self.widget_factory(config=self.config,
421 widget = self.widget_factory(config=self.config,
422 local_kernel=False)
422 local_kernel=False)
423 widget._existing = True
423 widget._existing = True
424 widget._may_close = False
424 widget._may_close = False
425 widget._confirm_exit = False
425 widget._confirm_exit = False
426 widget.kernel_manager = kernel_manager
426 widget.kernel_manager = kernel_manager
427 return widget
427 return widget
428
428
429 def init_qt_elements(self):
429 def init_qt_elements(self):
430 # Create the widget.
430 # Create the widget.
431 self.app = QtGui.QApplication([])
431 self.app = QtGui.QApplication([])
432
432
433 base_path = os.path.abspath(os.path.dirname(__file__))
433 base_path = os.path.abspath(os.path.dirname(__file__))
434 icon_path = os.path.join(base_path, 'resources', 'icon', 'IPythonConsole.svg')
434 icon_path = os.path.join(base_path, 'resources', 'icon', 'IPythonConsole.svg')
435 self.app.icon = QtGui.QIcon(icon_path)
435 self.app.icon = QtGui.QIcon(icon_path)
436 QtGui.QApplication.setWindowIcon(self.app.icon)
436 QtGui.QApplication.setWindowIcon(self.app.icon)
437
437
438 local_kernel = (not self.existing) or self.ip in LOCAL_IPS
438 local_kernel = (not self.existing) or self.ip in LOCAL_IPS
439 self.widget = self.widget_factory(config=self.config,
439 self.widget = self.widget_factory(config=self.config,
440 local_kernel=local_kernel)
440 local_kernel=local_kernel)
441 self.widget._existing = self.existing
441 self.widget._existing = self.existing
442 self.widget._may_close = not self.existing
442 self.widget._may_close = not self.existing
443 self.widget._confirm_exit = self.confirm_exit
443 self.widget._confirm_exit = self.confirm_exit
444
444
445 self.widget.kernel_manager = self.kernel_manager
445 self.widget.kernel_manager = self.kernel_manager
446 self.window = MainWindow(self.app,
446 self.window = MainWindow(self.app,
447 confirm_exit=self.confirm_exit,
447 confirm_exit=self.confirm_exit,
448 new_frontend_factory=self.new_frontend_master,
448 new_frontend_factory=self.new_frontend_master,
449 slave_frontend_factory=self.new_frontend_slave,
449 slave_frontend_factory=self.new_frontend_slave,
450 )
450 )
451 self.window.log = self.log
451 self.window.log = self.log
452 self.window.add_tab_with_frontend(self.widget)
452 self.window.add_tab_with_frontend(self.widget)
453 self.window.init_menu_bar()
453 self.window.init_menu_bar()
454
454
455 # we need to populate the 'Magic Menu' once the kernel has answer at least once
456 self.kernel_manager.shell_channel.first_reply.connect(self.window.pop.trigger)
457
458 self.window.setWindowTitle('Python' if self.pure else 'IPython')
455 self.window.setWindowTitle('Python' if self.pure else 'IPython')
459
456
460 def init_colors(self):
457 def init_colors(self):
461 """Configure the coloring of the widget"""
458 """Configure the coloring of the widget"""
462 # Note: This will be dramatically simplified when colors
459 # Note: This will be dramatically simplified when colors
463 # are removed from the backend.
460 # are removed from the backend.
464
461
465 if self.pure:
462 if self.pure:
466 # only IPythonWidget supports styling
463 # only IPythonWidget supports styling
467 return
464 return
468
465
469 # parse the colors arg down to current known labels
466 # parse the colors arg down to current known labels
470 try:
467 try:
471 colors = self.config.ZMQInteractiveShell.colors
468 colors = self.config.ZMQInteractiveShell.colors
472 except AttributeError:
469 except AttributeError:
473 colors = None
470 colors = None
474 try:
471 try:
475 style = self.config.IPythonWidget.syntax_style
472 style = self.config.IPythonWidget.syntax_style
476 except AttributeError:
473 except AttributeError:
477 style = None
474 style = None
478
475
479 # find the value for colors:
476 # find the value for colors:
480 if colors:
477 if colors:
481 colors=colors.lower()
478 colors=colors.lower()
482 if colors in ('lightbg', 'light'):
479 if colors in ('lightbg', 'light'):
483 colors='lightbg'
480 colors='lightbg'
484 elif colors in ('dark', 'linux'):
481 elif colors in ('dark', 'linux'):
485 colors='linux'
482 colors='linux'
486 else:
483 else:
487 colors='nocolor'
484 colors='nocolor'
488 elif style:
485 elif style:
489 if style=='bw':
486 if style=='bw':
490 colors='nocolor'
487 colors='nocolor'
491 elif styles.dark_style(style):
488 elif styles.dark_style(style):
492 colors='linux'
489 colors='linux'
493 else:
490 else:
494 colors='lightbg'
491 colors='lightbg'
495 else:
492 else:
496 colors=None
493 colors=None
497
494
498 # Configure the style.
495 # Configure the style.
499 widget = self.widget
496 widget = self.widget
500 if style:
497 if style:
501 widget.style_sheet = styles.sheet_from_template(style, colors)
498 widget.style_sheet = styles.sheet_from_template(style, colors)
502 widget.syntax_style = style
499 widget.syntax_style = style
503 widget._syntax_style_changed()
500 widget._syntax_style_changed()
504 widget._style_sheet_changed()
501 widget._style_sheet_changed()
505 elif colors:
502 elif colors:
506 # use a default style
503 # use a default style
507 widget.set_default_style(colors=colors)
504 widget.set_default_style(colors=colors)
508 else:
505 else:
509 # this is redundant for now, but allows the widget's
506 # this is redundant for now, but allows the widget's
510 # defaults to change
507 # defaults to change
511 widget.set_default_style()
508 widget.set_default_style()
512
509
513 if self.stylesheet:
510 if self.stylesheet:
514 # we got an expicit stylesheet
511 # we got an expicit stylesheet
515 if os.path.isfile(self.stylesheet):
512 if os.path.isfile(self.stylesheet):
516 with open(self.stylesheet) as f:
513 with open(self.stylesheet) as f:
517 sheet = f.read()
514 sheet = f.read()
518 widget.style_sheet = sheet
515 widget.style_sheet = sheet
519 widget._style_sheet_changed()
516 widget._style_sheet_changed()
520 else:
517 else:
521 raise IOError("Stylesheet %r not found."%self.stylesheet)
518 raise IOError("Stylesheet %r not found."%self.stylesheet)
522
519
523 @catch_config_error
520 @catch_config_error
524 def initialize(self, argv=None):
521 def initialize(self, argv=None):
525 super(IPythonQtConsoleApp, self).initialize(argv)
522 super(IPythonQtConsoleApp, self).initialize(argv)
526 self.init_connection_file()
523 self.init_connection_file()
527 default_secure(self.config)
524 default_secure(self.config)
528 self.init_ssh()
525 self.init_ssh()
529 self.init_kernel_manager()
526 self.init_kernel_manager()
530 self.init_qt_elements()
527 self.init_qt_elements()
531 self.init_colors()
528 self.init_colors()
532
529
533 def start(self):
530 def start(self):
534
531
535 # draw the window
532 # draw the window
536 self.window.show()
533 self.window.show()
537
534
538 # Start the application main loop.
535 # Start the application main loop.
539 self.app.exec_()
536 self.app.exec_()
540
537
541 #-----------------------------------------------------------------------------
538 #-----------------------------------------------------------------------------
542 # Main entry point
539 # Main entry point
543 #-----------------------------------------------------------------------------
540 #-----------------------------------------------------------------------------
544
541
545 def main():
542 def main():
546 app = IPythonQtConsoleApp()
543 app = IPythonQtConsoleApp()
547 app.initialize()
544 app.initialize()
548 app.start()
545 app.start()
549
546
550
547
551 if __name__ == '__main__':
548 if __name__ == '__main__':
552 main()
549 main()
General Comments 0
You need to be logged in to leave comments. Login now