##// END OF EJS Templates
fix docstrig, replace eval by regExp...
Matthias BUSSONNIER -
Show More
@@ -1,711 +1,728 b''
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, transform_classic_prompt
16 16 from IPython.core.oinspect import call_tip
17 17 from IPython.frontend.qt.base_frontend_mixin import BaseFrontendMixin
18 18 from IPython.utils.traitlets import Bool, Instance, Unicode
19 19 from bracket_matcher import BracketMatcher
20 20 from call_tip_widget import CallTipWidget
21 21 from completion_lexer import CompletionLexer
22 22 from history_console_widget import HistoryConsoleWidget
23 23 from pygments_highlighter import PygmentsHighlighter
24 24
25 25
26 26 class FrontendHighlighter(PygmentsHighlighter):
27 27 """ A PygmentsHighlighter that understands and ignores prompts.
28 28 """
29 29
30 30 def __init__(self, frontend):
31 31 super(FrontendHighlighter, self).__init__(frontend._control.document())
32 32 self._current_offset = 0
33 33 self._frontend = frontend
34 34 self.highlighting_on = False
35 35
36 36 def highlightBlock(self, string):
37 37 """ Highlight a block of text. Reimplemented to highlight selectively.
38 38 """
39 39 if not self.highlighting_on:
40 40 return
41 41
42 42 # The input to this function is a unicode string that may contain
43 43 # paragraph break characters, non-breaking spaces, etc. Here we acquire
44 44 # the string as plain text so we can compare it.
45 45 current_block = self.currentBlock()
46 46 string = self._frontend._get_block_plain_text(current_block)
47 47
48 48 # Decide whether to check for the regular or continuation prompt.
49 49 if current_block.contains(self._frontend._prompt_pos):
50 50 prompt = self._frontend._prompt
51 51 else:
52 52 prompt = self._frontend._continuation_prompt
53 53
54 54 # Only highlight if we can identify a prompt, but make sure not to
55 55 # highlight the prompt.
56 56 if string.startswith(prompt):
57 57 self._current_offset = len(prompt)
58 58 string = string[len(prompt):]
59 59 super(FrontendHighlighter, self).highlightBlock(string)
60 60
61 61 def rehighlightBlock(self, block):
62 62 """ Reimplemented to temporarily enable highlighting if disabled.
63 63 """
64 64 old = self.highlighting_on
65 65 self.highlighting_on = True
66 66 super(FrontendHighlighter, self).rehighlightBlock(block)
67 67 self.highlighting_on = old
68 68
69 69 def setFormat(self, start, count, format):
70 70 """ Reimplemented to highlight selectively.
71 71 """
72 72 start += self._current_offset
73 73 super(FrontendHighlighter, self).setFormat(start, count, format)
74 74
75 75
76 76 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
77 77 """ A Qt frontend for a generic Python kernel.
78 78 """
79 79
80 80 # The text to show when the kernel is (re)started.
81 81 banner = Unicode()
82 82
83 83 # An option and corresponding signal for overriding the default kernel
84 84 # interrupt behavior.
85 85 custom_interrupt = Bool(False)
86 86 custom_interrupt_requested = QtCore.Signal()
87 87
88 88 # An option and corresponding signals for overriding the default kernel
89 89 # restart behavior.
90 90 custom_restart = Bool(False)
91 91 custom_restart_kernel_died = QtCore.Signal(float)
92 92 custom_restart_requested = QtCore.Signal()
93 93
94 94 # Whether to automatically show calltips on open-parentheses.
95 95 enable_calltips = Bool(True, config=True,
96 96 help="Whether to draw information calltips on open-parentheses.")
97 97
98 98 # Emitted when a user visible 'execute_request' has been submitted to the
99 99 # kernel from the FrontendWidget. Contains the code to be executed.
100 100 executing = QtCore.Signal(object)
101 101
102 102 # Emitted when a user-visible 'execute_reply' has been received from the
103 103 # kernel and processed by the FrontendWidget. Contains the response message.
104 104 executed = QtCore.Signal(object)
105 105
106 106 # Emitted when an exit request has been received from the kernel.
107 107 exit_requested = QtCore.Signal(object)
108 108
109 109 # Protected class variables.
110 110 _CallTipRequest = namedtuple('_CallTipRequest', ['id', 'pos'])
111 111 _CompletionRequest = namedtuple('_CompletionRequest', ['id', 'pos'])
112 112 _ExecutionRequest = namedtuple('_ExecutionRequest', ['id', 'kind'])
113 113 _input_splitter_class = InputSplitter
114 114 _local_kernel = False
115 115 _highlighter = Instance(FrontendHighlighter)
116 116
117 117 #---------------------------------------------------------------------------
118 118 # 'object' interface
119 119 #---------------------------------------------------------------------------
120 120
121 121 def __init__(self, *args, **kw):
122 122 super(FrontendWidget, self).__init__(*args, **kw)
123 123 # FIXME: remove this when PySide min version is updated past 1.0.7
124 124 # forcefully disable calltips if PySide is < 1.0.7, because they crash
125 125 if qt.QT_API == qt.QT_API_PYSIDE:
126 126 import PySide
127 127 if PySide.__version_info__ < (1,0,7):
128 128 self.log.warn("PySide %s < 1.0.7 detected, disabling calltips" % PySide.__version__)
129 129 self.enable_calltips = False
130 130
131 131 # FrontendWidget protected variables.
132 132 self._bracket_matcher = BracketMatcher(self._control)
133 133 self._call_tip_widget = CallTipWidget(self._control)
134 134 self._completion_lexer = CompletionLexer(PythonLexer())
135 135 self._copy_raw_action = QtGui.QAction('Copy (Raw Text)', None)
136 136 self._hidden = False
137 137 self._highlighter = FrontendHighlighter(self)
138 138 self._input_splitter = self._input_splitter_class(input_mode='cell')
139 139 self._kernel_manager = None
140 140 self._request_info = {}
141 141 self._callback_dict = {}
142 142
143 143 # Configure the ConsoleWidget.
144 144 self.tab_width = 4
145 145 self._set_continuation_prompt('... ')
146 146
147 147 # Configure the CallTipWidget.
148 148 self._call_tip_widget.setFont(self.font)
149 149 self.font_changed.connect(self._call_tip_widget.setFont)
150 150
151 151 # Configure actions.
152 152 action = self._copy_raw_action
153 153 key = QtCore.Qt.CTRL | QtCore.Qt.SHIFT | QtCore.Qt.Key_C
154 154 action.setEnabled(False)
155 155 action.setShortcut(QtGui.QKeySequence(key))
156 156 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
157 157 action.triggered.connect(self.copy_raw)
158 158 self.copy_available.connect(action.setEnabled)
159 159 self.addAction(action)
160 160
161 161 # Connect signal handlers.
162 162 document = self._control.document()
163 163 document.contentsChange.connect(self._document_contents_change)
164 164
165 165 # Set flag for whether we are connected via localhost.
166 166 self._local_kernel = kw.get('local_kernel',
167 167 FrontendWidget._local_kernel)
168 168
169 169 #---------------------------------------------------------------------------
170 170 # 'ConsoleWidget' public interface
171 171 #---------------------------------------------------------------------------
172 172
173 173 def copy(self):
174 174 """ Copy the currently selected text to the clipboard, removing prompts.
175 175 """
176 176 text = self._control.textCursor().selection().toPlainText()
177 177 if text:
178 178 lines = map(transform_classic_prompt, text.splitlines())
179 179 text = '\n'.join(lines)
180 180 QtGui.QApplication.clipboard().setText(text)
181 181
182 182 #---------------------------------------------------------------------------
183 183 # 'ConsoleWidget' abstract interface
184 184 #---------------------------------------------------------------------------
185 185
186 186 def _is_complete(self, source, interactive):
187 187 """ Returns whether 'source' can be completely processed and a new
188 188 prompt created. When triggered by an Enter/Return key press,
189 189 'interactive' is True; otherwise, it is False.
190 190 """
191 191 complete = self._input_splitter.push(source)
192 192 if interactive:
193 193 complete = not self._input_splitter.push_accepts_more()
194 194 return complete
195 195
196 196 def _execute(self, source, hidden):
197 197 """ Execute 'source'. If 'hidden', do not show any output.
198 198
199 199 See parent class :meth:`execute` docstring for full details.
200 200 """
201 201 msg_id = self.kernel_manager.shell_channel.execute(source, hidden)
202 202 self._request_info['execute'] = self._ExecutionRequest(msg_id, 'user')
203 203 self._hidden = hidden
204 204 if not hidden:
205 205 self.executing.emit(source)
206 206
207 207 def _prompt_started_hook(self):
208 208 """ Called immediately after a new prompt is displayed.
209 209 """
210 210 if not self._reading:
211 211 self._highlighter.highlighting_on = True
212 212
213 213 def _prompt_finished_hook(self):
214 214 """ Called immediately after a prompt is finished, i.e. when some input
215 215 will be processed and a new prompt displayed.
216 216 """
217 217 # Flush all state from the input splitter so the next round of
218 218 # reading input starts with a clean buffer.
219 219 self._input_splitter.reset()
220 220
221 221 if not self._reading:
222 222 self._highlighter.highlighting_on = False
223 223
224 224 def _tab_pressed(self):
225 225 """ Called when the tab key is pressed. Returns whether to continue
226 226 processing the event.
227 227 """
228 228 # Perform tab completion if:
229 229 # 1) The cursor is in the input buffer.
230 230 # 2) There is a non-whitespace character before the cursor.
231 231 text = self._get_input_buffer_cursor_line()
232 232 if text is None:
233 233 return False
234 234 complete = bool(text[:self._get_input_buffer_cursor_column()].strip())
235 235 if complete:
236 236 self._complete()
237 237 return not complete
238 238
239 239 #---------------------------------------------------------------------------
240 240 # 'ConsoleWidget' protected interface
241 241 #---------------------------------------------------------------------------
242 242
243 243 def _context_menu_make(self, pos):
244 244 """ Reimplemented to add an action for raw copy.
245 245 """
246 246 menu = super(FrontendWidget, self)._context_menu_make(pos)
247 247 for before_action in menu.actions():
248 248 if before_action.shortcut().matches(QtGui.QKeySequence.Paste) == \
249 249 QtGui.QKeySequence.ExactMatch:
250 250 menu.insertAction(before_action, self._copy_raw_action)
251 251 break
252 252 return menu
253 253
254 254 def request_interrupt_kernel(self):
255 255 if self._executing:
256 256 self.interrupt_kernel()
257 257
258 258 def request_restart_kernel(self):
259 259 message = 'Are you sure you want to restart the kernel?'
260 260 self.restart_kernel(message, now=False)
261 261
262 262 def _event_filter_console_keypress(self, event):
263 263 """ Reimplemented for execution interruption and smart backspace.
264 264 """
265 265 key = event.key()
266 266 if self._control_key_down(event.modifiers(), include_command=False):
267 267
268 268 if key == QtCore.Qt.Key_C and self._executing:
269 269 self.request_interrupt_kernel()
270 270 return True
271 271
272 272 elif key == QtCore.Qt.Key_Period:
273 273 self.request_restart_kernel()
274 274 return True
275 275
276 276 elif not event.modifiers() & QtCore.Qt.AltModifier:
277 277
278 278 # Smart backspace: remove four characters in one backspace if:
279 279 # 1) everything left of the cursor is whitespace
280 280 # 2) the four characters immediately left of the cursor are spaces
281 281 if key == QtCore.Qt.Key_Backspace:
282 282 col = self._get_input_buffer_cursor_column()
283 283 cursor = self._control.textCursor()
284 284 if col > 3 and not cursor.hasSelection():
285 285 text = self._get_input_buffer_cursor_line()[:col]
286 286 if text.endswith(' ') and not text.strip():
287 287 cursor.movePosition(QtGui.QTextCursor.Left,
288 288 QtGui.QTextCursor.KeepAnchor, 4)
289 289 cursor.removeSelectedText()
290 290 return True
291 291
292 292 return super(FrontendWidget, self)._event_filter_console_keypress(event)
293 293
294 294 def _insert_continuation_prompt(self, cursor):
295 295 """ Reimplemented for auto-indentation.
296 296 """
297 297 super(FrontendWidget, self)._insert_continuation_prompt(cursor)
298 298 cursor.insertText(' ' * self._input_splitter.indent_spaces)
299 299
300 300 #---------------------------------------------------------------------------
301 301 # 'BaseFrontendMixin' abstract interface
302 302 #---------------------------------------------------------------------------
303 303
304 304 def _handle_complete_reply(self, rep):
305 305 """ Handle replies for tab completion.
306 306 """
307 307 self.log.debug("complete: %s", rep.get('content', ''))
308 308 cursor = self._get_cursor()
309 309 info = self._request_info.get('complete')
310 310 if info and info.id == rep['parent_header']['msg_id'] and \
311 311 info.pos == cursor.position():
312 312 text = '.'.join(self._get_context())
313 313 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
314 314 self._complete_with_items(cursor, rep['content']['matches'])
315 315
316 316 def _silent_exec_callback(self, expr, callback):
317 317 """Silently execute `expr` in the kernel and call `callback` with reply
318 318
319 `expr` : valid string to be executed by the kernel.
320 `callback` : function accepting one string as argument.
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`
321 330
322 331 The `callback` is called with the 'repr()' of the result of `expr` as
323 first argument. To get the object, do 'eval()' on the passed value.
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
324 338 """
325 339
326 340 # generate uuid, which would be used as a indication of wether or not
327 341 # the unique request originate from here (can use msg id ?)
328 342 local_uuid = str(uuid.uuid1())
329 343 msg_id = self.kernel_manager.shell_channel.execute('',
330 344 silent=True, user_expressions={ local_uuid:expr })
331 345 self._callback_dict[local_uuid] = callback
332 346 self._request_info['execute'] = self._ExecutionRequest(msg_id, 'silent_exec_callback')
333 347
334 348 def _handle_exec_callback(self, msg):
335 349 """Execute `callback` corresonding to `msg` reply, after ``_silent_exec_callback``
336 350
337 `msg` : raw message send by the kernel containing an `user_expressions`
351 Parameters
352 ----------
353 msg : raw message send by the kernel containing an `user_expressions`
338 354 and having a 'silent_exec_callback' kind.
339 355
356 Notes
357 -----
340 358 This fonction will look for a `callback` associated with the
341 359 corresponding message id. Association has been made by
342 ``_silent_exec_callback``. `callback`is then called with the `repr()`
360 `_silent_exec_callback`. `callback` is then called with the `repr()`
343 361 of the value of corresponding `user_expressions` as argument.
344 362 `callback` is then removed from the known list so that any message
345 363 coming again with the same id won't trigger it.
364
346 365 """
347 366
348 cnt = msg['content']
349 ue = cnt['user_expressions']
350 for i in ue.keys():
351 if i in self._callback_dict:
352 self._callback_dict[i](ue[i])
353 self._callback_dict.pop(i)
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])
354 371
355 372 def _handle_execute_reply(self, msg):
356 373 """ Handles replies for code execution.
357 374 """
358 375 self.log.debug("execute: %s", msg.get('content', ''))
359 376 info = self._request_info.get('execute')
360 377 # unset reading flag, because if execute finished, raw_input can't
361 378 # still be pending.
362 379 self._reading = False
363 380 if info and info.id == msg['parent_header']['msg_id'] and \
364 381 info.kind == 'user' and not self._hidden:
365 382 # Make sure that all output from the SUB channel has been processed
366 383 # before writing a new prompt.
367 384 self.kernel_manager.sub_channel.flush()
368 385
369 386 # Reset the ANSI style information to prevent bad text in stdout
370 387 # from messing up our colors. We're not a true terminal so we're
371 388 # allowed to do this.
372 389 if self.ansi_codes:
373 390 self._ansi_processor.reset_sgr()
374 391
375 392 content = msg['content']
376 393 status = content['status']
377 394 if status == 'ok':
378 395 self._process_execute_ok(msg)
379 396 elif status == 'error':
380 397 self._process_execute_error(msg)
381 398 elif status == 'aborted':
382 399 self._process_execute_abort(msg)
383 400
384 401 self._show_interpreter_prompt_for_reply(msg)
385 402 self.executed.emit(msg)
386 403 elif info and info.id == msg['parent_header']['msg_id'] and \
387 404 info.kind == 'silent_exec_callback' and not self._hidden:
388 405 self._handle_exec_callback(msg)
389 406 else:
390 407 super(FrontendWidget, self)._handle_execute_reply(msg)
391 408
392 409 def _handle_input_request(self, msg):
393 410 """ Handle requests for raw_input.
394 411 """
395 412 self.log.debug("input: %s", msg.get('content', ''))
396 413 if self._hidden:
397 414 raise RuntimeError('Request for raw input during hidden execution.')
398 415
399 416 # Make sure that all output from the SUB channel has been processed
400 417 # before entering readline mode.
401 418 self.kernel_manager.sub_channel.flush()
402 419
403 420 def callback(line):
404 421 self.kernel_manager.stdin_channel.input(line)
405 422 if self._reading:
406 423 self.log.debug("Got second input request, assuming first was interrupted.")
407 424 self._reading = False
408 425 self._readline(msg['content']['prompt'], callback=callback)
409 426
410 427 def _handle_kernel_died(self, since_last_heartbeat):
411 428 """ Handle the kernel's death by asking if the user wants to restart.
412 429 """
413 430 self.log.debug("kernel died: %s", since_last_heartbeat)
414 431 if self.custom_restart:
415 432 self.custom_restart_kernel_died.emit(since_last_heartbeat)
416 433 else:
417 434 message = 'The kernel heartbeat has been inactive for %.2f ' \
418 435 'seconds. Do you want to restart the kernel? You may ' \
419 436 'first want to check the network connection.' % \
420 437 since_last_heartbeat
421 438 self.restart_kernel(message, now=True)
422 439
423 440 def _handle_object_info_reply(self, rep):
424 441 """ Handle replies for call tips.
425 442 """
426 443 self.log.debug("oinfo: %s", rep.get('content', ''))
427 444 cursor = self._get_cursor()
428 445 info = self._request_info.get('call_tip')
429 446 if info and info.id == rep['parent_header']['msg_id'] and \
430 447 info.pos == cursor.position():
431 448 # Get the information for a call tip. For now we format the call
432 449 # line as string, later we can pass False to format_call and
433 450 # syntax-highlight it ourselves for nicer formatting in the
434 451 # calltip.
435 452 content = rep['content']
436 453 # if this is from pykernel, 'docstring' will be the only key
437 454 if content.get('ismagic', False):
438 455 # Don't generate a call-tip for magics. Ideally, we should
439 456 # generate a tooltip, but not on ( like we do for actual
440 457 # callables.
441 458 call_info, doc = None, None
442 459 else:
443 460 call_info, doc = call_tip(content, format_call=True)
444 461 if call_info or doc:
445 462 self._call_tip_widget.show_call_info(call_info, doc)
446 463
447 464 def _handle_pyout(self, msg):
448 465 """ Handle display hook output.
449 466 """
450 467 self.log.debug("pyout: %s", msg.get('content', ''))
451 468 if not self._hidden and self._is_from_this_session(msg):
452 469 text = msg['content']['data']
453 470 self._append_plain_text(text + '\n', before_prompt=True)
454 471
455 472 def _handle_stream(self, msg):
456 473 """ Handle stdout, stderr, and stdin.
457 474 """
458 475 self.log.debug("stream: %s", msg.get('content', ''))
459 476 if not self._hidden and self._is_from_this_session(msg):
460 477 # Most consoles treat tabs as being 8 space characters. Convert tabs
461 478 # to spaces so that output looks as expected regardless of this
462 479 # widget's tab width.
463 480 text = msg['content']['data'].expandtabs(8)
464 481
465 482 self._append_plain_text(text, before_prompt=True)
466 483 self._control.moveCursor(QtGui.QTextCursor.End)
467 484
468 485 def _handle_shutdown_reply(self, msg):
469 486 """ Handle shutdown signal, only if from other console.
470 487 """
471 488 self.log.debug("shutdown: %s", msg.get('content', ''))
472 489 if not self._hidden and not self._is_from_this_session(msg):
473 490 if self._local_kernel:
474 491 if not msg['content']['restart']:
475 492 self.exit_requested.emit(self)
476 493 else:
477 494 # we just got notified of a restart!
478 495 time.sleep(0.25) # wait 1/4 sec to reset
479 496 # lest the request for a new prompt
480 497 # goes to the old kernel
481 498 self.reset()
482 499 else: # remote kernel, prompt on Kernel shutdown/reset
483 500 title = self.window().windowTitle()
484 501 if not msg['content']['restart']:
485 502 reply = QtGui.QMessageBox.question(self, title,
486 503 "Kernel has been shutdown permanently. "
487 504 "Close the Console?",
488 505 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
489 506 if reply == QtGui.QMessageBox.Yes:
490 507 self.exit_requested.emit(self)
491 508 else:
492 509 reply = QtGui.QMessageBox.question(self, title,
493 510 "Kernel has been reset. Clear the Console?",
494 511 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
495 512 if reply == QtGui.QMessageBox.Yes:
496 513 time.sleep(0.25) # wait 1/4 sec to reset
497 514 # lest the request for a new prompt
498 515 # goes to the old kernel
499 516 self.reset()
500 517
501 518 def _started_channels(self):
502 519 """ Called when the KernelManager channels have started listening or
503 520 when the frontend is assigned an already listening KernelManager.
504 521 """
505 522 self.reset()
506 523
507 524 #---------------------------------------------------------------------------
508 525 # 'FrontendWidget' public interface
509 526 #---------------------------------------------------------------------------
510 527
511 528 def copy_raw(self):
512 529 """ Copy the currently selected text to the clipboard without attempting
513 530 to remove prompts or otherwise alter the text.
514 531 """
515 532 self._control.copy()
516 533
517 534 def execute_file(self, path, hidden=False):
518 535 """ Attempts to execute file with 'path'. If 'hidden', no output is
519 536 shown.
520 537 """
521 538 self.execute('execfile(%r)' % path, hidden=hidden)
522 539
523 540 def interrupt_kernel(self):
524 541 """ Attempts to interrupt the running kernel.
525 542
526 543 Also unsets _reading flag, to avoid runtime errors
527 544 if raw_input is called again.
528 545 """
529 546 if self.custom_interrupt:
530 547 self._reading = False
531 548 self.custom_interrupt_requested.emit()
532 549 elif self.kernel_manager.has_kernel:
533 550 self._reading = False
534 551 self.kernel_manager.interrupt_kernel()
535 552 else:
536 553 self._append_plain_text('Kernel process is either remote or '
537 554 'unspecified. Cannot interrupt.\n')
538 555
539 556 def reset(self):
540 557 """ Resets the widget to its initial state. Similar to ``clear``, but
541 558 also re-writes the banner and aborts execution if necessary.
542 559 """
543 560 if self._executing:
544 561 self._executing = False
545 562 self._request_info['execute'] = None
546 563 self._reading = False
547 564 self._highlighter.highlighting_on = False
548 565
549 566 self._control.clear()
550 567 self._append_plain_text(self.banner)
551 568 # update output marker for stdout/stderr, so that startup
552 569 # messages appear after banner:
553 570 self._append_before_prompt_pos = self._get_cursor().position()
554 571 self._show_interpreter_prompt()
555 572
556 573 def restart_kernel(self, message, now=False):
557 574 """ Attempts to restart the running kernel.
558 575 """
559 576 # FIXME: now should be configurable via a checkbox in the dialog. Right
560 577 # now at least the heartbeat path sets it to True and the manual restart
561 578 # to False. But those should just be the pre-selected states of a
562 579 # checkbox that the user could override if so desired. But I don't know
563 580 # enough Qt to go implementing the checkbox now.
564 581
565 582 if self.custom_restart:
566 583 self.custom_restart_requested.emit()
567 584
568 585 elif self.kernel_manager.has_kernel:
569 586 # Pause the heart beat channel to prevent further warnings.
570 587 self.kernel_manager.hb_channel.pause()
571 588
572 589 # Prompt the user to restart the kernel. Un-pause the heartbeat if
573 590 # they decline. (If they accept, the heartbeat will be un-paused
574 591 # automatically when the kernel is restarted.)
575 592 buttons = QtGui.QMessageBox.Yes | QtGui.QMessageBox.No
576 593 result = QtGui.QMessageBox.question(self, 'Restart kernel?',
577 594 message, buttons)
578 595 if result == QtGui.QMessageBox.Yes:
579 596 try:
580 597 self.kernel_manager.restart_kernel(now=now)
581 598 except RuntimeError:
582 599 self._append_plain_text('Kernel started externally. '
583 600 'Cannot restart.\n')
584 601 else:
585 602 self.reset()
586 603 else:
587 604 self.kernel_manager.hb_channel.unpause()
588 605
589 606 else:
590 607 self._append_plain_text('Kernel process is either remote or '
591 608 'unspecified. Cannot restart.\n')
592 609
593 610 #---------------------------------------------------------------------------
594 611 # 'FrontendWidget' protected interface
595 612 #---------------------------------------------------------------------------
596 613
597 614 def _call_tip(self):
598 615 """ Shows a call tip, if appropriate, at the current cursor location.
599 616 """
600 617 # Decide if it makes sense to show a call tip
601 618 if not self.enable_calltips:
602 619 return False
603 620 cursor = self._get_cursor()
604 621 cursor.movePosition(QtGui.QTextCursor.Left)
605 622 if cursor.document().characterAt(cursor.position()) != '(':
606 623 return False
607 624 context = self._get_context(cursor)
608 625 if not context:
609 626 return False
610 627
611 628 # Send the metadata request to the kernel
612 629 name = '.'.join(context)
613 630 msg_id = self.kernel_manager.shell_channel.object_info(name)
614 631 pos = self._get_cursor().position()
615 632 self._request_info['call_tip'] = self._CallTipRequest(msg_id, pos)
616 633 return True
617 634
618 635 def _complete(self):
619 636 """ Performs completion at the current cursor location.
620 637 """
621 638 context = self._get_context()
622 639 if context:
623 640 # Send the completion request to the kernel
624 641 msg_id = self.kernel_manager.shell_channel.complete(
625 642 '.'.join(context), # text
626 643 self._get_input_buffer_cursor_line(), # line
627 644 self._get_input_buffer_cursor_column(), # cursor_pos
628 645 self.input_buffer) # block
629 646 pos = self._get_cursor().position()
630 647 info = self._CompletionRequest(msg_id, pos)
631 648 self._request_info['complete'] = info
632 649
633 650 def _get_context(self, cursor=None):
634 651 """ Gets the context for the specified cursor (or the current cursor
635 652 if none is specified).
636 653 """
637 654 if cursor is None:
638 655 cursor = self._get_cursor()
639 656 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
640 657 QtGui.QTextCursor.KeepAnchor)
641 658 text = cursor.selection().toPlainText()
642 659 return self._completion_lexer.get_context(text)
643 660
644 661 def _process_execute_abort(self, msg):
645 662 """ Process a reply for an aborted execution request.
646 663 """
647 664 self._append_plain_text("ERROR: execution aborted\n")
648 665
649 666 def _process_execute_error(self, msg):
650 667 """ Process a reply for an execution request that resulted in an error.
651 668 """
652 669 content = msg['content']
653 670 # If a SystemExit is passed along, this means exit() was called - also
654 671 # all the ipython %exit magic syntax of '-k' to be used to keep
655 672 # the kernel running
656 673 if content['ename']=='SystemExit':
657 674 keepkernel = content['evalue']=='-k' or content['evalue']=='True'
658 675 self._keep_kernel_on_exit = keepkernel
659 676 self.exit_requested.emit(self)
660 677 else:
661 678 traceback = ''.join(content['traceback'])
662 679 self._append_plain_text(traceback)
663 680
664 681 def _process_execute_ok(self, msg):
665 682 """ Process a reply for a successful execution equest.
666 683 """
667 684 payload = msg['content']['payload']
668 685 for item in payload:
669 686 if not self._process_execute_payload(item):
670 687 warning = 'Warning: received unknown payload of type %s'
671 688 print(warning % repr(item['source']))
672 689
673 690 def _process_execute_payload(self, item):
674 691 """ Process a single payload item from the list of payload items in an
675 692 execution reply. Returns whether the payload was handled.
676 693 """
677 694 # The basic FrontendWidget doesn't handle payloads, as they are a
678 695 # mechanism for going beyond the standard Python interpreter model.
679 696 return False
680 697
681 698 def _show_interpreter_prompt(self):
682 699 """ Shows a prompt for the interpreter.
683 700 """
684 701 self._show_prompt('>>> ')
685 702
686 703 def _show_interpreter_prompt_for_reply(self, msg):
687 704 """ Shows a prompt for the interpreter given an 'execute_reply' message.
688 705 """
689 706 self._show_interpreter_prompt()
690 707
691 708 #------ Signal handlers ----------------------------------------------------
692 709
693 710 def _document_contents_change(self, position, removed, added):
694 711 """ Called whenever the document's content changes. Display a call tip
695 712 if appropriate.
696 713 """
697 714 # Calculate where the cursor should be *after* the change:
698 715 position += added
699 716
700 717 document = self._control.document()
701 718 if position == self._get_cursor().position():
702 719 self._call_tip()
703 720
704 721 #------ Trait default initializers -----------------------------------------
705 722
706 723 def _banner_default(self):
707 724 """ Returns the standard Python banner.
708 725 """
709 726 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
710 727 '"license" for more information.'
711 728 return banner % (sys.version, sys.platform)
@@ -1,879 +1,904 b''
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
547 548 def _make_dynamic_magic(self,magic):
548 549 """Return a function `fun` that will execute `magic` on active frontend.
549 550
550 `magic` : valid python string
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
551 561
552 return `fun`, function with no parameters
562 See Also
563 --------
564 populate_all_magic_menu : generate the "All Magics..." menu
553 565
566 Notes
567 -----
554 568 `fun` execute `magic` an active frontend at the moment it is triggerd,
555 569 not the active frontend at the moment it has been created.
556 570
557 571 This function is mostly used to create the "All Magics..." Menu at run time.
558 572 """
559 573 # need to level nested function to be sure to past magic
560 574 # on active frontend **at run time**.
561 575 def inner_dynamic_magic():
562 576 self.active_frontend.execute(magic)
563 577 inner_dynamic_magic.__name__ = "dynamics_magic_s"
564 578 return inner_dynamic_magic
565 579
566 580 def populate_all_magic_menu(self, listofmagic=None):
567 581 """Clean "All Magics..." menu and repopulate it with `listofmagic`
568 582
569 `listofmagic` : string, repr() of a list of strings.
583 Parameters
584 ----------
585 listofmagic : string,
586 repr() of a list of strings, send back by the kernel
570 587
588 Notes
589 -----
571 590 `listofmagic`is a repr() of list because it is fed with the result of
572 591 a 'user_expression'
573 592 """
574 593 alm_magic_menu = self.all_magic_menu
575 594 alm_magic_menu.clear()
576 595
577 596 # list of protected magic that don't like to be called without argument
578 597 # append '?' to the end to print the docstring when called from the menu
579 598 protected_magic = set(["more","less","load_ext","pycat","loadpy","save"])
580
581 for magic in eval(listofmagic):
599 magics=re.findall('\w+', listofmagic)
600 for magic in magics:
582 601 if magic in protected_magic:
583 602 pmagic = '%s%s%s'%('%',magic,'?')
584 603 else:
585 604 pmagic = '%s%s'%('%',magic)
586 605 xaction = QtGui.QAction(pmagic,
587 606 self,
588 607 triggered=self._make_dynamic_magic(pmagic)
589 608 )
590 609 alm_magic_menu.addAction(xaction)
591 610
592 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 """
593 618 # first define a callback which will get the list of all magic and put it in the menu.
594 self.active_frontend._silent_exec_callback('get_ipython().lsmagic()',self.populate_all_magic_menu)
619 self.active_frontend._silent_exec_callback('get_ipython().lsmagic()', self.populate_all_magic_menu)
595 620
596 621 def init_magic_menu(self):
597 622 self.magic_menu = self.menuBar().addMenu("&Magic")
598 623 self.all_magic_menu = self.magic_menu.addMenu("&All Magics")
599 624
600 625 # this action should not appear as it will be cleard when menu
601 626 # will be updated at first kernel response.
602 627 self.pop = QtGui.QAction("&Update All Magic Menu ",
603 628 self, triggered=self.update_all_magic_menu)
604 629 self.add_menu_action(self.all_magic_menu, self.pop)
605 630
606 631 self.reset_action = QtGui.QAction("&Reset",
607 632 self,
608 633 statusTip="Clear all varible from workspace",
609 634 triggered=self.reset_magic_active_frontend)
610 635 self.add_menu_action(self.magic_menu, self.reset_action)
611 636
612 637 self.history_action = QtGui.QAction("&History",
613 638 self,
614 639 statusTip="show command history",
615 640 triggered=self.history_magic_active_frontend)
616 641 self.add_menu_action(self.magic_menu, self.history_action)
617 642
618 643 self.save_action = QtGui.QAction("E&xport History ",
619 644 self,
620 645 statusTip="Export History as Python File",
621 646 triggered=self.save_magic_active_frontend)
622 647 self.add_menu_action(self.magic_menu, self.save_action)
623 648
624 649 self.who_action = QtGui.QAction("&Who",
625 650 self,
626 651 statusTip="List interactive variable",
627 652 triggered=self.who_magic_active_frontend)
628 653 self.add_menu_action(self.magic_menu, self.who_action)
629 654
630 655 self.who_ls_action = QtGui.QAction("Wh&o ls",
631 656 self,
632 657 statusTip="Return a list of interactive variable",
633 658 triggered=self.who_ls_magic_active_frontend)
634 659 self.add_menu_action(self.magic_menu, self.who_ls_action)
635 660
636 661 self.whos_action = QtGui.QAction("Who&s",
637 662 self,
638 663 statusTip="List interactive variable with detail",
639 664 triggered=self.whos_magic_active_frontend)
640 665 self.add_menu_action(self.magic_menu, self.whos_action)
641
666
642 667 def init_window_menu(self):
643 668 self.window_menu = self.menuBar().addMenu("&Window")
644 669 if sys.platform == 'darwin':
645 670 # add min/maximize actions to OSX, which lacks default bindings.
646 671 self.minimizeAct = QtGui.QAction("Mini&mize",
647 672 self,
648 673 shortcut="Ctrl+m",
649 674 statusTip="Minimize the window/Restore Normal Size",
650 675 triggered=self.toggleMinimized)
651 676 # maximize is called 'Zoom' on OSX for some reason
652 677 self.maximizeAct = QtGui.QAction("&Zoom",
653 678 self,
654 679 shortcut="Ctrl+Shift+M",
655 680 statusTip="Maximize the window/Restore Normal Size",
656 681 triggered=self.toggleMaximized)
657 682
658 683 self.add_menu_action(self.window_menu, self.minimizeAct)
659 684 self.add_menu_action(self.window_menu, self.maximizeAct)
660 685 self.window_menu.addSeparator()
661 686
662 687 prev_key = "Ctrl+Shift+Left" if sys.platform == 'darwin' else "Ctrl+PgUp"
663 688 self.prev_tab_act = QtGui.QAction("Pre&vious Tab",
664 689 self,
665 690 shortcut=prev_key,
666 691 statusTip="Select previous tab",
667 692 triggered=self.prev_tab)
668 693 self.add_menu_action(self.window_menu, self.prev_tab_act)
669 694
670 695 next_key = "Ctrl+Shift+Right" if sys.platform == 'darwin' else "Ctrl+PgDown"
671 696 self.next_tab_act = QtGui.QAction("Ne&xt Tab",
672 697 self,
673 698 shortcut=next_key,
674 699 statusTip="Select next tab",
675 700 triggered=self.next_tab)
676 701 self.add_menu_action(self.window_menu, self.next_tab_act)
677 702
678 703 def init_help_menu(self):
679 704 # please keep the Help menu in Mac Os even if empty. It will
680 705 # automatically contain a search field to search inside menus and
681 706 # please keep it spelled in English, as long as Qt Doesn't support
682 707 # a QAction.MenuRole like HelpMenuRole otherwise it will loose
683 708 # this search field fonctionality
684 709
685 710 self.help_menu = self.menuBar().addMenu("&Help")
686 711
687 712
688 713 # Help Menu
689 714
690 715 self.intro_active_frontend_action = QtGui.QAction("&Intro to IPython",
691 716 self,
692 717 triggered=self.intro_active_frontend
693 718 )
694 719 self.add_menu_action(self.help_menu, self.intro_active_frontend_action)
695 720
696 721 self.quickref_active_frontend_action = QtGui.QAction("IPython &Cheat Sheet",
697 722 self,
698 723 triggered=self.quickref_active_frontend
699 724 )
700 725 self.add_menu_action(self.help_menu, self.quickref_active_frontend_action)
701 726
702 727 self.guiref_active_frontend_action = QtGui.QAction("&Qt Console",
703 728 self,
704 729 triggered=self.guiref_active_frontend
705 730 )
706 731 self.add_menu_action(self.help_menu, self.guiref_active_frontend_action)
707 732
708 733 self.onlineHelpAct = QtGui.QAction("Open Online &Help",
709 734 self,
710 735 triggered=self._open_online_help)
711 736 self.add_menu_action(self.help_menu, self.onlineHelpAct)
712 737
713 738 # minimize/maximize/fullscreen actions:
714 739
715 740 def toggle_menu_bar(self):
716 741 menu_bar = self.menuBar()
717 742 if menu_bar.isVisible():
718 743 menu_bar.setVisible(False)
719 744 else:
720 745 menu_bar.setVisible(True)
721 746
722 747 def toggleMinimized(self):
723 748 if not self.isMinimized():
724 749 self.showMinimized()
725 750 else:
726 751 self.showNormal()
727 752
728 753 def _open_online_help(self):
729 754 filename="http://ipython.org/ipython-doc/stable/index.html"
730 755 webbrowser.open(filename, new=1, autoraise=True)
731 756
732 757 def toggleMaximized(self):
733 758 if not self.isMaximized():
734 759 self.showMaximized()
735 760 else:
736 761 self.showNormal()
737 762
738 763 # Min/Max imizing while in full screen give a bug
739 764 # when going out of full screen, at least on OSX
740 765 def toggleFullScreen(self):
741 766 if not self.isFullScreen():
742 767 self.showFullScreen()
743 768 if sys.platform == 'darwin':
744 769 self.maximizeAct.setEnabled(False)
745 770 self.minimizeAct.setEnabled(False)
746 771 else:
747 772 self.showNormal()
748 773 if sys.platform == 'darwin':
749 774 self.maximizeAct.setEnabled(True)
750 775 self.minimizeAct.setEnabled(True)
751 776
752 777 def close_active_frontend(self):
753 778 self.close_tab(self.active_frontend)
754 779
755 780 def restart_kernel_active_frontend(self):
756 781 self.active_frontend.request_restart_kernel()
757 782
758 783 def interrupt_kernel_active_frontend(self):
759 784 self.active_frontend.request_interrupt_kernel()
760 785
761 786 def cut_active_frontend(self):
762 787 widget = self.active_frontend
763 788 if widget.can_cut():
764 789 widget.cut()
765 790
766 791 def copy_active_frontend(self):
767 792 widget = self.active_frontend
768 793 if widget.can_copy():
769 794 widget.copy()
770 795
771 796 def copy_raw_active_frontend(self):
772 797 self.active_frontend._copy_raw_action.trigger()
773 798
774 799 def paste_active_frontend(self):
775 800 widget = self.active_frontend
776 801 if widget.can_paste():
777 802 widget.paste()
778 803
779 804 def undo_active_frontend(self):
780 805 self.active_frontend.undo()
781 806
782 807 def redo_active_frontend(self):
783 808 self.active_frontend.redo()
784 809
785 810 def reset_magic_active_frontend(self):
786 811 self.active_frontend.execute("%reset")
787 812
788 813 def history_magic_active_frontend(self):
789 814 self.active_frontend.execute("%history")
790 815
791 816 def save_magic_active_frontend(self):
792 817 self.active_frontend.save_magic()
793 818
794 819 def clear_magic_active_frontend(self):
795 820 self.active_frontend.execute("%clear")
796 821
797 822 def who_magic_active_frontend(self):
798 823 self.active_frontend.execute("%who")
799 824
800 825 def who_ls_magic_active_frontend(self):
801 826 self.active_frontend.execute("%who_ls")
802 827
803 828 def whos_magic_active_frontend(self):
804 829 self.active_frontend.execute("%whos")
805 830
806 831 def print_action_active_frontend(self):
807 832 self.active_frontend.print_action.trigger()
808 833
809 834 def export_action_active_frontend(self):
810 835 self.active_frontend.export_action.trigger()
811 836
812 837 def select_all_active_frontend(self):
813 838 self.active_frontend.select_all_action.trigger()
814 839
815 840 def increase_font_size_active_frontend(self):
816 841 self.active_frontend.increase_font_size.trigger()
817 842
818 843 def decrease_font_size_active_frontend(self):
819 844 self.active_frontend.decrease_font_size.trigger()
820 845
821 846 def reset_font_size_active_frontend(self):
822 847 self.active_frontend.reset_font_size.trigger()
823 848
824 849 def guiref_active_frontend(self):
825 850 self.active_frontend.execute("%guiref")
826 851
827 852 def intro_active_frontend(self):
828 853 self.active_frontend.execute("?")
829 854
830 855 def quickref_active_frontend(self):
831 856 self.active_frontend.execute("%quickref")
832 857 #---------------------------------------------------------------------------
833 858 # QWidget interface
834 859 #---------------------------------------------------------------------------
835 860
836 861 def closeEvent(self, event):
837 862 """ Forward the close event to every tabs contained by the windows
838 863 """
839 864 if self.tab_widget.count() == 0:
840 865 # no tabs, just close
841 866 event.accept()
842 867 return
843 868 # Do Not loop on the widget count as it change while closing
844 869 title = self.window().windowTitle()
845 870 cancel = QtGui.QMessageBox.Cancel
846 871 okay = QtGui.QMessageBox.Ok
847 872
848 873 if self.confirm_exit:
849 874 if self.tab_widget.count() > 1:
850 875 msg = "Close all tabs, stop all kernels, and Quit?"
851 876 else:
852 877 msg = "Close console, stop kernel, and Quit?"
853 878 info = "Kernels not started here (e.g. notebooks) will be left alone."
854 879 closeall = QtGui.QPushButton("&Yes, quit everything", self)
855 880 closeall.setShortcut('Y')
856 881 box = QtGui.QMessageBox(QtGui.QMessageBox.Question,
857 882 title, msg)
858 883 box.setInformativeText(info)
859 884 box.addButton(cancel)
860 885 box.addButton(closeall, QtGui.QMessageBox.YesRole)
861 886 box.setDefaultButton(closeall)
862 887 box.setEscapeButton(cancel)
863 888 pixmap = QtGui.QPixmap(self._app.icon.pixmap(QtCore.QSize(64,64)))
864 889 box.setIconPixmap(pixmap)
865 890 reply = box.exec_()
866 891 else:
867 892 reply = okay
868 893
869 894 if reply == cancel:
870 895 event.ignore()
871 896 return
872 897 if reply == okay:
873 898 while self.tab_widget.count() >= 1:
874 899 # prevent further confirmations:
875 900 widget = self.active_frontend
876 901 widget._confirm_exit = False
877 902 self.close_tab(widget)
878 903 event.accept()
879 904
General Comments 0
You need to be logged in to leave comments. Login now