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