##// END OF EJS Templates
Generate "All magics..." menu live...
Matthias BUSSONNIER -
Show More
@@ -1,667 +1,708 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 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=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 a function in the kernel and send the reply to the callback
318
319 expr should be a valid string to be executed by the kernel.
320
321 callback a function accepting one argument (str)
322
323 the callback is called with the 'repr()' of the result as first argument.
324 to get the object, do 'eval()' on the passed value.
325 """
326
327 # generate uuid, which would be used as a indication of wether or not
328 # the unique request originate from here (can use msg id ?)
329 local_uuid = str(uuid.uuid1())
330 msg_id = self.kernel_manager.shell_channel.execute('',
331 silent=True,
332 user_expressions={ local_uuid:expr,
333 }
334 )
335 self._callback_dict[local_uuid]=callback
336 self._request_info['execute'] = self._ExecutionRequest(msg_id, 'silent_exec_callback')
337
338 def _handle_exec_callback(self,msg):
339 """ Called when _silent_exec_callback message comme back from the kernel.
340
341 Strip the message comming back from the kernel and send it to the
342 corresponding callback function.
343 """
344 cnt=msg['content']
345 ue=cnt['user_expressions']
346 for i in ue.keys():
347 if i in self._callback_dict.keys():
348 f= self._callback_dict[i]
349 f(ue[i])
350 self._callback_dict.pop(i)
351
314 352 def _handle_execute_reply(self, msg):
315 353 """ Handles replies for code execution.
316 354 """
317 355 self.log.debug("execute: %s", msg.get('content', ''))
318 356 info = self._request_info.get('execute')
319 357 # unset reading flag, because if execute finished, raw_input can't
320 358 # still be pending.
321 359 self._reading = False
322 360 if info and info.id == msg['parent_header']['msg_id'] and \
323 361 info.kind == 'user' and not self._hidden:
324 362 # Make sure that all output from the SUB channel has been processed
325 363 # before writing a new prompt.
326 364 self.kernel_manager.sub_channel.flush()
327 365
328 366 # Reset the ANSI style information to prevent bad text in stdout
329 367 # from messing up our colors. We're not a true terminal so we're
330 368 # allowed to do this.
331 369 if self.ansi_codes:
332 370 self._ansi_processor.reset_sgr()
333 371
334 372 content = msg['content']
335 373 status = content['status']
336 374 if status == 'ok':
337 375 self._process_execute_ok(msg)
338 376 elif status == 'error':
339 377 self._process_execute_error(msg)
340 378 elif status == 'aborted':
341 379 self._process_execute_abort(msg)
342 380
343 381 self._show_interpreter_prompt_for_reply(msg)
344 382 self.executed.emit(msg)
383 elif info and info.id == msg['parent_header']['msg_id'] and \
384 info.kind == 'silent_exec_callback' and not self._hidden:
385 self._handle_exec_callback(msg)
345 386 else:
346 387 super(FrontendWidget, self)._handle_execute_reply(msg)
347 388
348 389 def _handle_input_request(self, msg):
349 390 """ Handle requests for raw_input.
350 391 """
351 392 self.log.debug("input: %s", msg.get('content', ''))
352 393 if self._hidden:
353 394 raise RuntimeError('Request for raw input during hidden execution.')
354 395
355 396 # Make sure that all output from the SUB channel has been processed
356 397 # before entering readline mode.
357 398 self.kernel_manager.sub_channel.flush()
358 399
359 400 def callback(line):
360 401 self.kernel_manager.stdin_channel.input(line)
361 402 if self._reading:
362 403 self.log.debug("Got second input request, assuming first was interrupted.")
363 404 self._reading = False
364 405 self._readline(msg['content']['prompt'], callback=callback)
365 406
366 407 def _handle_kernel_died(self, since_last_heartbeat):
367 408 """ Handle the kernel's death by asking if the user wants to restart.
368 409 """
369 410 self.log.debug("kernel died: %s", since_last_heartbeat)
370 411 if self.custom_restart:
371 412 self.custom_restart_kernel_died.emit(since_last_heartbeat)
372 413 else:
373 414 message = 'The kernel heartbeat has been inactive for %.2f ' \
374 415 'seconds. Do you want to restart the kernel? You may ' \
375 416 'first want to check the network connection.' % \
376 417 since_last_heartbeat
377 418 self.restart_kernel(message, now=True)
378 419
379 420 def _handle_object_info_reply(self, rep):
380 421 """ Handle replies for call tips.
381 422 """
382 423 self.log.debug("oinfo: %s", rep.get('content', ''))
383 424 cursor = self._get_cursor()
384 425 info = self._request_info.get('call_tip')
385 426 if info and info.id == rep['parent_header']['msg_id'] and \
386 427 info.pos == cursor.position():
387 428 # Get the information for a call tip. For now we format the call
388 429 # line as string, later we can pass False to format_call and
389 430 # syntax-highlight it ourselves for nicer formatting in the
390 431 # calltip.
391 432 content = rep['content']
392 433 # if this is from pykernel, 'docstring' will be the only key
393 434 if content.get('ismagic', False):
394 435 # Don't generate a call-tip for magics. Ideally, we should
395 436 # generate a tooltip, but not on ( like we do for actual
396 437 # callables.
397 438 call_info, doc = None, None
398 439 else:
399 440 call_info, doc = call_tip(content, format_call=True)
400 441 if call_info or doc:
401 442 self._call_tip_widget.show_call_info(call_info, doc)
402 443
403 444 def _handle_pyout(self, msg):
404 445 """ Handle display hook output.
405 446 """
406 447 self.log.debug("pyout: %s", msg.get('content', ''))
407 448 if not self._hidden and self._is_from_this_session(msg):
408 449 text = msg['content']['data']
409 450 self._append_plain_text(text + '\n', before_prompt=True)
410 451
411 452 def _handle_stream(self, msg):
412 453 """ Handle stdout, stderr, and stdin.
413 454 """
414 455 self.log.debug("stream: %s", msg.get('content', ''))
415 456 if not self._hidden and self._is_from_this_session(msg):
416 457 # Most consoles treat tabs as being 8 space characters. Convert tabs
417 458 # to spaces so that output looks as expected regardless of this
418 459 # widget's tab width.
419 460 text = msg['content']['data'].expandtabs(8)
420 461
421 462 self._append_plain_text(text, before_prompt=True)
422 463 self._control.moveCursor(QtGui.QTextCursor.End)
423 464
424 465 def _handle_shutdown_reply(self, msg):
425 466 """ Handle shutdown signal, only if from other console.
426 467 """
427 468 self.log.debug("shutdown: %s", msg.get('content', ''))
428 469 if not self._hidden and not self._is_from_this_session(msg):
429 470 if self._local_kernel:
430 471 if not msg['content']['restart']:
431 472 self.exit_requested.emit(self)
432 473 else:
433 474 # we just got notified of a restart!
434 475 time.sleep(0.25) # wait 1/4 sec to reset
435 476 # lest the request for a new prompt
436 477 # goes to the old kernel
437 478 self.reset()
438 479 else: # remote kernel, prompt on Kernel shutdown/reset
439 480 title = self.window().windowTitle()
440 481 if not msg['content']['restart']:
441 482 reply = QtGui.QMessageBox.question(self, title,
442 483 "Kernel has been shutdown permanently. "
443 484 "Close the Console?",
444 485 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
445 486 if reply == QtGui.QMessageBox.Yes:
446 487 self.exit_requested.emit(self)
447 488 else:
448 489 reply = QtGui.QMessageBox.question(self, title,
449 490 "Kernel has been reset. Clear the Console?",
450 491 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
451 492 if reply == QtGui.QMessageBox.Yes:
452 493 time.sleep(0.25) # wait 1/4 sec to reset
453 494 # lest the request for a new prompt
454 495 # goes to the old kernel
455 496 self.reset()
456 497
457 498 def _started_channels(self):
458 499 """ Called when the KernelManager channels have started listening or
459 500 when the frontend is assigned an already listening KernelManager.
460 501 """
461 502 self.reset()
462 503
463 504 #---------------------------------------------------------------------------
464 505 # 'FrontendWidget' public interface
465 506 #---------------------------------------------------------------------------
466 507
467 508 def copy_raw(self):
468 509 """ Copy the currently selected text to the clipboard without attempting
469 510 to remove prompts or otherwise alter the text.
470 511 """
471 512 self._control.copy()
472 513
473 514 def execute_file(self, path, hidden=False):
474 515 """ Attempts to execute file with 'path'. If 'hidden', no output is
475 516 shown.
476 517 """
477 518 self.execute('execfile(%r)' % path, hidden=hidden)
478 519
479 520 def interrupt_kernel(self):
480 521 """ Attempts to interrupt the running kernel.
481 522
482 523 Also unsets _reading flag, to avoid runtime errors
483 524 if raw_input is called again.
484 525 """
485 526 if self.custom_interrupt:
486 527 self._reading = False
487 528 self.custom_interrupt_requested.emit()
488 529 elif self.kernel_manager.has_kernel:
489 530 self._reading = False
490 531 self.kernel_manager.interrupt_kernel()
491 532 else:
492 533 self._append_plain_text('Kernel process is either remote or '
493 534 'unspecified. Cannot interrupt.\n')
494 535
495 536 def reset(self):
496 537 """ Resets the widget to its initial state. Similar to ``clear``, but
497 538 also re-writes the banner and aborts execution if necessary.
498 539 """
499 540 if self._executing:
500 541 self._executing = False
501 542 self._request_info['execute'] = None
502 543 self._reading = False
503 544 self._highlighter.highlighting_on = False
504 545
505 546 self._control.clear()
506 547 self._append_plain_text(self.banner)
507 548 # update output marker for stdout/stderr, so that startup
508 549 # messages appear after banner:
509 550 self._append_before_prompt_pos = self._get_cursor().position()
510 551 self._show_interpreter_prompt()
511 552
512 553 def restart_kernel(self, message, now=False):
513 554 """ Attempts to restart the running kernel.
514 555 """
515 556 # FIXME: now should be configurable via a checkbox in the dialog. Right
516 557 # now at least the heartbeat path sets it to True and the manual restart
517 558 # to False. But those should just be the pre-selected states of a
518 559 # checkbox that the user could override if so desired. But I don't know
519 560 # enough Qt to go implementing the checkbox now.
520 561
521 562 if self.custom_restart:
522 563 self.custom_restart_requested.emit()
523 564
524 565 elif self.kernel_manager.has_kernel:
525 566 # Pause the heart beat channel to prevent further warnings.
526 567 self.kernel_manager.hb_channel.pause()
527 568
528 569 # Prompt the user to restart the kernel. Un-pause the heartbeat if
529 570 # they decline. (If they accept, the heartbeat will be un-paused
530 571 # automatically when the kernel is restarted.)
531 572 buttons = QtGui.QMessageBox.Yes | QtGui.QMessageBox.No
532 573 result = QtGui.QMessageBox.question(self, 'Restart kernel?',
533 574 message, buttons)
534 575 if result == QtGui.QMessageBox.Yes:
535 576 try:
536 577 self.kernel_manager.restart_kernel(now=now)
537 578 except RuntimeError:
538 579 self._append_plain_text('Kernel started externally. '
539 580 'Cannot restart.\n')
540 581 else:
541 582 self.reset()
542 583 else:
543 584 self.kernel_manager.hb_channel.unpause()
544 585
545 586 else:
546 587 self._append_plain_text('Kernel process is either remote or '
547 588 'unspecified. Cannot restart.\n')
548 589
549 590 #---------------------------------------------------------------------------
550 591 # 'FrontendWidget' protected interface
551 592 #---------------------------------------------------------------------------
552 593
553 594 def _call_tip(self):
554 595 """ Shows a call tip, if appropriate, at the current cursor location.
555 596 """
556 597 # Decide if it makes sense to show a call tip
557 598 if not self.enable_calltips:
558 599 return False
559 600 cursor = self._get_cursor()
560 601 cursor.movePosition(QtGui.QTextCursor.Left)
561 602 if cursor.document().characterAt(cursor.position()) != '(':
562 603 return False
563 604 context = self._get_context(cursor)
564 605 if not context:
565 606 return False
566 607
567 608 # Send the metadata request to the kernel
568 609 name = '.'.join(context)
569 610 msg_id = self.kernel_manager.shell_channel.object_info(name)
570 611 pos = self._get_cursor().position()
571 612 self._request_info['call_tip'] = self._CallTipRequest(msg_id, pos)
572 613 return True
573 614
574 615 def _complete(self):
575 616 """ Performs completion at the current cursor location.
576 617 """
577 618 context = self._get_context()
578 619 if context:
579 620 # Send the completion request to the kernel
580 621 msg_id = self.kernel_manager.shell_channel.complete(
581 622 '.'.join(context), # text
582 623 self._get_input_buffer_cursor_line(), # line
583 624 self._get_input_buffer_cursor_column(), # cursor_pos
584 625 self.input_buffer) # block
585 626 pos = self._get_cursor().position()
586 627 info = self._CompletionRequest(msg_id, pos)
587 628 self._request_info['complete'] = info
588 629
589 630 def _get_context(self, cursor=None):
590 631 """ Gets the context for the specified cursor (or the current cursor
591 632 if none is specified).
592 633 """
593 634 if cursor is None:
594 635 cursor = self._get_cursor()
595 636 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
596 637 QtGui.QTextCursor.KeepAnchor)
597 638 text = cursor.selection().toPlainText()
598 639 return self._completion_lexer.get_context(text)
599 640
600 641 def _process_execute_abort(self, msg):
601 642 """ Process a reply for an aborted execution request.
602 643 """
603 644 self._append_plain_text("ERROR: execution aborted\n")
604 645
605 646 def _process_execute_error(self, msg):
606 647 """ Process a reply for an execution request that resulted in an error.
607 648 """
608 649 content = msg['content']
609 650 # If a SystemExit is passed along, this means exit() was called - also
610 651 # all the ipython %exit magic syntax of '-k' to be used to keep
611 652 # the kernel running
612 653 if content['ename']=='SystemExit':
613 654 keepkernel = content['evalue']=='-k' or content['evalue']=='True'
614 655 self._keep_kernel_on_exit = keepkernel
615 656 self.exit_requested.emit(self)
616 657 else:
617 658 traceback = ''.join(content['traceback'])
618 659 self._append_plain_text(traceback)
619 660
620 661 def _process_execute_ok(self, msg):
621 662 """ Process a reply for a successful execution equest.
622 663 """
623 664 payload = msg['content']['payload']
624 665 for item in payload:
625 666 if not self._process_execute_payload(item):
626 667 warning = 'Warning: received unknown payload of type %s'
627 668 print(warning % repr(item['source']))
628 669
629 670 def _process_execute_payload(self, item):
630 671 """ Process a single payload item from the list of payload items in an
631 672 execution reply. Returns whether the payload was handled.
632 673 """
633 674 # The basic FrontendWidget doesn't handle payloads, as they are a
634 675 # mechanism for going beyond the standard Python interpreter model.
635 676 return False
636 677
637 678 def _show_interpreter_prompt(self):
638 679 """ Shows a prompt for the interpreter.
639 680 """
640 681 self._show_prompt('>>> ')
641 682
642 683 def _show_interpreter_prompt_for_reply(self, msg):
643 684 """ Shows a prompt for the interpreter given an 'execute_reply' message.
644 685 """
645 686 self._show_interpreter_prompt()
646 687
647 688 #------ Signal handlers ----------------------------------------------------
648 689
649 690 def _document_contents_change(self, position, removed, added):
650 691 """ Called whenever the document's content changes. Display a call tip
651 692 if appropriate.
652 693 """
653 694 # Calculate where the cursor should be *after* the change:
654 695 position += added
655 696
656 697 document = self._control.document()
657 698 if position == self._get_cursor().position():
658 699 self._call_tip()
659 700
660 701 #------ Trait default initializers -----------------------------------------
661 702
662 703 def _banner_default(self):
663 704 """ Returns the standard Python banner.
664 705 """
665 706 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
666 707 '"license" for more information.'
667 708 return banner % (sys.version, sys.platform)
@@ -1,851 +1,876 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 23 import webbrowser
24 24 from threading import Thread
25 25
26 26 # System library imports
27 27 from IPython.external.qt import QtGui,QtCore
28 28
29 29 def background(f):
30 30 """call a function in a simple thread, to prevent blocking"""
31 31 t = Thread(target=f)
32 32 t.start()
33 33 return t
34 34
35 35 #-----------------------------------------------------------------------------
36 36 # Classes
37 37 #-----------------------------------------------------------------------------
38 38
39 39 class MainWindow(QtGui.QMainWindow):
40 40
41 41 #---------------------------------------------------------------------------
42 42 # 'object' interface
43 43 #---------------------------------------------------------------------------
44 44
45 45 def __init__(self, app,
46 46 confirm_exit=True,
47 47 new_frontend_factory=None, slave_frontend_factory=None,
48 48 ):
49 49 """ Create a tabbed MainWindow for managing IPython FrontendWidgets
50 50
51 51 Parameters
52 52 ----------
53 53
54 54 app : reference to QApplication parent
55 55 confirm_exit : bool, optional
56 56 Whether we should prompt on close of tabs
57 57 new_frontend_factory : callable
58 58 A callable that returns a new IPythonWidget instance, attached to
59 59 its own running kernel.
60 60 slave_frontend_factory : callable
61 61 A callable that takes an existing IPythonWidget, and returns a new
62 62 IPythonWidget instance, attached to the same kernel.
63 63 """
64 64
65 65 super(MainWindow, self).__init__()
66 66 self._kernel_counter = 0
67 67 self._app = app
68 68 self.confirm_exit = confirm_exit
69 69 self.new_frontend_factory = new_frontend_factory
70 70 self.slave_frontend_factory = slave_frontend_factory
71 71
72 72 self.tab_widget = QtGui.QTabWidget(self)
73 73 self.tab_widget.setDocumentMode(True)
74 74 self.tab_widget.setTabsClosable(True)
75 75 self.tab_widget.tabCloseRequested[int].connect(self.close_tab)
76 76
77 77 self.setCentralWidget(self.tab_widget)
78 78 # hide tab bar at first, since we have no tabs:
79 79 self.tab_widget.tabBar().setVisible(False)
80 80 # prevent focus in tab bar
81 81 self.tab_widget.setFocusPolicy(QtCore.Qt.NoFocus)
82 82
83 83 def update_tab_bar_visibility(self):
84 84 """ update visibility of the tabBar depending of the number of tab
85 85
86 86 0 or 1 tab, tabBar hidden
87 87 2+ tabs, tabBar visible
88 88
89 89 send a self.close if number of tab ==0
90 90
91 91 need to be called explicitely, or be connected to tabInserted/tabRemoved
92 92 """
93 93 if self.tab_widget.count() <= 1:
94 94 self.tab_widget.tabBar().setVisible(False)
95 95 else:
96 96 self.tab_widget.tabBar().setVisible(True)
97 97 if self.tab_widget.count()==0 :
98 98 self.close()
99 99
100 100 @property
101 101 def next_kernel_id(self):
102 102 """constantly increasing counter for kernel IDs"""
103 103 c = self._kernel_counter
104 104 self._kernel_counter += 1
105 105 return c
106 106
107 107 @property
108 108 def active_frontend(self):
109 109 return self.tab_widget.currentWidget()
110 110
111 111 def create_tab_with_new_frontend(self):
112 112 """create a new frontend and attach it to a new tab"""
113 113 widget = self.new_frontend_factory()
114 114 self.add_tab_with_frontend(widget)
115 115
116 116 def create_tab_with_current_kernel(self):
117 117 """create a new frontend attached to the same kernel as the current tab"""
118 118 current_widget = self.tab_widget.currentWidget()
119 119 current_widget_index = self.tab_widget.indexOf(current_widget)
120 120 current_widget_name = self.tab_widget.tabText(current_widget_index)
121 121 widget = self.slave_frontend_factory(current_widget)
122 122 if 'slave' in current_widget_name:
123 123 # don't keep stacking slaves
124 124 name = current_widget_name
125 125 else:
126 126 name = '(%s) slave' % current_widget_name
127 127 self.add_tab_with_frontend(widget,name=name)
128 128
129 129 def close_tab(self,current_tab):
130 130 """ Called when you need to try to close a tab.
131 131
132 132 It takes the number of the tab to be closed as argument, or a referece
133 133 to the wiget insite this tab
134 134 """
135 135
136 136 # let's be sure "tab" and "closing widget are respectivey the index of the tab to close
137 137 # and a reference to the trontend to close
138 138 if type(current_tab) is not int :
139 139 current_tab = self.tab_widget.indexOf(current_tab)
140 140 closing_widget=self.tab_widget.widget(current_tab)
141 141
142 142
143 143 # when trying to be closed, widget might re-send a request to be closed again, but will
144 144 # be deleted when event will be processed. So need to check that widget still exist and
145 145 # skip if not. One example of this is when 'exit' is send in a slave tab. 'exit' will be
146 146 # re-send by this fonction on the master widget, which ask all slaves widget to exit
147 147 if closing_widget==None:
148 148 return
149 149
150 150 #get a list of all slave widgets on the same kernel.
151 151 slave_tabs = self.find_slave_widgets(closing_widget)
152 152
153 153 keepkernel = None #Use the prompt by default
154 154 if hasattr(closing_widget,'_keep_kernel_on_exit'): #set by exit magic
155 155 keepkernel = closing_widget._keep_kernel_on_exit
156 156 # If signal sent by exit magic (_keep_kernel_on_exit, exist and not None)
157 157 # we set local slave tabs._hidden to True to avoid prompting for kernel
158 158 # restart when they get the signal. and then "forward" the 'exit'
159 159 # to the main window
160 160 if keepkernel is not None:
161 161 for tab in slave_tabs:
162 162 tab._hidden = True
163 163 if closing_widget in slave_tabs:
164 164 try :
165 165 self.find_master_tab(closing_widget).execute('exit')
166 166 except AttributeError:
167 167 self.log.info("Master already closed or not local, closing only current tab")
168 168 self.tab_widget.removeTab(current_tab)
169 169 self.update_tab_bar_visibility()
170 170 return
171 171
172 172 kernel_manager = closing_widget.kernel_manager
173 173
174 174 if keepkernel is None and not closing_widget._confirm_exit:
175 175 # don't prompt, just terminate the kernel if we own it
176 176 # or leave it alone if we don't
177 177 keepkernel = closing_widget._existing
178 178 if keepkernel is None: #show prompt
179 179 if kernel_manager and kernel_manager.channels_running:
180 180 title = self.window().windowTitle()
181 181 cancel = QtGui.QMessageBox.Cancel
182 182 okay = QtGui.QMessageBox.Ok
183 183 if closing_widget._may_close:
184 184 msg = "You are closing the tab : "+'"'+self.tab_widget.tabText(current_tab)+'"'
185 185 info = "Would you like to quit the Kernel and close all attached Consoles as well?"
186 186 justthis = QtGui.QPushButton("&No, just this Tab", self)
187 187 justthis.setShortcut('N')
188 188 closeall = QtGui.QPushButton("&Yes, close all", self)
189 189 closeall.setShortcut('Y')
190 190 box = QtGui.QMessageBox(QtGui.QMessageBox.Question,
191 191 title, msg)
192 192 box.setInformativeText(info)
193 193 box.addButton(cancel)
194 194 box.addButton(justthis, QtGui.QMessageBox.NoRole)
195 195 box.addButton(closeall, QtGui.QMessageBox.YesRole)
196 196 box.setDefaultButton(closeall)
197 197 box.setEscapeButton(cancel)
198 198 pixmap = QtGui.QPixmap(self._app.icon.pixmap(QtCore.QSize(64,64)))
199 199 box.setIconPixmap(pixmap)
200 200 reply = box.exec_()
201 201 if reply == 1: # close All
202 202 for slave in slave_tabs:
203 203 background(slave.kernel_manager.stop_channels)
204 204 self.tab_widget.removeTab(self.tab_widget.indexOf(slave))
205 205 closing_widget.execute("exit")
206 206 self.tab_widget.removeTab(current_tab)
207 207 background(kernel_manager.stop_channels)
208 208 elif reply == 0: # close Console
209 209 if not closing_widget._existing:
210 210 # Have kernel: don't quit, just close the tab
211 211 closing_widget.execute("exit True")
212 212 self.tab_widget.removeTab(current_tab)
213 213 background(kernel_manager.stop_channels)
214 214 else:
215 215 reply = QtGui.QMessageBox.question(self, title,
216 216 "Are you sure you want to close this Console?"+
217 217 "\nThe Kernel and other Consoles will remain active.",
218 218 okay|cancel,
219 219 defaultButton=okay
220 220 )
221 221 if reply == okay:
222 222 self.tab_widget.removeTab(current_tab)
223 223 elif keepkernel: #close console but leave kernel running (no prompt)
224 224 self.tab_widget.removeTab(current_tab)
225 225 background(kernel_manager.stop_channels)
226 226 else: #close console and kernel (no prompt)
227 227 self.tab_widget.removeTab(current_tab)
228 228 if kernel_manager and kernel_manager.channels_running:
229 229 for slave in slave_tabs:
230 230 background(slave.kernel_manager.stop_channels)
231 231 self.tab_widget.removeTab(self.tab_widget.indexOf(slave))
232 232 kernel_manager.shutdown_kernel()
233 233 background(kernel_manager.stop_channels)
234 234
235 235 self.update_tab_bar_visibility()
236 236
237 237 def add_tab_with_frontend(self,frontend,name=None):
238 238 """ insert a tab with a given frontend in the tab bar, and give it a name
239 239
240 240 """
241 241 if not name:
242 242 name = 'kernel %i' % self.next_kernel_id
243 243 self.tab_widget.addTab(frontend,name)
244 244 self.update_tab_bar_visibility()
245 245 self.make_frontend_visible(frontend)
246 246 frontend.exit_requested.connect(self.close_tab)
247 247
248 248 def next_tab(self):
249 249 self.tab_widget.setCurrentIndex((self.tab_widget.currentIndex()+1))
250 250
251 251 def prev_tab(self):
252 252 self.tab_widget.setCurrentIndex((self.tab_widget.currentIndex()-1))
253 253
254 254 def make_frontend_visible(self,frontend):
255 255 widget_index=self.tab_widget.indexOf(frontend)
256 256 if widget_index > 0 :
257 257 self.tab_widget.setCurrentIndex(widget_index)
258 258
259 259 def find_master_tab(self,tab,as_list=False):
260 260 """
261 261 Try to return the frontend that own the kernel attached to the given widget/tab.
262 262
263 263 Only find frontend owed by the current application. Selection
264 264 based on port of the kernel, might be inacurate if several kernel
265 265 on different ip use same port number.
266 266
267 267 This fonction does the conversion tabNumber/widget if needed.
268 268 Might return None if no master widget (non local kernel)
269 269 Will crash IPython if more than 1 masterWidget
270 270
271 271 When asList set to True, always return a list of widget(s) owning
272 272 the kernel. The list might be empty or containing several Widget.
273 273 """
274 274
275 275 #convert from/to int/richIpythonWidget if needed
276 276 if isinstance(tab, int):
277 277 tab = self.tab_widget.widget(tab)
278 278 km=tab.kernel_manager
279 279
280 280 #build list of all widgets
281 281 widget_list = [self.tab_widget.widget(i) for i in range(self.tab_widget.count())]
282 282
283 283 # widget that are candidate to be the owner of the kernel does have all the same port of the curent widget
284 284 # And should have a _may_close attribute
285 285 filtered_widget_list = [ widget for widget in widget_list if
286 286 widget.kernel_manager.connection_file == km.connection_file and
287 287 hasattr(widget,'_may_close') ]
288 288 # the master widget is the one that may close the kernel
289 289 master_widget= [ widget for widget in filtered_widget_list if widget._may_close]
290 290 if as_list:
291 291 return master_widget
292 292 assert(len(master_widget)<=1 )
293 293 if len(master_widget)==0:
294 294 return None
295 295
296 296 return master_widget[0]
297 297
298 298 def find_slave_widgets(self,tab):
299 299 """return all the frontends that do not own the kernel attached to the given widget/tab.
300 300
301 301 Only find frontends owned by the current application. Selection
302 302 based on connection file of the kernel.
303 303
304 304 This function does the conversion tabNumber/widget if needed.
305 305 """
306 306 #convert from/to int/richIpythonWidget if needed
307 307 if isinstance(tab, int):
308 308 tab = self.tab_widget.widget(tab)
309 309 km=tab.kernel_manager
310 310
311 311 #build list of all widgets
312 312 widget_list = [self.tab_widget.widget(i) for i in range(self.tab_widget.count())]
313 313
314 314 # widget that are candidate not to be the owner of the kernel does have all the same port of the curent widget
315 315 filtered_widget_list = ( widget for widget in widget_list if
316 316 widget.kernel_manager.connection_file == km.connection_file)
317 317 # Get a list of all widget owning the same kernel and removed it from
318 318 # the previous cadidate. (better using sets ?)
319 319 master_widget_list = self.find_master_tab(tab, as_list=True)
320 320 slave_list = [widget for widget in filtered_widget_list if widget not in master_widget_list]
321 321
322 322 return slave_list
323 323
324 324 # Populate the menu bar with common actions and shortcuts
325 325 def add_menu_action(self, menu, action, defer_shortcut=False):
326 326 """Add action to menu as well as self
327 327
328 328 So that when the menu bar is invisible, its actions are still available.
329 329
330 330 If defer_shortcut is True, set the shortcut context to widget-only,
331 331 where it will avoid conflict with shortcuts already bound to the
332 332 widgets themselves.
333 333 """
334 334 menu.addAction(action)
335 335 self.addAction(action)
336 336
337 337 if defer_shortcut:
338 338 action.setShortcutContext(QtCore.Qt.WidgetShortcut)
339 339
340 340 def init_menu_bar(self):
341 341 #create menu in the order they should appear in the menu bar
342 342 self.init_file_menu()
343 343 self.init_edit_menu()
344 344 self.init_view_menu()
345 345 self.init_kernel_menu()
346 346 self.init_magic_menu()
347 347 self.init_window_menu()
348 348 self.init_help_menu()
349 349
350 350 def init_file_menu(self):
351 351 self.file_menu = self.menuBar().addMenu("&File")
352 352
353 353 self.new_kernel_tab_act = QtGui.QAction("New Tab with &New kernel",
354 354 self,
355 355 shortcut="Ctrl+T",
356 356 triggered=self.create_tab_with_new_frontend)
357 357 self.add_menu_action(self.file_menu, self.new_kernel_tab_act)
358 358
359 359 self.slave_kernel_tab_act = QtGui.QAction("New Tab with Sa&me kernel",
360 360 self,
361 361 shortcut="Ctrl+Shift+T",
362 362 triggered=self.create_tab_with_current_kernel)
363 363 self.add_menu_action(self.file_menu, self.slave_kernel_tab_act)
364 364
365 365 self.file_menu.addSeparator()
366 366
367 367 self.close_action=QtGui.QAction("&Close Tab",
368 368 self,
369 369 shortcut=QtGui.QKeySequence.Close,
370 370 triggered=self.close_active_frontend
371 371 )
372 372 self.add_menu_action(self.file_menu, self.close_action)
373 373
374 374 self.export_action=QtGui.QAction("&Save to HTML/XHTML",
375 375 self,
376 376 shortcut=QtGui.QKeySequence.Save,
377 377 triggered=self.export_action_active_frontend
378 378 )
379 379 self.add_menu_action(self.file_menu, self.export_action, True)
380 380
381 381 self.file_menu.addSeparator()
382 382
383 383 printkey = QtGui.QKeySequence(QtGui.QKeySequence.Print)
384 384 if printkey.matches("Ctrl+P") and sys.platform != 'darwin':
385 385 # Only override the default if there is a collision.
386 386 # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
387 387 printkey = "Ctrl+Shift+P"
388 388 self.print_action = QtGui.QAction("&Print",
389 389 self,
390 390 shortcut=printkey,
391 391 triggered=self.print_action_active_frontend)
392 392 self.add_menu_action(self.file_menu, self.print_action, True)
393 393
394 394 if sys.platform != 'darwin':
395 395 # OSX always has Quit in the Application menu, only add it
396 396 # to the File menu elsewhere.
397 397
398 398 self.file_menu.addSeparator()
399 399
400 400 self.quit_action = QtGui.QAction("&Quit",
401 401 self,
402 402 shortcut=QtGui.QKeySequence.Quit,
403 403 triggered=self.close,
404 404 )
405 405 self.add_menu_action(self.file_menu, self.quit_action)
406 406
407 407
408 408 def init_edit_menu(self):
409 409 self.edit_menu = self.menuBar().addMenu("&Edit")
410 410
411 411 self.undo_action = QtGui.QAction("&Undo",
412 412 self,
413 413 shortcut=QtGui.QKeySequence.Undo,
414 414 statusTip="Undo last action if possible",
415 415 triggered=self.undo_active_frontend
416 416 )
417 417 self.add_menu_action(self.edit_menu, self.undo_action)
418 418
419 419 self.redo_action = QtGui.QAction("&Redo",
420 420 self,
421 421 shortcut=QtGui.QKeySequence.Redo,
422 422 statusTip="Redo last action if possible",
423 423 triggered=self.redo_active_frontend)
424 424 self.add_menu_action(self.edit_menu, self.redo_action)
425 425
426 426 self.edit_menu.addSeparator()
427 427
428 428 self.cut_action = QtGui.QAction("&Cut",
429 429 self,
430 430 shortcut=QtGui.QKeySequence.Cut,
431 431 triggered=self.cut_active_frontend
432 432 )
433 433 self.add_menu_action(self.edit_menu, self.cut_action, True)
434 434
435 435 self.copy_action = QtGui.QAction("&Copy",
436 436 self,
437 437 shortcut=QtGui.QKeySequence.Copy,
438 438 triggered=self.copy_active_frontend
439 439 )
440 440 self.add_menu_action(self.edit_menu, self.copy_action, True)
441 441
442 442 self.copy_raw_action = QtGui.QAction("Copy (&Raw Text)",
443 443 self,
444 444 shortcut="Ctrl+Shift+C",
445 445 triggered=self.copy_raw_active_frontend
446 446 )
447 447 self.add_menu_action(self.edit_menu, self.copy_raw_action, True)
448 448
449 449 self.paste_action = QtGui.QAction("&Paste",
450 450 self,
451 451 shortcut=QtGui.QKeySequence.Paste,
452 452 triggered=self.paste_active_frontend
453 453 )
454 454 self.add_menu_action(self.edit_menu, self.paste_action, True)
455 455
456 456 self.edit_menu.addSeparator()
457 457
458 458 selectall = QtGui.QKeySequence(QtGui.QKeySequence.SelectAll)
459 459 if selectall.matches("Ctrl+A") and sys.platform != 'darwin':
460 460 # Only override the default if there is a collision.
461 461 # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
462 462 selectall = "Ctrl+Shift+A"
463 463 self.select_all_action = QtGui.QAction("Select &All",
464 464 self,
465 465 shortcut=selectall,
466 466 triggered=self.select_all_active_frontend
467 467 )
468 468 self.add_menu_action(self.edit_menu, self.select_all_action, True)
469 469
470 470
471 471 def init_view_menu(self):
472 472 self.view_menu = self.menuBar().addMenu("&View")
473 473
474 474 if sys.platform != 'darwin':
475 475 # disable on OSX, where there is always a menu bar
476 476 self.toggle_menu_bar_act = QtGui.QAction("Toggle &Menu Bar",
477 477 self,
478 478 shortcut="Ctrl+Shift+M",
479 479 statusTip="Toggle visibility of menubar",
480 480 triggered=self.toggle_menu_bar)
481 481 self.add_menu_action(self.view_menu, self.toggle_menu_bar_act)
482 482
483 483 fs_key = "Ctrl+Meta+F" if sys.platform == 'darwin' else "F11"
484 484 self.full_screen_act = QtGui.QAction("&Full Screen",
485 485 self,
486 486 shortcut=fs_key,
487 487 statusTip="Toggle between Fullscreen and Normal Size",
488 488 triggered=self.toggleFullScreen)
489 489 self.add_menu_action(self.view_menu, self.full_screen_act)
490 490
491 491 self.view_menu.addSeparator()
492 492
493 493 self.increase_font_size = QtGui.QAction("Zoom &In",
494 494 self,
495 495 shortcut=QtGui.QKeySequence.ZoomIn,
496 496 triggered=self.increase_font_size_active_frontend
497 497 )
498 498 self.add_menu_action(self.view_menu, self.increase_font_size, True)
499 499
500 500 self.decrease_font_size = QtGui.QAction("Zoom &Out",
501 501 self,
502 502 shortcut=QtGui.QKeySequence.ZoomOut,
503 503 triggered=self.decrease_font_size_active_frontend
504 504 )
505 505 self.add_menu_action(self.view_menu, self.decrease_font_size, True)
506 506
507 507 self.reset_font_size = QtGui.QAction("Zoom &Reset",
508 508 self,
509 509 shortcut="Ctrl+0",
510 510 triggered=self.reset_font_size_active_frontend
511 511 )
512 512 self.add_menu_action(self.view_menu, self.reset_font_size, True)
513 513
514 514 self.view_menu.addSeparator()
515 515
516 516 self.clear_action = QtGui.QAction("&Clear Screen",
517 517 self,
518 518 shortcut='Ctrl+L',
519 519 statusTip="Clear the console",
520 520 triggered=self.clear_magic_active_frontend)
521 521 self.add_menu_action(self.view_menu, self.clear_action)
522 522
523 523 def init_kernel_menu(self):
524 524 self.kernel_menu = self.menuBar().addMenu("&Kernel")
525 525 # Qt on OSX maps Ctrl to Cmd, and Meta to Ctrl
526 526 # keep the signal shortcuts to ctrl, rather than
527 527 # platform-default like we do elsewhere.
528 528
529 529 ctrl = "Meta" if sys.platform == 'darwin' else "Ctrl"
530 530
531 531 self.interrupt_kernel_action = QtGui.QAction("Interrupt current Kernel",
532 532 self,
533 533 triggered=self.interrupt_kernel_active_frontend,
534 534 shortcut=ctrl+"+C",
535 535 )
536 536 self.add_menu_action(self.kernel_menu, self.interrupt_kernel_action)
537 537
538 538 self.restart_kernel_action = QtGui.QAction("Restart current Kernel",
539 539 self,
540 540 triggered=self.restart_kernel_active_frontend,
541 541 shortcut=ctrl+"+.",
542 542 )
543 543 self.add_menu_action(self.kernel_menu, self.restart_kernel_action)
544 544
545 545 self.kernel_menu.addSeparator()
546 546
547 def update_all_magic_menu(self):
548 # first define a callback which will get the list of all magic and put it in the menu.
549 def populate_all_magic_menu(val=None):
550 alm_magic_menu = self.magic_menu.addMenu("&All Magics...")
551 def make_dynamic_magic(i):
552 def inner_dynamic_magic():
553 self.active_frontend.execute(i)
554 inner_dynamic_magic.__name__ = "dynamics_magic_s"
555 return inner_dynamic_magic
556
557 for magic in eval(val):
558 pmagic = '%s%s'%('%',magic)
559 xaction = QtGui.QAction(pmagic,
560 self,
561 triggered=make_dynamic_magic(pmagic)
562 )
563 alm_magic_menu.addAction(xaction)
564 self.active_frontend._silent_exec_callback('get_ipython().lsmagic()',populate_all_magic_menu)
565
547 566 def init_magic_menu(self):
548 567 self.magic_menu = self.menuBar().addMenu("&Magic")
549 568 self.all_magic_menu = self.magic_menu.addMenu("&All Magics")
550 569
570 self.pop = QtGui.QAction("&Populate All Magic Menu",
571 self,
572 statusTip="Clear all varible from workspace",
573 triggered=self.update_all_magic_menu)
574 self.add_menu_action(self.magic_menu, self.pop)
575
551 576 self.reset_action = QtGui.QAction("&Reset",
552 577 self,
553 578 statusTip="Clear all varible from workspace",
554 579 triggered=self.reset_magic_active_frontend)
555 580 self.add_menu_action(self.magic_menu, self.reset_action)
556 581
557 582 self.history_action = QtGui.QAction("&History",
558 583 self,
559 584 statusTip="show command history",
560 585 triggered=self.history_magic_active_frontend)
561 586 self.add_menu_action(self.magic_menu, self.history_action)
562 587
563 588 self.save_action = QtGui.QAction("E&xport History ",
564 589 self,
565 590 statusTip="Export History as Python File",
566 591 triggered=self.save_magic_active_frontend)
567 592 self.add_menu_action(self.magic_menu, self.save_action)
568 593
569 594 self.who_action = QtGui.QAction("&Who",
570 595 self,
571 596 statusTip="List interactive variable",
572 597 triggered=self.who_magic_active_frontend)
573 598 self.add_menu_action(self.magic_menu, self.who_action)
574 599
575 600 self.who_ls_action = QtGui.QAction("Wh&o ls",
576 601 self,
577 602 statusTip="Return a list of interactive variable",
578 603 triggered=self.who_ls_magic_active_frontend)
579 604 self.add_menu_action(self.magic_menu, self.who_ls_action)
580 605
581 606 self.whos_action = QtGui.QAction("Who&s",
582 607 self,
583 608 statusTip="List interactive variable with detail",
584 609 triggered=self.whos_magic_active_frontend)
585 610 self.add_menu_action(self.magic_menu, self.whos_action)
586 611
587 612 # allmagics submenu:
588 613
589 614 #for now this is just a copy and paste, but we should get this dynamically
590 615 magiclist=["%alias", "%autocall", "%automagic", "%bookmark", "%cd", "%clear",
591 616 "%colors", "%debug", "%dhist", "%dirs", "%doctest_mode", "%ed", "%edit", "%env", "%gui",
592 617 "%guiref", "%hist", "%history", "%install_default_config", "%install_profiles",
593 618 "%less", "%load_ext", "%loadpy", "%logoff", "%logon", "%logstart", "%logstate",
594 619 "%logstop", "%lsmagic", "%macro", "%magic", "%man", "%more", "%notebook", "%page",
595 620 "%pastebin", "%pdb", "%pdef", "%pdoc", "%pfile", "%pinfo", "%pinfo2", "%popd", "%pprint",
596 621 "%precision", "%profile", "%prun", "%psearch", "%psource", "%pushd", "%pwd", "%pycat",
597 622 "%pylab", "%quickref", "%recall", "%rehashx", "%reload_ext", "%rep", "%rerun",
598 623 "%reset", "%reset_selective", "%run", "%save", "%sc", "%sx", "%tb", "%time", "%timeit",
599 624 "%unalias", "%unload_ext", "%who", "%who_ls", "%whos", "%xdel", "%xmode"]
600 625
601 626 def make_dynamic_magic(i):
602 627 def inner_dynamic_magic():
603 628 self.active_frontend.execute(i)
604 629 inner_dynamic_magic.__name__ = "dynamics_magic_%s" % i
605 630 return inner_dynamic_magic
606 631
607 632 for magic in magiclist:
608 633 xaction = QtGui.QAction(magic,
609 634 self,
610 635 triggered=make_dynamic_magic(magic)
611 636 )
612 637 self.all_magic_menu.addAction(xaction)
613 638
614 639 def init_window_menu(self):
615 640 self.window_menu = self.menuBar().addMenu("&Window")
616 641 if sys.platform == 'darwin':
617 642 # add min/maximize actions to OSX, which lacks default bindings.
618 643 self.minimizeAct = QtGui.QAction("Mini&mize",
619 644 self,
620 645 shortcut="Ctrl+m",
621 646 statusTip="Minimize the window/Restore Normal Size",
622 647 triggered=self.toggleMinimized)
623 648 # maximize is called 'Zoom' on OSX for some reason
624 649 self.maximizeAct = QtGui.QAction("&Zoom",
625 650 self,
626 651 shortcut="Ctrl+Shift+M",
627 652 statusTip="Maximize the window/Restore Normal Size",
628 653 triggered=self.toggleMaximized)
629 654
630 655 self.add_menu_action(self.window_menu, self.minimizeAct)
631 656 self.add_menu_action(self.window_menu, self.maximizeAct)
632 657 self.window_menu.addSeparator()
633 658
634 659 prev_key = "Ctrl+Shift+Left" if sys.platform == 'darwin' else "Ctrl+PgUp"
635 660 self.prev_tab_act = QtGui.QAction("Pre&vious Tab",
636 661 self,
637 662 shortcut=prev_key,
638 663 statusTip="Select previous tab",
639 664 triggered=self.prev_tab)
640 665 self.add_menu_action(self.window_menu, self.prev_tab_act)
641 666
642 667 next_key = "Ctrl+Shift+Right" if sys.platform == 'darwin' else "Ctrl+PgDown"
643 668 self.next_tab_act = QtGui.QAction("Ne&xt Tab",
644 669 self,
645 670 shortcut=next_key,
646 671 statusTip="Select next tab",
647 672 triggered=self.next_tab)
648 673 self.add_menu_action(self.window_menu, self.next_tab_act)
649 674
650 675 def init_help_menu(self):
651 676 # please keep the Help menu in Mac Os even if empty. It will
652 677 # automatically contain a search field to search inside menus and
653 678 # please keep it spelled in English, as long as Qt Doesn't support
654 679 # a QAction.MenuRole like HelpMenuRole otherwise it will loose
655 680 # this search field fonctionality
656 681
657 682 self.help_menu = self.menuBar().addMenu("&Help")
658 683
659 684
660 685 # Help Menu
661 686
662 687 self.intro_active_frontend_action = QtGui.QAction("&Intro to IPython",
663 688 self,
664 689 triggered=self.intro_active_frontend
665 690 )
666 691 self.add_menu_action(self.help_menu, self.intro_active_frontend_action)
667 692
668 693 self.quickref_active_frontend_action = QtGui.QAction("IPython &Cheat Sheet",
669 694 self,
670 695 triggered=self.quickref_active_frontend
671 696 )
672 697 self.add_menu_action(self.help_menu, self.quickref_active_frontend_action)
673 698
674 699 self.guiref_active_frontend_action = QtGui.QAction("&Qt Console",
675 700 self,
676 701 triggered=self.guiref_active_frontend
677 702 )
678 703 self.add_menu_action(self.help_menu, self.guiref_active_frontend_action)
679 704
680 705 self.onlineHelpAct = QtGui.QAction("Open Online &Help",
681 706 self,
682 707 triggered=self._open_online_help)
683 708 self.add_menu_action(self.help_menu, self.onlineHelpAct)
684 709
685 710 # minimize/maximize/fullscreen actions:
686 711
687 712 def toggle_menu_bar(self):
688 713 menu_bar = self.menuBar()
689 714 if menu_bar.isVisible():
690 715 menu_bar.setVisible(False)
691 716 else:
692 717 menu_bar.setVisible(True)
693 718
694 719 def toggleMinimized(self):
695 720 if not self.isMinimized():
696 721 self.showMinimized()
697 722 else:
698 723 self.showNormal()
699 724
700 725 def _open_online_help(self):
701 726 filename="http://ipython.org/ipython-doc/stable/index.html"
702 727 webbrowser.open(filename, new=1, autoraise=True)
703 728
704 729 def toggleMaximized(self):
705 730 if not self.isMaximized():
706 731 self.showMaximized()
707 732 else:
708 733 self.showNormal()
709 734
710 735 # Min/Max imizing while in full screen give a bug
711 736 # when going out of full screen, at least on OSX
712 737 def toggleFullScreen(self):
713 738 if not self.isFullScreen():
714 739 self.showFullScreen()
715 740 if sys.platform == 'darwin':
716 741 self.maximizeAct.setEnabled(False)
717 742 self.minimizeAct.setEnabled(False)
718 743 else:
719 744 self.showNormal()
720 745 if sys.platform == 'darwin':
721 746 self.maximizeAct.setEnabled(True)
722 747 self.minimizeAct.setEnabled(True)
723 748
724 749 def close_active_frontend(self):
725 750 self.close_tab(self.active_frontend)
726 751
727 752 def restart_kernel_active_frontend(self):
728 753 self.active_frontend.request_restart_kernel()
729 754
730 755 def interrupt_kernel_active_frontend(self):
731 756 self.active_frontend.request_interrupt_kernel()
732 757
733 758 def cut_active_frontend(self):
734 759 widget = self.active_frontend
735 760 if widget.can_cut():
736 761 widget.cut()
737 762
738 763 def copy_active_frontend(self):
739 764 widget = self.active_frontend
740 765 if widget.can_copy():
741 766 widget.copy()
742 767
743 768 def copy_raw_active_frontend(self):
744 769 self.active_frontend._copy_raw_action.trigger()
745 770
746 771 def paste_active_frontend(self):
747 772 widget = self.active_frontend
748 773 if widget.can_paste():
749 774 widget.paste()
750 775
751 776 def undo_active_frontend(self):
752 777 self.active_frontend.undo()
753 778
754 779 def redo_active_frontend(self):
755 780 self.active_frontend.redo()
756 781
757 782 def reset_magic_active_frontend(self):
758 783 self.active_frontend.execute("%reset")
759 784
760 785 def history_magic_active_frontend(self):
761 786 self.active_frontend.execute("%history")
762 787
763 788 def save_magic_active_frontend(self):
764 789 self.active_frontend.save_magic()
765 790
766 791 def clear_magic_active_frontend(self):
767 792 self.active_frontend.execute("%clear")
768 793
769 794 def who_magic_active_frontend(self):
770 795 self.active_frontend.execute("%who")
771 796
772 797 def who_ls_magic_active_frontend(self):
773 798 self.active_frontend.execute("%who_ls")
774 799
775 800 def whos_magic_active_frontend(self):
776 801 self.active_frontend.execute("%whos")
777 802
778 803 def print_action_active_frontend(self):
779 804 self.active_frontend.print_action.trigger()
780 805
781 806 def export_action_active_frontend(self):
782 807 self.active_frontend.export_action.trigger()
783 808
784 809 def select_all_active_frontend(self):
785 810 self.active_frontend.select_all_action.trigger()
786 811
787 812 def increase_font_size_active_frontend(self):
788 813 self.active_frontend.increase_font_size.trigger()
789 814
790 815 def decrease_font_size_active_frontend(self):
791 816 self.active_frontend.decrease_font_size.trigger()
792 817
793 818 def reset_font_size_active_frontend(self):
794 819 self.active_frontend.reset_font_size.trigger()
795 820
796 821 def guiref_active_frontend(self):
797 822 self.active_frontend.execute("%guiref")
798 823
799 824 def intro_active_frontend(self):
800 825 self.active_frontend.execute("?")
801 826
802 827 def quickref_active_frontend(self):
803 828 self.active_frontend.execute("%quickref")
804 829 #---------------------------------------------------------------------------
805 830 # QWidget interface
806 831 #---------------------------------------------------------------------------
807 832
808 833 def closeEvent(self, event):
809 834 """ Forward the close event to every tabs contained by the windows
810 835 """
811 836 if self.tab_widget.count() == 0:
812 837 # no tabs, just close
813 838 event.accept()
814 839 return
815 840 # Do Not loop on the widget count as it change while closing
816 841 title = self.window().windowTitle()
817 842 cancel = QtGui.QMessageBox.Cancel
818 843 okay = QtGui.QMessageBox.Ok
819 844
820 845 if self.confirm_exit:
821 846 if self.tab_widget.count() > 1:
822 847 msg = "Close all tabs, stop all kernels, and Quit?"
823 848 else:
824 849 msg = "Close console, stop kernel, and Quit?"
825 850 info = "Kernels not started here (e.g. notebooks) will be left alone."
826 851 closeall = QtGui.QPushButton("&Yes, quit everything", self)
827 852 closeall.setShortcut('Y')
828 853 box = QtGui.QMessageBox(QtGui.QMessageBox.Question,
829 854 title, msg)
830 855 box.setInformativeText(info)
831 856 box.addButton(cancel)
832 857 box.addButton(closeall, QtGui.QMessageBox.YesRole)
833 858 box.setDefaultButton(closeall)
834 859 box.setEscapeButton(cancel)
835 860 pixmap = QtGui.QPixmap(self._app.icon.pixmap(QtCore.QSize(64,64)))
836 861 box.setIconPixmap(pixmap)
837 862 reply = box.exec_()
838 863 else:
839 864 reply = okay
840 865
841 866 if reply == cancel:
842 867 event.ignore()
843 868 return
844 869 if reply == okay:
845 870 while self.tab_widget.count() >= 1:
846 871 # prevent further confirmations:
847 872 widget = self.active_frontend
848 873 widget._confirm_exit = False
849 874 self.close_tab(widget)
850 875 event.accept()
851 876
General Comments 0
You need to be logged in to leave comments. Login now