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