##// END OF EJS Templates
Merge pull request #1065 from Carreau/qtconsole-racecondition...
Fernando Perez -
r5527:a5fd0a3d merge
parent child Browse files
Show More
@@ -1,728 +1,730 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 msg_id = msg['parent_header']['msg_id']
378 info = self._request_info['execute'].get(msg_id)
377 # unset reading flag, because if execute finished, raw_input can't
379 # unset reading flag, because if execute finished, raw_input can't
378 # still be pending.
380 # still be pending.
379 self._reading = False
381 self._reading = False
380 if info and info.id == msg['parent_header']['msg_id'] and \
382 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
383 # Make sure that all output from the SUB channel has been processed
383 # before writing a new prompt.
384 # before writing a new prompt.
384 self.kernel_manager.sub_channel.flush()
385 self.kernel_manager.sub_channel.flush()
385
386
386 # Reset the ANSI style information to prevent bad text in stdout
387 # 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
388 # from messing up our colors. We're not a true terminal so we're
388 # allowed to do this.
389 # allowed to do this.
389 if self.ansi_codes:
390 if self.ansi_codes:
390 self._ansi_processor.reset_sgr()
391 self._ansi_processor.reset_sgr()
391
392
392 content = msg['content']
393 content = msg['content']
393 status = content['status']
394 status = content['status']
394 if status == 'ok':
395 if status == 'ok':
395 self._process_execute_ok(msg)
396 self._process_execute_ok(msg)
396 elif status == 'error':
397 elif status == 'error':
397 self._process_execute_error(msg)
398 self._process_execute_error(msg)
398 elif status == 'aborted':
399 elif status == 'aborted':
399 self._process_execute_abort(msg)
400 self._process_execute_abort(msg)
400
401
401 self._show_interpreter_prompt_for_reply(msg)
402 self._show_interpreter_prompt_for_reply(msg)
402 self.executed.emit(msg)
403 self.executed.emit(msg)
403 elif info and info.id == msg['parent_header']['msg_id'] and \
404 self._request_info['execute'].pop(msg_id)
404 info.kind == 'silent_exec_callback' and not self._hidden:
405 elif info and info.kind == 'silent_exec_callback' and not self._hidden:
405 self._handle_exec_callback(msg)
406 self._handle_exec_callback(msg)
407 self._request_info['execute'].pop(msg_id)
406 else:
408 else:
407 super(FrontendWidget, self)._handle_execute_reply(msg)
409 super(FrontendWidget, self)._handle_execute_reply(msg)
408
410
409 def _handle_input_request(self, msg):
411 def _handle_input_request(self, msg):
410 """ Handle requests for raw_input.
412 """ Handle requests for raw_input.
411 """
413 """
412 self.log.debug("input: %s", msg.get('content', ''))
414 self.log.debug("input: %s", msg.get('content', ''))
413 if self._hidden:
415 if self._hidden:
414 raise RuntimeError('Request for raw input during hidden execution.')
416 raise RuntimeError('Request for raw input during hidden execution.')
415
417
416 # Make sure that all output from the SUB channel has been processed
418 # Make sure that all output from the SUB channel has been processed
417 # before entering readline mode.
419 # before entering readline mode.
418 self.kernel_manager.sub_channel.flush()
420 self.kernel_manager.sub_channel.flush()
419
421
420 def callback(line):
422 def callback(line):
421 self.kernel_manager.stdin_channel.input(line)
423 self.kernel_manager.stdin_channel.input(line)
422 if self._reading:
424 if self._reading:
423 self.log.debug("Got second input request, assuming first was interrupted.")
425 self.log.debug("Got second input request, assuming first was interrupted.")
424 self._reading = False
426 self._reading = False
425 self._readline(msg['content']['prompt'], callback=callback)
427 self._readline(msg['content']['prompt'], callback=callback)
426
428
427 def _handle_kernel_died(self, since_last_heartbeat):
429 def _handle_kernel_died(self, since_last_heartbeat):
428 """ Handle the kernel's death by asking if the user wants to restart.
430 """ Handle the kernel's death by asking if the user wants to restart.
429 """
431 """
430 self.log.debug("kernel died: %s", since_last_heartbeat)
432 self.log.debug("kernel died: %s", since_last_heartbeat)
431 if self.custom_restart:
433 if self.custom_restart:
432 self.custom_restart_kernel_died.emit(since_last_heartbeat)
434 self.custom_restart_kernel_died.emit(since_last_heartbeat)
433 else:
435 else:
434 message = 'The kernel heartbeat has been inactive for %.2f ' \
436 message = 'The kernel heartbeat has been inactive for %.2f ' \
435 'seconds. Do you want to restart the kernel? You may ' \
437 'seconds. Do you want to restart the kernel? You may ' \
436 'first want to check the network connection.' % \
438 'first want to check the network connection.' % \
437 since_last_heartbeat
439 since_last_heartbeat
438 self.restart_kernel(message, now=True)
440 self.restart_kernel(message, now=True)
439
441
440 def _handle_object_info_reply(self, rep):
442 def _handle_object_info_reply(self, rep):
441 """ Handle replies for call tips.
443 """ Handle replies for call tips.
442 """
444 """
443 self.log.debug("oinfo: %s", rep.get('content', ''))
445 self.log.debug("oinfo: %s", rep.get('content', ''))
444 cursor = self._get_cursor()
446 cursor = self._get_cursor()
445 info = self._request_info.get('call_tip')
447 info = self._request_info.get('call_tip')
446 if info and info.id == rep['parent_header']['msg_id'] and \
448 if info and info.id == rep['parent_header']['msg_id'] and \
447 info.pos == cursor.position():
449 info.pos == cursor.position():
448 # Get the information for a call tip. For now we format the call
450 # 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
451 # line as string, later we can pass False to format_call and
450 # syntax-highlight it ourselves for nicer formatting in the
452 # syntax-highlight it ourselves for nicer formatting in the
451 # calltip.
453 # calltip.
452 content = rep['content']
454 content = rep['content']
453 # if this is from pykernel, 'docstring' will be the only key
455 # if this is from pykernel, 'docstring' will be the only key
454 if content.get('ismagic', False):
456 if content.get('ismagic', False):
455 # Don't generate a call-tip for magics. Ideally, we should
457 # Don't generate a call-tip for magics. Ideally, we should
456 # generate a tooltip, but not on ( like we do for actual
458 # generate a tooltip, but not on ( like we do for actual
457 # callables.
459 # callables.
458 call_info, doc = None, None
460 call_info, doc = None, None
459 else:
461 else:
460 call_info, doc = call_tip(content, format_call=True)
462 call_info, doc = call_tip(content, format_call=True)
461 if call_info or doc:
463 if call_info or doc:
462 self._call_tip_widget.show_call_info(call_info, doc)
464 self._call_tip_widget.show_call_info(call_info, doc)
463
465
464 def _handle_pyout(self, msg):
466 def _handle_pyout(self, msg):
465 """ Handle display hook output.
467 """ Handle display hook output.
466 """
468 """
467 self.log.debug("pyout: %s", msg.get('content', ''))
469 self.log.debug("pyout: %s", msg.get('content', ''))
468 if not self._hidden and self._is_from_this_session(msg):
470 if not self._hidden and self._is_from_this_session(msg):
469 text = msg['content']['data']
471 text = msg['content']['data']
470 self._append_plain_text(text + '\n', before_prompt=True)
472 self._append_plain_text(text + '\n', before_prompt=True)
471
473
472 def _handle_stream(self, msg):
474 def _handle_stream(self, msg):
473 """ Handle stdout, stderr, and stdin.
475 """ Handle stdout, stderr, and stdin.
474 """
476 """
475 self.log.debug("stream: %s", msg.get('content', ''))
477 self.log.debug("stream: %s", msg.get('content', ''))
476 if not self._hidden and self._is_from_this_session(msg):
478 if not self._hidden and self._is_from_this_session(msg):
477 # Most consoles treat tabs as being 8 space characters. Convert tabs
479 # Most consoles treat tabs as being 8 space characters. Convert tabs
478 # to spaces so that output looks as expected regardless of this
480 # to spaces so that output looks as expected regardless of this
479 # widget's tab width.
481 # widget's tab width.
480 text = msg['content']['data'].expandtabs(8)
482 text = msg['content']['data'].expandtabs(8)
481
483
482 self._append_plain_text(text, before_prompt=True)
484 self._append_plain_text(text, before_prompt=True)
483 self._control.moveCursor(QtGui.QTextCursor.End)
485 self._control.moveCursor(QtGui.QTextCursor.End)
484
486
485 def _handle_shutdown_reply(self, msg):
487 def _handle_shutdown_reply(self, msg):
486 """ Handle shutdown signal, only if from other console.
488 """ Handle shutdown signal, only if from other console.
487 """
489 """
488 self.log.debug("shutdown: %s", msg.get('content', ''))
490 self.log.debug("shutdown: %s", msg.get('content', ''))
489 if not self._hidden and not self._is_from_this_session(msg):
491 if not self._hidden and not self._is_from_this_session(msg):
490 if self._local_kernel:
492 if self._local_kernel:
491 if not msg['content']['restart']:
493 if not msg['content']['restart']:
492 self.exit_requested.emit(self)
494 self.exit_requested.emit(self)
493 else:
495 else:
494 # we just got notified of a restart!
496 # we just got notified of a restart!
495 time.sleep(0.25) # wait 1/4 sec to reset
497 time.sleep(0.25) # wait 1/4 sec to reset
496 # lest the request for a new prompt
498 # lest the request for a new prompt
497 # goes to the old kernel
499 # goes to the old kernel
498 self.reset()
500 self.reset()
499 else: # remote kernel, prompt on Kernel shutdown/reset
501 else: # remote kernel, prompt on Kernel shutdown/reset
500 title = self.window().windowTitle()
502 title = self.window().windowTitle()
501 if not msg['content']['restart']:
503 if not msg['content']['restart']:
502 reply = QtGui.QMessageBox.question(self, title,
504 reply = QtGui.QMessageBox.question(self, title,
503 "Kernel has been shutdown permanently. "
505 "Kernel has been shutdown permanently. "
504 "Close the Console?",
506 "Close the Console?",
505 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
507 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
506 if reply == QtGui.QMessageBox.Yes:
508 if reply == QtGui.QMessageBox.Yes:
507 self.exit_requested.emit(self)
509 self.exit_requested.emit(self)
508 else:
510 else:
509 reply = QtGui.QMessageBox.question(self, title,
511 reply = QtGui.QMessageBox.question(self, title,
510 "Kernel has been reset. Clear the Console?",
512 "Kernel has been reset. Clear the Console?",
511 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
513 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
512 if reply == QtGui.QMessageBox.Yes:
514 if reply == QtGui.QMessageBox.Yes:
513 time.sleep(0.25) # wait 1/4 sec to reset
515 time.sleep(0.25) # wait 1/4 sec to reset
514 # lest the request for a new prompt
516 # lest the request for a new prompt
515 # goes to the old kernel
517 # goes to the old kernel
516 self.reset()
518 self.reset()
517
519
518 def _started_channels(self):
520 def _started_channels(self):
519 """ Called when the KernelManager channels have started listening or
521 """ Called when the KernelManager channels have started listening or
520 when the frontend is assigned an already listening KernelManager.
522 when the frontend is assigned an already listening KernelManager.
521 """
523 """
522 self.reset()
524 self.reset()
523
525
524 #---------------------------------------------------------------------------
526 #---------------------------------------------------------------------------
525 # 'FrontendWidget' public interface
527 # 'FrontendWidget' public interface
526 #---------------------------------------------------------------------------
528 #---------------------------------------------------------------------------
527
529
528 def copy_raw(self):
530 def copy_raw(self):
529 """ Copy the currently selected text to the clipboard without attempting
531 """ Copy the currently selected text to the clipboard without attempting
530 to remove prompts or otherwise alter the text.
532 to remove prompts or otherwise alter the text.
531 """
533 """
532 self._control.copy()
534 self._control.copy()
533
535
534 def execute_file(self, path, hidden=False):
536 def execute_file(self, path, hidden=False):
535 """ Attempts to execute file with 'path'. If 'hidden', no output is
537 """ Attempts to execute file with 'path'. If 'hidden', no output is
536 shown.
538 shown.
537 """
539 """
538 self.execute('execfile(%r)' % path, hidden=hidden)
540 self.execute('execfile(%r)' % path, hidden=hidden)
539
541
540 def interrupt_kernel(self):
542 def interrupt_kernel(self):
541 """ Attempts to interrupt the running kernel.
543 """ Attempts to interrupt the running kernel.
542
544
543 Also unsets _reading flag, to avoid runtime errors
545 Also unsets _reading flag, to avoid runtime errors
544 if raw_input is called again.
546 if raw_input is called again.
545 """
547 """
546 if self.custom_interrupt:
548 if self.custom_interrupt:
547 self._reading = False
549 self._reading = False
548 self.custom_interrupt_requested.emit()
550 self.custom_interrupt_requested.emit()
549 elif self.kernel_manager.has_kernel:
551 elif self.kernel_manager.has_kernel:
550 self._reading = False
552 self._reading = False
551 self.kernel_manager.interrupt_kernel()
553 self.kernel_manager.interrupt_kernel()
552 else:
554 else:
553 self._append_plain_text('Kernel process is either remote or '
555 self._append_plain_text('Kernel process is either remote or '
554 'unspecified. Cannot interrupt.\n')
556 'unspecified. Cannot interrupt.\n')
555
557
556 def reset(self):
558 def reset(self):
557 """ Resets the widget to its initial state. Similar to ``clear``, but
559 """ Resets the widget to its initial state. Similar to ``clear``, but
558 also re-writes the banner and aborts execution if necessary.
560 also re-writes the banner and aborts execution if necessary.
559 """
561 """
560 if self._executing:
562 if self._executing:
561 self._executing = False
563 self._executing = False
562 self._request_info['execute'] = None
564 self._request_info['execute'] = {}
563 self._reading = False
565 self._reading = False
564 self._highlighter.highlighting_on = False
566 self._highlighter.highlighting_on = False
565
567
566 self._control.clear()
568 self._control.clear()
567 self._append_plain_text(self.banner)
569 self._append_plain_text(self.banner)
568 # update output marker for stdout/stderr, so that startup
570 # update output marker for stdout/stderr, so that startup
569 # messages appear after banner:
571 # messages appear after banner:
570 self._append_before_prompt_pos = self._get_cursor().position()
572 self._append_before_prompt_pos = self._get_cursor().position()
571 self._show_interpreter_prompt()
573 self._show_interpreter_prompt()
572
574
573 def restart_kernel(self, message, now=False):
575 def restart_kernel(self, message, now=False):
574 """ Attempts to restart the running kernel.
576 """ Attempts to restart the running kernel.
575 """
577 """
576 # FIXME: now should be configurable via a checkbox in the dialog. Right
578 # 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
579 # 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
580 # 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
581 # checkbox that the user could override if so desired. But I don't know
580 # enough Qt to go implementing the checkbox now.
582 # enough Qt to go implementing the checkbox now.
581
583
582 if self.custom_restart:
584 if self.custom_restart:
583 self.custom_restart_requested.emit()
585 self.custom_restart_requested.emit()
584
586
585 elif self.kernel_manager.has_kernel:
587 elif self.kernel_manager.has_kernel:
586 # Pause the heart beat channel to prevent further warnings.
588 # Pause the heart beat channel to prevent further warnings.
587 self.kernel_manager.hb_channel.pause()
589 self.kernel_manager.hb_channel.pause()
588
590
589 # Prompt the user to restart the kernel. Un-pause the heartbeat if
591 # Prompt the user to restart the kernel. Un-pause the heartbeat if
590 # they decline. (If they accept, the heartbeat will be un-paused
592 # they decline. (If they accept, the heartbeat will be un-paused
591 # automatically when the kernel is restarted.)
593 # automatically when the kernel is restarted.)
592 buttons = QtGui.QMessageBox.Yes | QtGui.QMessageBox.No
594 buttons = QtGui.QMessageBox.Yes | QtGui.QMessageBox.No
593 result = QtGui.QMessageBox.question(self, 'Restart kernel?',
595 result = QtGui.QMessageBox.question(self, 'Restart kernel?',
594 message, buttons)
596 message, buttons)
595 if result == QtGui.QMessageBox.Yes:
597 if result == QtGui.QMessageBox.Yes:
596 try:
598 try:
597 self.kernel_manager.restart_kernel(now=now)
599 self.kernel_manager.restart_kernel(now=now)
598 except RuntimeError:
600 except RuntimeError:
599 self._append_plain_text('Kernel started externally. '
601 self._append_plain_text('Kernel started externally. '
600 'Cannot restart.\n')
602 'Cannot restart.\n')
601 else:
603 else:
602 self.reset()
604 self.reset()
603 else:
605 else:
604 self.kernel_manager.hb_channel.unpause()
606 self.kernel_manager.hb_channel.unpause()
605
607
606 else:
608 else:
607 self._append_plain_text('Kernel process is either remote or '
609 self._append_plain_text('Kernel process is either remote or '
608 'unspecified. Cannot restart.\n')
610 'unspecified. Cannot restart.\n')
609
611
610 #---------------------------------------------------------------------------
612 #---------------------------------------------------------------------------
611 # 'FrontendWidget' protected interface
613 # 'FrontendWidget' protected interface
612 #---------------------------------------------------------------------------
614 #---------------------------------------------------------------------------
613
615
614 def _call_tip(self):
616 def _call_tip(self):
615 """ Shows a call tip, if appropriate, at the current cursor location.
617 """ Shows a call tip, if appropriate, at the current cursor location.
616 """
618 """
617 # Decide if it makes sense to show a call tip
619 # Decide if it makes sense to show a call tip
618 if not self.enable_calltips:
620 if not self.enable_calltips:
619 return False
621 return False
620 cursor = self._get_cursor()
622 cursor = self._get_cursor()
621 cursor.movePosition(QtGui.QTextCursor.Left)
623 cursor.movePosition(QtGui.QTextCursor.Left)
622 if cursor.document().characterAt(cursor.position()) != '(':
624 if cursor.document().characterAt(cursor.position()) != '(':
623 return False
625 return False
624 context = self._get_context(cursor)
626 context = self._get_context(cursor)
625 if not context:
627 if not context:
626 return False
628 return False
627
629
628 # Send the metadata request to the kernel
630 # Send the metadata request to the kernel
629 name = '.'.join(context)
631 name = '.'.join(context)
630 msg_id = self.kernel_manager.shell_channel.object_info(name)
632 msg_id = self.kernel_manager.shell_channel.object_info(name)
631 pos = self._get_cursor().position()
633 pos = self._get_cursor().position()
632 self._request_info['call_tip'] = self._CallTipRequest(msg_id, pos)
634 self._request_info['call_tip'] = self._CallTipRequest(msg_id, pos)
633 return True
635 return True
634
636
635 def _complete(self):
637 def _complete(self):
636 """ Performs completion at the current cursor location.
638 """ Performs completion at the current cursor location.
637 """
639 """
638 context = self._get_context()
640 context = self._get_context()
639 if context:
641 if context:
640 # Send the completion request to the kernel
642 # Send the completion request to the kernel
641 msg_id = self.kernel_manager.shell_channel.complete(
643 msg_id = self.kernel_manager.shell_channel.complete(
642 '.'.join(context), # text
644 '.'.join(context), # text
643 self._get_input_buffer_cursor_line(), # line
645 self._get_input_buffer_cursor_line(), # line
644 self._get_input_buffer_cursor_column(), # cursor_pos
646 self._get_input_buffer_cursor_column(), # cursor_pos
645 self.input_buffer) # block
647 self.input_buffer) # block
646 pos = self._get_cursor().position()
648 pos = self._get_cursor().position()
647 info = self._CompletionRequest(msg_id, pos)
649 info = self._CompletionRequest(msg_id, pos)
648 self._request_info['complete'] = info
650 self._request_info['complete'] = info
649
651
650 def _get_context(self, cursor=None):
652 def _get_context(self, cursor=None):
651 """ Gets the context for the specified cursor (or the current cursor
653 """ Gets the context for the specified cursor (or the current cursor
652 if none is specified).
654 if none is specified).
653 """
655 """
654 if cursor is None:
656 if cursor is None:
655 cursor = self._get_cursor()
657 cursor = self._get_cursor()
656 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
658 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
657 QtGui.QTextCursor.KeepAnchor)
659 QtGui.QTextCursor.KeepAnchor)
658 text = cursor.selection().toPlainText()
660 text = cursor.selection().toPlainText()
659 return self._completion_lexer.get_context(text)
661 return self._completion_lexer.get_context(text)
660
662
661 def _process_execute_abort(self, msg):
663 def _process_execute_abort(self, msg):
662 """ Process a reply for an aborted execution request.
664 """ Process a reply for an aborted execution request.
663 """
665 """
664 self._append_plain_text("ERROR: execution aborted\n")
666 self._append_plain_text("ERROR: execution aborted\n")
665
667
666 def _process_execute_error(self, msg):
668 def _process_execute_error(self, msg):
667 """ Process a reply for an execution request that resulted in an error.
669 """ Process a reply for an execution request that resulted in an error.
668 """
670 """
669 content = msg['content']
671 content = msg['content']
670 # If a SystemExit is passed along, this means exit() was called - also
672 # 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
673 # all the ipython %exit magic syntax of '-k' to be used to keep
672 # the kernel running
674 # the kernel running
673 if content['ename']=='SystemExit':
675 if content['ename']=='SystemExit':
674 keepkernel = content['evalue']=='-k' or content['evalue']=='True'
676 keepkernel = content['evalue']=='-k' or content['evalue']=='True'
675 self._keep_kernel_on_exit = keepkernel
677 self._keep_kernel_on_exit = keepkernel
676 self.exit_requested.emit(self)
678 self.exit_requested.emit(self)
677 else:
679 else:
678 traceback = ''.join(content['traceback'])
680 traceback = ''.join(content['traceback'])
679 self._append_plain_text(traceback)
681 self._append_plain_text(traceback)
680
682
681 def _process_execute_ok(self, msg):
683 def _process_execute_ok(self, msg):
682 """ Process a reply for a successful execution equest.
684 """ Process a reply for a successful execution equest.
683 """
685 """
684 payload = msg['content']['payload']
686 payload = msg['content']['payload']
685 for item in payload:
687 for item in payload:
686 if not self._process_execute_payload(item):
688 if not self._process_execute_payload(item):
687 warning = 'Warning: received unknown payload of type %s'
689 warning = 'Warning: received unknown payload of type %s'
688 print(warning % repr(item['source']))
690 print(warning % repr(item['source']))
689
691
690 def _process_execute_payload(self, item):
692 def _process_execute_payload(self, item):
691 """ Process a single payload item from the list of payload items in an
693 """ Process a single payload item from the list of payload items in an
692 execution reply. Returns whether the payload was handled.
694 execution reply. Returns whether the payload was handled.
693 """
695 """
694 # The basic FrontendWidget doesn't handle payloads, as they are a
696 # The basic FrontendWidget doesn't handle payloads, as they are a
695 # mechanism for going beyond the standard Python interpreter model.
697 # mechanism for going beyond the standard Python interpreter model.
696 return False
698 return False
697
699
698 def _show_interpreter_prompt(self):
700 def _show_interpreter_prompt(self):
699 """ Shows a prompt for the interpreter.
701 """ Shows a prompt for the interpreter.
700 """
702 """
701 self._show_prompt('>>> ')
703 self._show_prompt('>>> ')
702
704
703 def _show_interpreter_prompt_for_reply(self, msg):
705 def _show_interpreter_prompt_for_reply(self, msg):
704 """ Shows a prompt for the interpreter given an 'execute_reply' message.
706 """ Shows a prompt for the interpreter given an 'execute_reply' message.
705 """
707 """
706 self._show_interpreter_prompt()
708 self._show_interpreter_prompt()
707
709
708 #------ Signal handlers ----------------------------------------------------
710 #------ Signal handlers ----------------------------------------------------
709
711
710 def _document_contents_change(self, position, removed, added):
712 def _document_contents_change(self, position, removed, added):
711 """ Called whenever the document's content changes. Display a call tip
713 """ Called whenever the document's content changes. Display a call tip
712 if appropriate.
714 if appropriate.
713 """
715 """
714 # Calculate where the cursor should be *after* the change:
716 # Calculate where the cursor should be *after* the change:
715 position += added
717 position += added
716
718
717 document = self._control.document()
719 document = self._control.document()
718 if position == self._get_cursor().position():
720 if position == self._get_cursor().position():
719 self._call_tip()
721 self._call_tip()
720
722
721 #------ Trait default initializers -----------------------------------------
723 #------ Trait default initializers -----------------------------------------
722
724
723 def _banner_default(self):
725 def _banner_default(self):
724 """ Returns the standard Python banner.
726 """ Returns the standard Python banner.
725 """
727 """
726 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
728 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
727 '"license" for more information.'
729 '"license" for more information.'
728 return banner % (sys.version, sys.platform)
730 return banner % (sys.version, sys.platform)
@@ -1,283 +1,283 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 msg_id = msg['parent_header']['msg_id']
220 if info and info.id == msg['parent_header']['msg_id'] and \
220 info = self._request_info.get['execute'].pop(msg_id,None)
221 info.kind == 'save_magic' and not self._hidden:
221 if info and info.kind == 'save_magic' and not self._hidden:
222 content = msg['content']
222 content = msg['content']
223 status = content['status']
223 status = content['status']
224 if status == 'ok':
224 if status == 'ok':
225 self._max_session_history=(int(content['user_expressions']['hlen']))
225 self._max_session_history=(int(content['user_expressions']['hlen']))
226
226
227 def save_magic(self):
227 def save_magic(self):
228 # update the session history length
228 # update the session history length
229 self._request_update_session_history_length()
229 self._request_update_session_history_length()
230
230
231 file_name,extFilter = QtGui.QFileDialog.getSaveFileName(self,
231 file_name,extFilter = QtGui.QFileDialog.getSaveFileName(self,
232 "Enter A filename",
232 "Enter A filename",
233 filter='Python File (*.py);; All files (*.*)'
233 filter='Python File (*.py);; All files (*.*)'
234 )
234 )
235
235
236 # let's the user search/type for a file name, while the history length
236 # let's the user search/type for a file name, while the history length
237 # is fetched
237 # is fetched
238
238
239 if file_name:
239 if file_name:
240 hist_range, ok = QtGui.QInputDialog.getText(self,
240 hist_range, ok = QtGui.QInputDialog.getText(self,
241 'Please enter an interval of command to save',
241 'Please enter an interval of command to save',
242 'Saving commands:',
242 'Saving commands:',
243 text=str('1-'+str(self._max_session_history))
243 text=str('1-'+str(self._max_session_history))
244 )
244 )
245 if ok:
245 if ok:
246 self.execute("%save"+" "+file_name+" "+str(hist_range))
246 self.execute("%save"+" "+file_name+" "+str(hist_range))
247
247
248 #---------------------------------------------------------------------------
248 #---------------------------------------------------------------------------
249 # 'HistoryConsoleWidget' protected interface
249 # 'HistoryConsoleWidget' protected interface
250 #---------------------------------------------------------------------------
250 #---------------------------------------------------------------------------
251
251
252 def _history_locked(self):
252 def _history_locked(self):
253 """ Returns whether history movement is locked.
253 """ Returns whether history movement is locked.
254 """
254 """
255 return (self.history_lock and
255 return (self.history_lock and
256 (self._get_edited_history(self._history_index) !=
256 (self._get_edited_history(self._history_index) !=
257 self.input_buffer) and
257 self.input_buffer) and
258 (self._get_prompt_cursor().blockNumber() !=
258 (self._get_prompt_cursor().blockNumber() !=
259 self._get_end_cursor().blockNumber()))
259 self._get_end_cursor().blockNumber()))
260
260
261 def _get_edited_history(self, index):
261 def _get_edited_history(self, index):
262 """ Retrieves a history item, possibly with temporary edits.
262 """ Retrieves a history item, possibly with temporary edits.
263 """
263 """
264 if index in self._history_edits:
264 if index in self._history_edits:
265 return self._history_edits[index]
265 return self._history_edits[index]
266 elif index == len(self._history):
266 elif index == len(self._history):
267 return unicode()
267 return unicode()
268 return self._history[index]
268 return self._history[index]
269
269
270 def _set_history(self, history):
270 def _set_history(self, history):
271 """ Replace the current history with a sequence of history items.
271 """ Replace the current history with a sequence of history items.
272 """
272 """
273 self._history = list(history)
273 self._history = list(history)
274 self._history_edits = {}
274 self._history_edits = {}
275 self._history_index = len(self._history)
275 self._history_index = len(self._history)
276
276
277 def _store_edits(self):
277 def _store_edits(self):
278 """ If there are edits to the current input buffer, store them.
278 """ If there are edits to the current input buffer, store them.
279 """
279 """
280 current = self.input_buffer
280 current = self.input_buffer
281 if self._history_index == len(self._history) or \
281 if self._history_index == len(self._history) or \
282 self._history[self._history_index] != current:
282 self._history[self._history_index] != current:
283 self._history_edits[self._history_index] = current
283 self._history_edits[self._history_index] = current
@@ -1,562 +1,563 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 msg_id = msg['parent_header'].get('msg_id')
169 if info and info.id == msg['parent_header']['msg_id']:
169 info = self._request_info['execute'].get(msg_id)
170 if info.kind == 'prompt':
170 if info and info.kind == 'prompt':
171 number = msg['content']['execution_count'] + 1
171 number = msg['content']['execution_count'] + 1
172 self._show_interpreter_prompt(number)
172 self._show_interpreter_prompt(number)
173 else:
173 self._request_info['execute'].pop(msg_id)
174 super(IPythonWidget, self)._handle_execute_reply(msg)
174 else:
175 super(IPythonWidget, self)._handle_execute_reply(msg)
175
176
176 def _handle_history_reply(self, msg):
177 def _handle_history_reply(self, msg):
177 """ Implemented to handle history tail replies, which are only supported
178 """ Implemented to handle history tail replies, which are only supported
178 by the IPython kernel.
179 by the IPython kernel.
179 """
180 """
180 self.log.debug("history: %s", msg.get('content', ''))
181 self.log.debug("history: %s", msg.get('content', ''))
181 content = msg['content']
182 content = msg['content']
182 if 'history' not in content:
183 if 'history' not in content:
183 self.log.error("History request failed: %r"%content)
184 self.log.error("History request failed: %r"%content)
184 if content.get('status', '') == 'aborted' and \
185 if content.get('status', '') == 'aborted' and \
185 not self._retrying_history_request:
186 not self._retrying_history_request:
186 # a *different* action caused this request to be aborted, so
187 # a *different* action caused this request to be aborted, so
187 # we should try again.
188 # we should try again.
188 self.log.error("Retrying aborted history request")
189 self.log.error("Retrying aborted history request")
189 # prevent multiple retries of aborted requests:
190 # prevent multiple retries of aborted requests:
190 self._retrying_history_request = True
191 self._retrying_history_request = True
191 # wait out the kernel's queue flush, which is currently timed at 0.1s
192 # wait out the kernel's queue flush, which is currently timed at 0.1s
192 time.sleep(0.25)
193 time.sleep(0.25)
193 self.kernel_manager.shell_channel.history(hist_access_type='tail',n=1000)
194 self.kernel_manager.shell_channel.history(hist_access_type='tail',n=1000)
194 else:
195 else:
195 self._retrying_history_request = False
196 self._retrying_history_request = False
196 return
197 return
197 # reset retry flag
198 # reset retry flag
198 self._retrying_history_request = False
199 self._retrying_history_request = False
199 history_items = content['history']
200 history_items = content['history']
200 items = []
201 items = []
201 last_cell = u""
202 last_cell = u""
202 for _, _, cell in history_items:
203 for _, _, cell in history_items:
203 cell = cell.rstrip()
204 cell = cell.rstrip()
204 if cell != last_cell:
205 if cell != last_cell:
205 items.append(cell)
206 items.append(cell)
206 last_cell = cell
207 last_cell = cell
207 self._set_history(items)
208 self._set_history(items)
208
209
209 def _handle_pyout(self, msg):
210 def _handle_pyout(self, msg):
210 """ Reimplemented for IPython-style "display hook".
211 """ Reimplemented for IPython-style "display hook".
211 """
212 """
212 self.log.debug("pyout: %s", msg.get('content', ''))
213 self.log.debug("pyout: %s", msg.get('content', ''))
213 if not self._hidden and self._is_from_this_session(msg):
214 if not self._hidden and self._is_from_this_session(msg):
214 content = msg['content']
215 content = msg['content']
215 prompt_number = content['execution_count']
216 prompt_number = content['execution_count']
216 data = content['data']
217 data = content['data']
217 if data.has_key('text/html'):
218 if data.has_key('text/html'):
218 self._append_plain_text(self.output_sep, True)
219 self._append_plain_text(self.output_sep, True)
219 self._append_html(self._make_out_prompt(prompt_number), True)
220 self._append_html(self._make_out_prompt(prompt_number), True)
220 html = data['text/html']
221 html = data['text/html']
221 self._append_plain_text('\n', True)
222 self._append_plain_text('\n', True)
222 self._append_html(html + self.output_sep2, True)
223 self._append_html(html + self.output_sep2, True)
223 elif data.has_key('text/plain'):
224 elif data.has_key('text/plain'):
224 self._append_plain_text(self.output_sep, True)
225 self._append_plain_text(self.output_sep, True)
225 self._append_html(self._make_out_prompt(prompt_number), True)
226 self._append_html(self._make_out_prompt(prompt_number), True)
226 text = data['text/plain']
227 text = data['text/plain']
227 # If the repr is multiline, make sure we start on a new line,
228 # If the repr is multiline, make sure we start on a new line,
228 # so that its lines are aligned.
229 # so that its lines are aligned.
229 if "\n" in text and not self.output_sep.endswith("\n"):
230 if "\n" in text and not self.output_sep.endswith("\n"):
230 self._append_plain_text('\n', True)
231 self._append_plain_text('\n', True)
231 self._append_plain_text(text + self.output_sep2, True)
232 self._append_plain_text(text + self.output_sep2, True)
232
233
233 def _handle_display_data(self, msg):
234 def _handle_display_data(self, msg):
234 """ The base handler for the ``display_data`` message.
235 """ The base handler for the ``display_data`` message.
235 """
236 """
236 self.log.debug("display: %s", msg.get('content', ''))
237 self.log.debug("display: %s", msg.get('content', ''))
237 # For now, we don't display data from other frontends, but we
238 # For now, we don't display data from other frontends, but we
238 # eventually will as this allows all frontends to monitor the display
239 # 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.
240 # 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):
241 if not self._hidden and self._is_from_this_session(msg):
241 source = msg['content']['source']
242 source = msg['content']['source']
242 data = msg['content']['data']
243 data = msg['content']['data']
243 metadata = msg['content']['metadata']
244 metadata = msg['content']['metadata']
244 # In the regular IPythonWidget, we simply print the plain text
245 # In the regular IPythonWidget, we simply print the plain text
245 # representation.
246 # representation.
246 if data.has_key('text/html'):
247 if data.has_key('text/html'):
247 html = data['text/html']
248 html = data['text/html']
248 self._append_html(html, True)
249 self._append_html(html, True)
249 elif data.has_key('text/plain'):
250 elif data.has_key('text/plain'):
250 text = data['text/plain']
251 text = data['text/plain']
251 self._append_plain_text(text, True)
252 self._append_plain_text(text, True)
252 # This newline seems to be needed for text and html output.
253 # This newline seems to be needed for text and html output.
253 self._append_plain_text(u'\n', True)
254 self._append_plain_text(u'\n', True)
254
255
255 def _started_channels(self):
256 def _started_channels(self):
256 """ Reimplemented to make a history request.
257 """ Reimplemented to make a history request.
257 """
258 """
258 super(IPythonWidget, self)._started_channels()
259 super(IPythonWidget, self)._started_channels()
259 self.kernel_manager.shell_channel.history(hist_access_type='tail',
260 self.kernel_manager.shell_channel.history(hist_access_type='tail',
260 n=1000)
261 n=1000)
261 #---------------------------------------------------------------------------
262 #---------------------------------------------------------------------------
262 # 'ConsoleWidget' public interface
263 # 'ConsoleWidget' public interface
263 #---------------------------------------------------------------------------
264 #---------------------------------------------------------------------------
264
265
265 def copy(self):
266 def copy(self):
266 """ Copy the currently selected text to the clipboard, removing prompts
267 """ Copy the currently selected text to the clipboard, removing prompts
267 if possible.
268 if possible.
268 """
269 """
269 text = self._control.textCursor().selection().toPlainText()
270 text = self._control.textCursor().selection().toPlainText()
270 if text:
271 if text:
271 lines = map(transform_ipy_prompt, text.splitlines())
272 lines = map(transform_ipy_prompt, text.splitlines())
272 text = '\n'.join(lines)
273 text = '\n'.join(lines)
273 QtGui.QApplication.clipboard().setText(text)
274 QtGui.QApplication.clipboard().setText(text)
274
275
275 #---------------------------------------------------------------------------
276 #---------------------------------------------------------------------------
276 # 'FrontendWidget' public interface
277 # 'FrontendWidget' public interface
277 #---------------------------------------------------------------------------
278 #---------------------------------------------------------------------------
278
279
279 def execute_file(self, path, hidden=False):
280 def execute_file(self, path, hidden=False):
280 """ Reimplemented to use the 'run' magic.
281 """ Reimplemented to use the 'run' magic.
281 """
282 """
282 # Use forward slashes on Windows to avoid escaping each separator.
283 # Use forward slashes on Windows to avoid escaping each separator.
283 if sys.platform == 'win32':
284 if sys.platform == 'win32':
284 path = os.path.normpath(path).replace('\\', '/')
285 path = os.path.normpath(path).replace('\\', '/')
285
286
286 # Perhaps we should not be using %run directly, but while we
287 # Perhaps we should not be using %run directly, but while we
287 # are, it is necessary to quote filenames containing spaces or quotes.
288 # are, it is necessary to quote filenames containing spaces or quotes.
288 # Escaping quotes in filename in %run seems tricky and inconsistent,
289 # Escaping quotes in filename in %run seems tricky and inconsistent,
289 # so not trying it at present.
290 # so not trying it at present.
290 if '"' in path:
291 if '"' in path:
291 if "'" in path:
292 if "'" in path:
292 raise ValueError("Can't run filename containing both single "
293 raise ValueError("Can't run filename containing both single "
293 "and double quotes: %s" % path)
294 "and double quotes: %s" % path)
294 path = "'%s'" % path
295 path = "'%s'" % path
295 elif ' ' in path or "'" in path:
296 elif ' ' in path or "'" in path:
296 path = '"%s"' % path
297 path = '"%s"' % path
297
298
298 self.execute('%%run %s' % path, hidden=hidden)
299 self.execute('%%run %s' % path, hidden=hidden)
299
300
300 #---------------------------------------------------------------------------
301 #---------------------------------------------------------------------------
301 # 'FrontendWidget' protected interface
302 # 'FrontendWidget' protected interface
302 #---------------------------------------------------------------------------
303 #---------------------------------------------------------------------------
303
304
304 def _complete(self):
305 def _complete(self):
305 """ Reimplemented to support IPython's improved completion machinery.
306 """ Reimplemented to support IPython's improved completion machinery.
306 """
307 """
307 # We let the kernel split the input line, so we *always* send an empty
308 # 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
309 # text field. Readline-based frontends do get a real text field which
309 # they can use.
310 # they can use.
310 text = ''
311 text = ''
311
312
312 # Send the completion request to the kernel
313 # Send the completion request to the kernel
313 msg_id = self.kernel_manager.shell_channel.complete(
314 msg_id = self.kernel_manager.shell_channel.complete(
314 text, # text
315 text, # text
315 self._get_input_buffer_cursor_line(), # line
316 self._get_input_buffer_cursor_line(), # line
316 self._get_input_buffer_cursor_column(), # cursor_pos
317 self._get_input_buffer_cursor_column(), # cursor_pos
317 self.input_buffer) # block
318 self.input_buffer) # block
318 pos = self._get_cursor().position()
319 pos = self._get_cursor().position()
319 info = self._CompletionRequest(msg_id, pos)
320 info = self._CompletionRequest(msg_id, pos)
320 self._request_info['complete'] = info
321 self._request_info['complete'] = info
321
322
322 def _process_execute_error(self, msg):
323 def _process_execute_error(self, msg):
323 """ Reimplemented for IPython-style traceback formatting.
324 """ Reimplemented for IPython-style traceback formatting.
324 """
325 """
325 content = msg['content']
326 content = msg['content']
326 traceback = '\n'.join(content['traceback']) + '\n'
327 traceback = '\n'.join(content['traceback']) + '\n'
327 if False:
328 if False:
328 # FIXME: For now, tracebacks come as plain text, so we can't use
329 # FIXME: For now, tracebacks come as plain text, so we can't use
329 # the html renderer yet. Once we refactor ultratb to produce
330 # the html renderer yet. Once we refactor ultratb to produce
330 # properly styled tracebacks, this branch should be the default
331 # properly styled tracebacks, this branch should be the default
331 traceback = traceback.replace(' ', '&nbsp;')
332 traceback = traceback.replace(' ', '&nbsp;')
332 traceback = traceback.replace('\n', '<br/>')
333 traceback = traceback.replace('\n', '<br/>')
333
334
334 ename = content['ename']
335 ename = content['ename']
335 ename_styled = '<span class="error">%s</span>' % ename
336 ename_styled = '<span class="error">%s</span>' % ename
336 traceback = traceback.replace(ename, ename_styled)
337 traceback = traceback.replace(ename, ename_styled)
337
338
338 self._append_html(traceback)
339 self._append_html(traceback)
339 else:
340 else:
340 # This is the fallback for now, using plain text with ansi escapes
341 # This is the fallback for now, using plain text with ansi escapes
341 self._append_plain_text(traceback)
342 self._append_plain_text(traceback)
342
343
343 def _process_execute_payload(self, item):
344 def _process_execute_payload(self, item):
344 """ Reimplemented to dispatch payloads to handler methods.
345 """ Reimplemented to dispatch payloads to handler methods.
345 """
346 """
346 handler = self._payload_handlers.get(item['source'])
347 handler = self._payload_handlers.get(item['source'])
347 if handler is None:
348 if handler is None:
348 # We have no handler for this type of payload, simply ignore it
349 # We have no handler for this type of payload, simply ignore it
349 return False
350 return False
350 else:
351 else:
351 handler(item)
352 handler(item)
352 return True
353 return True
353
354
354 def _show_interpreter_prompt(self, number=None):
355 def _show_interpreter_prompt(self, number=None):
355 """ Reimplemented for IPython-style prompts.
356 """ Reimplemented for IPython-style prompts.
356 """
357 """
357 # If a number was not specified, make a prompt number request.
358 # If a number was not specified, make a prompt number request.
358 if number is None:
359 if number is None:
359 msg_id = self.kernel_manager.shell_channel.execute('', silent=True)
360 msg_id = self.kernel_manager.shell_channel.execute('', silent=True)
360 info = self._ExecutionRequest(msg_id, 'prompt')
361 info = self._ExecutionRequest(msg_id, 'prompt')
361 self._request_info['execute'] = info
362 self._request_info['execute'][msg_id] = info
362 return
363 return
363
364
364 # Show a new prompt and save information about it so that it can be
365 # 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.
366 # updated later if the prompt number turns out to be wrong.
366 self._prompt_sep = self.input_sep
367 self._prompt_sep = self.input_sep
367 self._show_prompt(self._make_in_prompt(number), html=True)
368 self._show_prompt(self._make_in_prompt(number), html=True)
368 block = self._control.document().lastBlock()
369 block = self._control.document().lastBlock()
369 length = len(self._prompt)
370 length = len(self._prompt)
370 self._previous_prompt_obj = self._PromptBlock(block, length, number)
371 self._previous_prompt_obj = self._PromptBlock(block, length, number)
371
372
372 # Update continuation prompt to reflect (possibly) new prompt length.
373 # Update continuation prompt to reflect (possibly) new prompt length.
373 self._set_continuation_prompt(
374 self._set_continuation_prompt(
374 self._make_continuation_prompt(self._prompt), html=True)
375 self._make_continuation_prompt(self._prompt), html=True)
375
376
376 def _show_interpreter_prompt_for_reply(self, msg):
377 def _show_interpreter_prompt_for_reply(self, msg):
377 """ Reimplemented for IPython-style prompts.
378 """ Reimplemented for IPython-style prompts.
378 """
379 """
379 # Update the old prompt number if necessary.
380 # Update the old prompt number if necessary.
380 content = msg['content']
381 content = msg['content']
381 # abort replies do not have any keys:
382 # abort replies do not have any keys:
382 if content['status'] == 'aborted':
383 if content['status'] == 'aborted':
383 if self._previous_prompt_obj:
384 if self._previous_prompt_obj:
384 previous_prompt_number = self._previous_prompt_obj.number
385 previous_prompt_number = self._previous_prompt_obj.number
385 else:
386 else:
386 previous_prompt_number = 0
387 previous_prompt_number = 0
387 else:
388 else:
388 previous_prompt_number = content['execution_count']
389 previous_prompt_number = content['execution_count']
389 if self._previous_prompt_obj and \
390 if self._previous_prompt_obj and \
390 self._previous_prompt_obj.number != previous_prompt_number:
391 self._previous_prompt_obj.number != previous_prompt_number:
391 block = self._previous_prompt_obj.block
392 block = self._previous_prompt_obj.block
392
393
393 # Make sure the prompt block has not been erased.
394 # Make sure the prompt block has not been erased.
394 if block.isValid() and block.text():
395 if block.isValid() and block.text():
395
396
396 # Remove the old prompt and insert a new prompt.
397 # Remove the old prompt and insert a new prompt.
397 cursor = QtGui.QTextCursor(block)
398 cursor = QtGui.QTextCursor(block)
398 cursor.movePosition(QtGui.QTextCursor.Right,
399 cursor.movePosition(QtGui.QTextCursor.Right,
399 QtGui.QTextCursor.KeepAnchor,
400 QtGui.QTextCursor.KeepAnchor,
400 self._previous_prompt_obj.length)
401 self._previous_prompt_obj.length)
401 prompt = self._make_in_prompt(previous_prompt_number)
402 prompt = self._make_in_prompt(previous_prompt_number)
402 self._prompt = self._insert_html_fetching_plain_text(
403 self._prompt = self._insert_html_fetching_plain_text(
403 cursor, prompt)
404 cursor, prompt)
404
405
405 # When the HTML is inserted, Qt blows away the syntax
406 # When the HTML is inserted, Qt blows away the syntax
406 # highlighting for the line, so we need to rehighlight it.
407 # highlighting for the line, so we need to rehighlight it.
407 self._highlighter.rehighlightBlock(cursor.block())
408 self._highlighter.rehighlightBlock(cursor.block())
408
409
409 self._previous_prompt_obj = None
410 self._previous_prompt_obj = None
410
411
411 # Show a new prompt with the kernel's estimated prompt number.
412 # Show a new prompt with the kernel's estimated prompt number.
412 self._show_interpreter_prompt(previous_prompt_number + 1)
413 self._show_interpreter_prompt(previous_prompt_number + 1)
413
414
414 #---------------------------------------------------------------------------
415 #---------------------------------------------------------------------------
415 # 'IPythonWidget' interface
416 # 'IPythonWidget' interface
416 #---------------------------------------------------------------------------
417 #---------------------------------------------------------------------------
417
418
418 def set_default_style(self, colors='lightbg'):
419 def set_default_style(self, colors='lightbg'):
419 """ Sets the widget style to the class defaults.
420 """ Sets the widget style to the class defaults.
420
421
421 Parameters:
422 Parameters:
422 -----------
423 -----------
423 colors : str, optional (default lightbg)
424 colors : str, optional (default lightbg)
424 Whether to use the default IPython light background or dark
425 Whether to use the default IPython light background or dark
425 background or B&W style.
426 background or B&W style.
426 """
427 """
427 colors = colors.lower()
428 colors = colors.lower()
428 if colors=='lightbg':
429 if colors=='lightbg':
429 self.style_sheet = styles.default_light_style_sheet
430 self.style_sheet = styles.default_light_style_sheet
430 self.syntax_style = styles.default_light_syntax_style
431 self.syntax_style = styles.default_light_syntax_style
431 elif colors=='linux':
432 elif colors=='linux':
432 self.style_sheet = styles.default_dark_style_sheet
433 self.style_sheet = styles.default_dark_style_sheet
433 self.syntax_style = styles.default_dark_syntax_style
434 self.syntax_style = styles.default_dark_syntax_style
434 elif colors=='nocolor':
435 elif colors=='nocolor':
435 self.style_sheet = styles.default_bw_style_sheet
436 self.style_sheet = styles.default_bw_style_sheet
436 self.syntax_style = styles.default_bw_syntax_style
437 self.syntax_style = styles.default_bw_syntax_style
437 else:
438 else:
438 raise KeyError("No such color scheme: %s"%colors)
439 raise KeyError("No such color scheme: %s"%colors)
439
440
440 #---------------------------------------------------------------------------
441 #---------------------------------------------------------------------------
441 # 'IPythonWidget' protected interface
442 # 'IPythonWidget' protected interface
442 #---------------------------------------------------------------------------
443 #---------------------------------------------------------------------------
443
444
444 def _edit(self, filename, line=None):
445 def _edit(self, filename, line=None):
445 """ Opens a Python script for editing.
446 """ Opens a Python script for editing.
446
447
447 Parameters:
448 Parameters:
448 -----------
449 -----------
449 filename : str
450 filename : str
450 A path to a local system file.
451 A path to a local system file.
451
452
452 line : int, optional
453 line : int, optional
453 A line of interest in the file.
454 A line of interest in the file.
454 """
455 """
455 if self.custom_edit:
456 if self.custom_edit:
456 self.custom_edit_requested.emit(filename, line)
457 self.custom_edit_requested.emit(filename, line)
457 elif not self.editor:
458 elif not self.editor:
458 self._append_plain_text('No default editor available.\n'
459 self._append_plain_text('No default editor available.\n'
459 'Specify a GUI text editor in the `IPythonWidget.editor` '
460 'Specify a GUI text editor in the `IPythonWidget.editor` '
460 'configurable to enable the %edit magic')
461 'configurable to enable the %edit magic')
461 else:
462 else:
462 try:
463 try:
463 filename = '"%s"' % filename
464 filename = '"%s"' % filename
464 if line and self.editor_line:
465 if line and self.editor_line:
465 command = self.editor_line.format(filename=filename,
466 command = self.editor_line.format(filename=filename,
466 line=line)
467 line=line)
467 else:
468 else:
468 try:
469 try:
469 command = self.editor.format()
470 command = self.editor.format()
470 except KeyError:
471 except KeyError:
471 command = self.editor.format(filename=filename)
472 command = self.editor.format(filename=filename)
472 else:
473 else:
473 command += ' ' + filename
474 command += ' ' + filename
474 except KeyError:
475 except KeyError:
475 self._append_plain_text('Invalid editor command.\n')
476 self._append_plain_text('Invalid editor command.\n')
476 else:
477 else:
477 try:
478 try:
478 Popen(command, shell=True)
479 Popen(command, shell=True)
479 except OSError:
480 except OSError:
480 msg = 'Opening editor with command "%s" failed.\n'
481 msg = 'Opening editor with command "%s" failed.\n'
481 self._append_plain_text(msg % command)
482 self._append_plain_text(msg % command)
482
483
483 def _make_in_prompt(self, number):
484 def _make_in_prompt(self, number):
484 """ Given a prompt number, returns an HTML In prompt.
485 """ Given a prompt number, returns an HTML In prompt.
485 """
486 """
486 try:
487 try:
487 body = self.in_prompt % number
488 body = self.in_prompt % number
488 except TypeError:
489 except TypeError:
489 # allow in_prompt to leave out number, e.g. '>>> '
490 # allow in_prompt to leave out number, e.g. '>>> '
490 body = self.in_prompt
491 body = self.in_prompt
491 return '<span class="in-prompt">%s</span>' % body
492 return '<span class="in-prompt">%s</span>' % body
492
493
493 def _make_continuation_prompt(self, prompt):
494 def _make_continuation_prompt(self, prompt):
494 """ Given a plain text version of an In prompt, returns an HTML
495 """ Given a plain text version of an In prompt, returns an HTML
495 continuation prompt.
496 continuation prompt.
496 """
497 """
497 end_chars = '...: '
498 end_chars = '...: '
498 space_count = len(prompt.lstrip('\n')) - len(end_chars)
499 space_count = len(prompt.lstrip('\n')) - len(end_chars)
499 body = '&nbsp;' * space_count + end_chars
500 body = '&nbsp;' * space_count + end_chars
500 return '<span class="in-prompt">%s</span>' % body
501 return '<span class="in-prompt">%s</span>' % body
501
502
502 def _make_out_prompt(self, number):
503 def _make_out_prompt(self, number):
503 """ Given a prompt number, returns an HTML Out prompt.
504 """ Given a prompt number, returns an HTML Out prompt.
504 """
505 """
505 body = self.out_prompt % number
506 body = self.out_prompt % number
506 return '<span class="out-prompt">%s</span>' % body
507 return '<span class="out-prompt">%s</span>' % body
507
508
508 #------ Payload handlers --------------------------------------------------
509 #------ Payload handlers --------------------------------------------------
509
510
510 # Payload handlers with a generic interface: each takes the opaque payload
511 # Payload handlers with a generic interface: each takes the opaque payload
511 # dict, unpacks it and calls the underlying functions with the necessary
512 # dict, unpacks it and calls the underlying functions with the necessary
512 # arguments.
513 # arguments.
513
514
514 def _handle_payload_edit(self, item):
515 def _handle_payload_edit(self, item):
515 self._edit(item['filename'], item['line_number'])
516 self._edit(item['filename'], item['line_number'])
516
517
517 def _handle_payload_exit(self, item):
518 def _handle_payload_exit(self, item):
518 self._keep_kernel_on_exit = item['keepkernel']
519 self._keep_kernel_on_exit = item['keepkernel']
519 self.exit_requested.emit(self)
520 self.exit_requested.emit(self)
520
521
521 def _handle_payload_next_input(self, item):
522 def _handle_payload_next_input(self, item):
522 self.input_buffer = dedent(item['text'].rstrip())
523 self.input_buffer = dedent(item['text'].rstrip())
523
524
524 def _handle_payload_page(self, item):
525 def _handle_payload_page(self, item):
525 # Since the plain text widget supports only a very small subset of HTML
526 # 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
527 # and we have no control over the HTML source, we only page HTML
527 # payloads in the rich text widget.
528 # payloads in the rich text widget.
528 if item['html'] and self.kind == 'rich':
529 if item['html'] and self.kind == 'rich':
529 self._page(item['html'], html=True)
530 self._page(item['html'], html=True)
530 else:
531 else:
531 self._page(item['text'], html=False)
532 self._page(item['text'], html=False)
532
533
533 #------ Trait change handlers --------------------------------------------
534 #------ Trait change handlers --------------------------------------------
534
535
535 def _style_sheet_changed(self):
536 def _style_sheet_changed(self):
536 """ Set the style sheets of the underlying widgets.
537 """ Set the style sheets of the underlying widgets.
537 """
538 """
538 self.setStyleSheet(self.style_sheet)
539 self.setStyleSheet(self.style_sheet)
539 self._control.document().setDefaultStyleSheet(self.style_sheet)
540 self._control.document().setDefaultStyleSheet(self.style_sheet)
540 if self._page_control:
541 if self._page_control:
541 self._page_control.document().setDefaultStyleSheet(self.style_sheet)
542 self._page_control.document().setDefaultStyleSheet(self.style_sheet)
542
543
543 bg_color = self._control.palette().window().color()
544 bg_color = self._control.palette().window().color()
544 self._ansi_processor.set_background_color(bg_color)
545 self._ansi_processor.set_background_color(bg_color)
545
546
546
547
547 def _syntax_style_changed(self):
548 def _syntax_style_changed(self):
548 """ Set the style for the syntax highlighter.
549 """ Set the style for the syntax highlighter.
549 """
550 """
550 if self._highlighter is None:
551 if self._highlighter is None:
551 # ignore premature calls
552 # ignore premature calls
552 return
553 return
553 if self.syntax_style:
554 if self.syntax_style:
554 self._highlighter.set_style(self.syntax_style)
555 self._highlighter.set_style(self.syntax_style)
555 else:
556 else:
556 self._highlighter.set_style_sheet(self.style_sheet)
557 self._highlighter.set_style_sheet(self.style_sheet)
557
558
558 #------ Trait default initializers -----------------------------------------
559 #------ Trait default initializers -----------------------------------------
559
560
560 def _banner_default(self):
561 def _banner_default(self):
561 from IPython.core.usage import default_gui_banner
562 from IPython.core.usage import default_gui_banner
562 return default_gui_banner
563 return default_gui_banner
@@ -1,957 +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
629 self.pop = QtGui.QAction("&Update All Magic Menu ",
630 ########################################################################
630 self, triggered=self.update_all_magic_menu)
631 ## TEMPORARILY DISABLED - see #1057 for details. Uncomment this
631 self.add_menu_action(self.all_magic_menu, self.pop)
632 ## section when a proper fix is found
632 # we need to populate the 'Magic Menu' once the kernel has answer at
633
633 # least once let's do it immedialy, but it's assured to works
634 ## self.pop = QtGui.QAction("&Update All Magic Menu ",
634 self.pop.trigger()
635 ## self, triggered=self.update_all_magic_menu)
636 ## self.add_menu_action(self.all_magic_menu, self.pop)
637
638 ## END TEMPORARY FIX
639 ########################################################################
640
635
641 self.reset_action = QtGui.QAction("&Reset",
636 self.reset_action = QtGui.QAction("&Reset",
642 self,
637 self,
643 statusTip="Clear all varible from workspace",
638 statusTip="Clear all varible from workspace",
644 triggered=self.reset_magic_active_frontend)
639 triggered=self.reset_magic_active_frontend)
645 self.add_menu_action(self.magic_menu, self.reset_action)
640 self.add_menu_action(self.magic_menu, self.reset_action)
646
641
647 self.history_action = QtGui.QAction("&History",
642 self.history_action = QtGui.QAction("&History",
648 self,
643 self,
649 statusTip="show command history",
644 statusTip="show command history",
650 triggered=self.history_magic_active_frontend)
645 triggered=self.history_magic_active_frontend)
651 self.add_menu_action(self.magic_menu, self.history_action)
646 self.add_menu_action(self.magic_menu, self.history_action)
652
647
653 self.save_action = QtGui.QAction("E&xport History ",
648 self.save_action = QtGui.QAction("E&xport History ",
654 self,
649 self,
655 statusTip="Export History as Python File",
650 statusTip="Export History as Python File",
656 triggered=self.save_magic_active_frontend)
651 triggered=self.save_magic_active_frontend)
657 self.add_menu_action(self.magic_menu, self.save_action)
652 self.add_menu_action(self.magic_menu, self.save_action)
658
653
659 self.who_action = QtGui.QAction("&Who",
654 self.who_action = QtGui.QAction("&Who",
660 self,
655 self,
661 statusTip="List interactive variable",
656 statusTip="List interactive variable",
662 triggered=self.who_magic_active_frontend)
657 triggered=self.who_magic_active_frontend)
663 self.add_menu_action(self.magic_menu, self.who_action)
658 self.add_menu_action(self.magic_menu, self.who_action)
664
659
665 self.who_ls_action = QtGui.QAction("Wh&o ls",
660 self.who_ls_action = QtGui.QAction("Wh&o ls",
666 self,
661 self,
667 statusTip="Return a list of interactive variable",
662 statusTip="Return a list of interactive variable",
668 triggered=self.who_ls_magic_active_frontend)
663 triggered=self.who_ls_magic_active_frontend)
669 self.add_menu_action(self.magic_menu, self.who_ls_action)
664 self.add_menu_action(self.magic_menu, self.who_ls_action)
670
665
671 self.whos_action = QtGui.QAction("Who&s",
666 self.whos_action = QtGui.QAction("Who&s",
672 self,
667 self,
673 statusTip="List interactive variable with detail",
668 statusTip="List interactive variable with detail",
674 triggered=self.whos_magic_active_frontend)
669 triggered=self.whos_magic_active_frontend)
675 self.add_menu_action(self.magic_menu, self.whos_action)
670 self.add_menu_action(self.magic_menu, self.whos_action)
676
671
677
678 ########################################################################
679 ## TEMPORARILY ADDED BACK - see #1057 for details. The magic menu is
680 ## supposed to be dynamic, but the mechanism merged in #1057 has a race
681 ## condition that locks up the console very often. We're putting back
682 ## the static list temporarily/ Remove this code when a proper fix is
683 ## found.
684
685 # allmagics submenu.
686
687 # for now this is just a copy and paste, but we should get this
688 # dynamically
689 magiclist = ["%alias", "%autocall", "%automagic", "%bookmark", "%cd",
690 "%clear", "%colors", "%debug", "%dhist", "%dirs", "%doctest_mode",
691 "%ed", "%edit", "%env", "%gui", "%guiref", "%hist", "%history",
692 "%install_default_config", "%install_profiles", "%less", "%load_ext",
693 "%loadpy", "%logoff", "%logon", "%logstart", "%logstate", "%logstop",
694 "%lsmagic", "%macro", "%magic", "%man", "%more", "%notebook", "%page",
695 "%pastebin", "%pdb", "%pdef", "%pdoc", "%pfile", "%pinfo", "%pinfo2",
696 "%popd", "%pprint", "%precision", "%profile", "%prun", "%psearch",
697 "%psource", "%pushd", "%pwd", "%pycat", "%pylab", "%quickref",
698 "%recall", "%rehashx", "%reload_ext", "%rep", "%rerun", "%reset",
699 "%reset_selective", "%run", "%save", "%sc", "%sx", "%tb", "%time",
700 "%timeit", "%unalias", "%unload_ext", "%who", "%who_ls", "%whos",
701 "%xdel", "%xmode"]
702
703 def make_dynamic_magic(i):
704 def inner_dynamic_magic():
705 self.active_frontend.execute(i)
706 inner_dynamic_magic.__name__ = "dynamics_magic_%s" % i
707 return inner_dynamic_magic
708
709 for magic in magiclist:
710 xaction = QtGui.QAction(magic,
711 self,
712 triggered=make_dynamic_magic(magic)
713 )
714 self.all_magic_menu.addAction(xaction)
715
716 ## END TEMPORARY FIX
717 ########################################################################
718
719
720 def init_window_menu(self):
672 def init_window_menu(self):
721 self.window_menu = self.menuBar().addMenu("&Window")
673 self.window_menu = self.menuBar().addMenu("&Window")
722 if sys.platform == 'darwin':
674 if sys.platform == 'darwin':
723 # add min/maximize actions to OSX, which lacks default bindings.
675 # add min/maximize actions to OSX, which lacks default bindings.
724 self.minimizeAct = QtGui.QAction("Mini&mize",
676 self.minimizeAct = QtGui.QAction("Mini&mize",
725 self,
677 self,
726 shortcut="Ctrl+m",
678 shortcut="Ctrl+m",
727 statusTip="Minimize the window/Restore Normal Size",
679 statusTip="Minimize the window/Restore Normal Size",
728 triggered=self.toggleMinimized)
680 triggered=self.toggleMinimized)
729 # maximize is called 'Zoom' on OSX for some reason
681 # maximize is called 'Zoom' on OSX for some reason
730 self.maximizeAct = QtGui.QAction("&Zoom",
682 self.maximizeAct = QtGui.QAction("&Zoom",
731 self,
683 self,
732 shortcut="Ctrl+Shift+M",
684 shortcut="Ctrl+Shift+M",
733 statusTip="Maximize the window/Restore Normal Size",
685 statusTip="Maximize the window/Restore Normal Size",
734 triggered=self.toggleMaximized)
686 triggered=self.toggleMaximized)
735
687
736 self.add_menu_action(self.window_menu, self.minimizeAct)
688 self.add_menu_action(self.window_menu, self.minimizeAct)
737 self.add_menu_action(self.window_menu, self.maximizeAct)
689 self.add_menu_action(self.window_menu, self.maximizeAct)
738 self.window_menu.addSeparator()
690 self.window_menu.addSeparator()
739
691
740 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"
741 self.prev_tab_act = QtGui.QAction("Pre&vious Tab",
693 self.prev_tab_act = QtGui.QAction("Pre&vious Tab",
742 self,
694 self,
743 shortcut=prev_key,
695 shortcut=prev_key,
744 statusTip="Select previous tab",
696 statusTip="Select previous tab",
745 triggered=self.prev_tab)
697 triggered=self.prev_tab)
746 self.add_menu_action(self.window_menu, self.prev_tab_act)
698 self.add_menu_action(self.window_menu, self.prev_tab_act)
747
699
748 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"
749 self.next_tab_act = QtGui.QAction("Ne&xt Tab",
701 self.next_tab_act = QtGui.QAction("Ne&xt Tab",
750 self,
702 self,
751 shortcut=next_key,
703 shortcut=next_key,
752 statusTip="Select next tab",
704 statusTip="Select next tab",
753 triggered=self.next_tab)
705 triggered=self.next_tab)
754 self.add_menu_action(self.window_menu, self.next_tab_act)
706 self.add_menu_action(self.window_menu, self.next_tab_act)
755
707
756 def init_help_menu(self):
708 def init_help_menu(self):
757 # 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
758 # automatically contain a search field to search inside menus and
710 # automatically contain a search field to search inside menus and
759 # 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
760 # a QAction.MenuRole like HelpMenuRole otherwise it will loose
712 # a QAction.MenuRole like HelpMenuRole otherwise it will loose
761 # this search field fonctionality
713 # this search field fonctionality
762
714
763 self.help_menu = self.menuBar().addMenu("&Help")
715 self.help_menu = self.menuBar().addMenu("&Help")
764
716
765
717
766 # Help Menu
718 # Help Menu
767
719
768 self.intro_active_frontend_action = QtGui.QAction("&Intro to IPython",
720 self.intro_active_frontend_action = QtGui.QAction("&Intro to IPython",
769 self,
721 self,
770 triggered=self.intro_active_frontend
722 triggered=self.intro_active_frontend
771 )
723 )
772 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)
773
725
774 self.quickref_active_frontend_action = QtGui.QAction("IPython &Cheat Sheet",
726 self.quickref_active_frontend_action = QtGui.QAction("IPython &Cheat Sheet",
775 self,
727 self,
776 triggered=self.quickref_active_frontend
728 triggered=self.quickref_active_frontend
777 )
729 )
778 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)
779
731
780 self.guiref_active_frontend_action = QtGui.QAction("&Qt Console",
732 self.guiref_active_frontend_action = QtGui.QAction("&Qt Console",
781 self,
733 self,
782 triggered=self.guiref_active_frontend
734 triggered=self.guiref_active_frontend
783 )
735 )
784 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)
785
737
786 self.onlineHelpAct = QtGui.QAction("Open Online &Help",
738 self.onlineHelpAct = QtGui.QAction("Open Online &Help",
787 self,
739 self,
788 triggered=self._open_online_help)
740 triggered=self._open_online_help)
789 self.add_menu_action(self.help_menu, self.onlineHelpAct)
741 self.add_menu_action(self.help_menu, self.onlineHelpAct)
790
742
791 # minimize/maximize/fullscreen actions:
743 # minimize/maximize/fullscreen actions:
792
744
793 def toggle_menu_bar(self):
745 def toggle_menu_bar(self):
794 menu_bar = self.menuBar()
746 menu_bar = self.menuBar()
795 if menu_bar.isVisible():
747 if menu_bar.isVisible():
796 menu_bar.setVisible(False)
748 menu_bar.setVisible(False)
797 else:
749 else:
798 menu_bar.setVisible(True)
750 menu_bar.setVisible(True)
799
751
800 def toggleMinimized(self):
752 def toggleMinimized(self):
801 if not self.isMinimized():
753 if not self.isMinimized():
802 self.showMinimized()
754 self.showMinimized()
803 else:
755 else:
804 self.showNormal()
756 self.showNormal()
805
757
806 def _open_online_help(self):
758 def _open_online_help(self):
807 filename="http://ipython.org/ipython-doc/stable/index.html"
759 filename="http://ipython.org/ipython-doc/stable/index.html"
808 webbrowser.open(filename, new=1, autoraise=True)
760 webbrowser.open(filename, new=1, autoraise=True)
809
761
810 def toggleMaximized(self):
762 def toggleMaximized(self):
811 if not self.isMaximized():
763 if not self.isMaximized():
812 self.showMaximized()
764 self.showMaximized()
813 else:
765 else:
814 self.showNormal()
766 self.showNormal()
815
767
816 # Min/Max imizing while in full screen give a bug
768 # Min/Max imizing while in full screen give a bug
817 # when going out of full screen, at least on OSX
769 # when going out of full screen, at least on OSX
818 def toggleFullScreen(self):
770 def toggleFullScreen(self):
819 if not self.isFullScreen():
771 if not self.isFullScreen():
820 self.showFullScreen()
772 self.showFullScreen()
821 if sys.platform == 'darwin':
773 if sys.platform == 'darwin':
822 self.maximizeAct.setEnabled(False)
774 self.maximizeAct.setEnabled(False)
823 self.minimizeAct.setEnabled(False)
775 self.minimizeAct.setEnabled(False)
824 else:
776 else:
825 self.showNormal()
777 self.showNormal()
826 if sys.platform == 'darwin':
778 if sys.platform == 'darwin':
827 self.maximizeAct.setEnabled(True)
779 self.maximizeAct.setEnabled(True)
828 self.minimizeAct.setEnabled(True)
780 self.minimizeAct.setEnabled(True)
829
781
830 def close_active_frontend(self):
782 def close_active_frontend(self):
831 self.close_tab(self.active_frontend)
783 self.close_tab(self.active_frontend)
832
784
833 def restart_kernel_active_frontend(self):
785 def restart_kernel_active_frontend(self):
834 self.active_frontend.request_restart_kernel()
786 self.active_frontend.request_restart_kernel()
835
787
836 def interrupt_kernel_active_frontend(self):
788 def interrupt_kernel_active_frontend(self):
837 self.active_frontend.request_interrupt_kernel()
789 self.active_frontend.request_interrupt_kernel()
838
790
839 def cut_active_frontend(self):
791 def cut_active_frontend(self):
840 widget = self.active_frontend
792 widget = self.active_frontend
841 if widget.can_cut():
793 if widget.can_cut():
842 widget.cut()
794 widget.cut()
843
795
844 def copy_active_frontend(self):
796 def copy_active_frontend(self):
845 widget = self.active_frontend
797 widget = self.active_frontend
846 if widget.can_copy():
798 if widget.can_copy():
847 widget.copy()
799 widget.copy()
848
800
849 def copy_raw_active_frontend(self):
801 def copy_raw_active_frontend(self):
850 self.active_frontend._copy_raw_action.trigger()
802 self.active_frontend._copy_raw_action.trigger()
851
803
852 def paste_active_frontend(self):
804 def paste_active_frontend(self):
853 widget = self.active_frontend
805 widget = self.active_frontend
854 if widget.can_paste():
806 if widget.can_paste():
855 widget.paste()
807 widget.paste()
856
808
857 def undo_active_frontend(self):
809 def undo_active_frontend(self):
858 self.active_frontend.undo()
810 self.active_frontend.undo()
859
811
860 def redo_active_frontend(self):
812 def redo_active_frontend(self):
861 self.active_frontend.redo()
813 self.active_frontend.redo()
862
814
863 def reset_magic_active_frontend(self):
815 def reset_magic_active_frontend(self):
864 self.active_frontend.execute("%reset")
816 self.active_frontend.execute("%reset")
865
817
866 def history_magic_active_frontend(self):
818 def history_magic_active_frontend(self):
867 self.active_frontend.execute("%history")
819 self.active_frontend.execute("%history")
868
820
869 def save_magic_active_frontend(self):
821 def save_magic_active_frontend(self):
870 self.active_frontend.save_magic()
822 self.active_frontend.save_magic()
871
823
872 def clear_magic_active_frontend(self):
824 def clear_magic_active_frontend(self):
873 self.active_frontend.execute("%clear")
825 self.active_frontend.execute("%clear")
874
826
875 def who_magic_active_frontend(self):
827 def who_magic_active_frontend(self):
876 self.active_frontend.execute("%who")
828 self.active_frontend.execute("%who")
877
829
878 def who_ls_magic_active_frontend(self):
830 def who_ls_magic_active_frontend(self):
879 self.active_frontend.execute("%who_ls")
831 self.active_frontend.execute("%who_ls")
880
832
881 def whos_magic_active_frontend(self):
833 def whos_magic_active_frontend(self):
882 self.active_frontend.execute("%whos")
834 self.active_frontend.execute("%whos")
883
835
884 def print_action_active_frontend(self):
836 def print_action_active_frontend(self):
885 self.active_frontend.print_action.trigger()
837 self.active_frontend.print_action.trigger()
886
838
887 def export_action_active_frontend(self):
839 def export_action_active_frontend(self):
888 self.active_frontend.export_action.trigger()
840 self.active_frontend.export_action.trigger()
889
841
890 def select_all_active_frontend(self):
842 def select_all_active_frontend(self):
891 self.active_frontend.select_all_action.trigger()
843 self.active_frontend.select_all_action.trigger()
892
844
893 def increase_font_size_active_frontend(self):
845 def increase_font_size_active_frontend(self):
894 self.active_frontend.increase_font_size.trigger()
846 self.active_frontend.increase_font_size.trigger()
895
847
896 def decrease_font_size_active_frontend(self):
848 def decrease_font_size_active_frontend(self):
897 self.active_frontend.decrease_font_size.trigger()
849 self.active_frontend.decrease_font_size.trigger()
898
850
899 def reset_font_size_active_frontend(self):
851 def reset_font_size_active_frontend(self):
900 self.active_frontend.reset_font_size.trigger()
852 self.active_frontend.reset_font_size.trigger()
901
853
902 def guiref_active_frontend(self):
854 def guiref_active_frontend(self):
903 self.active_frontend.execute("%guiref")
855 self.active_frontend.execute("%guiref")
904
856
905 def intro_active_frontend(self):
857 def intro_active_frontend(self):
906 self.active_frontend.execute("?")
858 self.active_frontend.execute("?")
907
859
908 def quickref_active_frontend(self):
860 def quickref_active_frontend(self):
909 self.active_frontend.execute("%quickref")
861 self.active_frontend.execute("%quickref")
910 #---------------------------------------------------------------------------
862 #---------------------------------------------------------------------------
911 # QWidget interface
863 # QWidget interface
912 #---------------------------------------------------------------------------
864 #---------------------------------------------------------------------------
913
865
914 def closeEvent(self, event):
866 def closeEvent(self, event):
915 """ Forward the close event to every tabs contained by the windows
867 """ Forward the close event to every tabs contained by the windows
916 """
868 """
917 if self.tab_widget.count() == 0:
869 if self.tab_widget.count() == 0:
918 # no tabs, just close
870 # no tabs, just close
919 event.accept()
871 event.accept()
920 return
872 return
921 # 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
922 title = self.window().windowTitle()
874 title = self.window().windowTitle()
923 cancel = QtGui.QMessageBox.Cancel
875 cancel = QtGui.QMessageBox.Cancel
924 okay = QtGui.QMessageBox.Ok
876 okay = QtGui.QMessageBox.Ok
925
877
926 if self.confirm_exit:
878 if self.confirm_exit:
927 if self.tab_widget.count() > 1:
879 if self.tab_widget.count() > 1:
928 msg = "Close all tabs, stop all kernels, and Quit?"
880 msg = "Close all tabs, stop all kernels, and Quit?"
929 else:
881 else:
930 msg = "Close console, stop kernel, and Quit?"
882 msg = "Close console, stop kernel, and Quit?"
931 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."
932 closeall = QtGui.QPushButton("&Yes, quit everything", self)
884 closeall = QtGui.QPushButton("&Yes, quit everything", self)
933 closeall.setShortcut('Y')
885 closeall.setShortcut('Y')
934 box = QtGui.QMessageBox(QtGui.QMessageBox.Question,
886 box = QtGui.QMessageBox(QtGui.QMessageBox.Question,
935 title, msg)
887 title, msg)
936 box.setInformativeText(info)
888 box.setInformativeText(info)
937 box.addButton(cancel)
889 box.addButton(cancel)
938 box.addButton(closeall, QtGui.QMessageBox.YesRole)
890 box.addButton(closeall, QtGui.QMessageBox.YesRole)
939 box.setDefaultButton(closeall)
891 box.setDefaultButton(closeall)
940 box.setEscapeButton(cancel)
892 box.setEscapeButton(cancel)
941 pixmap = QtGui.QPixmap(self._app.icon.pixmap(QtCore.QSize(64,64)))
893 pixmap = QtGui.QPixmap(self._app.icon.pixmap(QtCore.QSize(64,64)))
942 box.setIconPixmap(pixmap)
894 box.setIconPixmap(pixmap)
943 reply = box.exec_()
895 reply = box.exec_()
944 else:
896 else:
945 reply = okay
897 reply = okay
946
898
947 if reply == cancel:
899 if reply == cancel:
948 event.ignore()
900 event.ignore()
949 return
901 return
950 if reply == okay:
902 if reply == okay:
951 while self.tab_widget.count() >= 1:
903 while self.tab_widget.count() >= 1:
952 # prevent further confirmations:
904 # prevent further confirmations:
953 widget = self.active_frontend
905 widget = self.active_frontend
954 widget._confirm_exit = False
906 widget._confirm_exit = False
955 self.close_tab(widget)
907 self.close_tab(widget)
956 event.accept()
908 event.accept()
957
909
@@ -1,559 +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
456 # least once
457
458 ########################################################################
459 ## TEMPORARILY DISABLED - see #1057 for details, uncomment the next
460 ## line when a proper fix is found:
461 ## self.kernel_manager.shell_channel.first_reply.connect(self.window.pop.trigger)
462 ## END TEMPORARY FIX
463 ########################################################################
464
465 self.window.setWindowTitle('Python' if self.pure else 'IPython')
455 self.window.setWindowTitle('Python' if self.pure else 'IPython')
466
456
467 def init_colors(self):
457 def init_colors(self):
468 """Configure the coloring of the widget"""
458 """Configure the coloring of the widget"""
469 # Note: This will be dramatically simplified when colors
459 # Note: This will be dramatically simplified when colors
470 # are removed from the backend.
460 # are removed from the backend.
471
461
472 if self.pure:
462 if self.pure:
473 # only IPythonWidget supports styling
463 # only IPythonWidget supports styling
474 return
464 return
475
465
476 # parse the colors arg down to current known labels
466 # parse the colors arg down to current known labels
477 try:
467 try:
478 colors = self.config.ZMQInteractiveShell.colors
468 colors = self.config.ZMQInteractiveShell.colors
479 except AttributeError:
469 except AttributeError:
480 colors = None
470 colors = None
481 try:
471 try:
482 style = self.config.IPythonWidget.syntax_style
472 style = self.config.IPythonWidget.syntax_style
483 except AttributeError:
473 except AttributeError:
484 style = None
474 style = None
485
475
486 # find the value for colors:
476 # find the value for colors:
487 if colors:
477 if colors:
488 colors=colors.lower()
478 colors=colors.lower()
489 if colors in ('lightbg', 'light'):
479 if colors in ('lightbg', 'light'):
490 colors='lightbg'
480 colors='lightbg'
491 elif colors in ('dark', 'linux'):
481 elif colors in ('dark', 'linux'):
492 colors='linux'
482 colors='linux'
493 else:
483 else:
494 colors='nocolor'
484 colors='nocolor'
495 elif style:
485 elif style:
496 if style=='bw':
486 if style=='bw':
497 colors='nocolor'
487 colors='nocolor'
498 elif styles.dark_style(style):
488 elif styles.dark_style(style):
499 colors='linux'
489 colors='linux'
500 else:
490 else:
501 colors='lightbg'
491 colors='lightbg'
502 else:
492 else:
503 colors=None
493 colors=None
504
494
505 # Configure the style.
495 # Configure the style.
506 widget = self.widget
496 widget = self.widget
507 if style:
497 if style:
508 widget.style_sheet = styles.sheet_from_template(style, colors)
498 widget.style_sheet = styles.sheet_from_template(style, colors)
509 widget.syntax_style = style
499 widget.syntax_style = style
510 widget._syntax_style_changed()
500 widget._syntax_style_changed()
511 widget._style_sheet_changed()
501 widget._style_sheet_changed()
512 elif colors:
502 elif colors:
513 # use a default style
503 # use a default style
514 widget.set_default_style(colors=colors)
504 widget.set_default_style(colors=colors)
515 else:
505 else:
516 # this is redundant for now, but allows the widget's
506 # this is redundant for now, but allows the widget's
517 # defaults to change
507 # defaults to change
518 widget.set_default_style()
508 widget.set_default_style()
519
509
520 if self.stylesheet:
510 if self.stylesheet:
521 # we got an expicit stylesheet
511 # we got an expicit stylesheet
522 if os.path.isfile(self.stylesheet):
512 if os.path.isfile(self.stylesheet):
523 with open(self.stylesheet) as f:
513 with open(self.stylesheet) as f:
524 sheet = f.read()
514 sheet = f.read()
525 widget.style_sheet = sheet
515 widget.style_sheet = sheet
526 widget._style_sheet_changed()
516 widget._style_sheet_changed()
527 else:
517 else:
528 raise IOError("Stylesheet %r not found."%self.stylesheet)
518 raise IOError("Stylesheet %r not found."%self.stylesheet)
529
519
530 @catch_config_error
520 @catch_config_error
531 def initialize(self, argv=None):
521 def initialize(self, argv=None):
532 super(IPythonQtConsoleApp, self).initialize(argv)
522 super(IPythonQtConsoleApp, self).initialize(argv)
533 self.init_connection_file()
523 self.init_connection_file()
534 default_secure(self.config)
524 default_secure(self.config)
535 self.init_ssh()
525 self.init_ssh()
536 self.init_kernel_manager()
526 self.init_kernel_manager()
537 self.init_qt_elements()
527 self.init_qt_elements()
538 self.init_colors()
528 self.init_colors()
539
529
540 def start(self):
530 def start(self):
541
531
542 # draw the window
532 # draw the window
543 self.window.show()
533 self.window.show()
544
534
545 # Start the application main loop.
535 # Start the application main loop.
546 self.app.exec_()
536 self.app.exec_()
547
537
548 #-----------------------------------------------------------------------------
538 #-----------------------------------------------------------------------------
549 # Main entry point
539 # Main entry point
550 #-----------------------------------------------------------------------------
540 #-----------------------------------------------------------------------------
551
541
552 def main():
542 def main():
553 app = IPythonQtConsoleApp()
543 app = IPythonQtConsoleApp()
554 app.initialize()
544 app.initialize()
555 app.start()
545 app.start()
556
546
557
547
558 if __name__ == '__main__':
548 if __name__ == '__main__':
559 main()
549 main()
General Comments 0
You need to be logged in to leave comments. Login now