##// END OF EJS Templates
qtconsole : allow copy with shortcut in pager...
Matthias BUSSONNIER -
Show More
@@ -1,1815 +1,1820 b''
1 1 """ An abstract base class for console-type widgets.
2 2 """
3 3 #-----------------------------------------------------------------------------
4 4 # Imports
5 5 #-----------------------------------------------------------------------------
6 6
7 7 # Standard library imports
8 8 import os
9 9 from os.path import commonprefix
10 10 import re
11 11 import sys
12 12 from textwrap import dedent
13 13 from unicodedata import category
14 14
15 15 # System library imports
16 16 from IPython.external.qt import QtCore, QtGui
17 17
18 18 # Local imports
19 19 from IPython.config.configurable import LoggingConfigurable
20 20 from IPython.frontend.qt.rich_text import HtmlExporter
21 21 from IPython.frontend.qt.util import MetaQObjectHasTraits, get_font
22 22 from IPython.utils.text import columnize
23 23 from IPython.utils.traitlets import Bool, Enum, Integer, Unicode
24 24 from ansi_code_processor import QtAnsiCodeProcessor
25 25 from completion_widget import CompletionWidget
26 26 from kill_ring import QtKillRing
27 27
28 28 #-----------------------------------------------------------------------------
29 29 # Functions
30 30 #-----------------------------------------------------------------------------
31 31
32 32 def is_letter_or_number(char):
33 33 """ Returns whether the specified unicode character is a letter or a number.
34 34 """
35 35 cat = category(char)
36 36 return cat.startswith('L') or cat.startswith('N')
37 37
38 38 #-----------------------------------------------------------------------------
39 39 # Classes
40 40 #-----------------------------------------------------------------------------
41 41
42 42 class ConsoleWidget(LoggingConfigurable, QtGui.QWidget):
43 43 """ An abstract base class for console-type widgets. This class has
44 44 functionality for:
45 45
46 46 * Maintaining a prompt and editing region
47 47 * Providing the traditional Unix-style console keyboard shortcuts
48 48 * Performing tab completion
49 49 * Paging text
50 50 * Handling ANSI escape codes
51 51
52 52 ConsoleWidget also provides a number of utility methods that will be
53 53 convenient to implementors of a console-style widget.
54 54 """
55 55 __metaclass__ = MetaQObjectHasTraits
56 56
57 57 #------ Configuration ------------------------------------------------------
58 58
59 59 ansi_codes = Bool(True, config=True,
60 60 help="Whether to process ANSI escape codes."
61 61 )
62 62 buffer_size = Integer(500, config=True,
63 63 help="""
64 64 The maximum number of lines of text before truncation. Specifying a
65 65 non-positive number disables text truncation (not recommended).
66 66 """
67 67 )
68 68 gui_completion = Bool(False, config=True,
69 69 help="""
70 70 Use a list widget instead of plain text output for tab completion.
71 71 """
72 72 )
73 73 # NOTE: this value can only be specified during initialization.
74 74 kind = Enum(['plain', 'rich'], default_value='plain', config=True,
75 75 help="""
76 76 The type of underlying text widget to use. Valid values are 'plain',
77 77 which specifies a QPlainTextEdit, and 'rich', which specifies a
78 78 QTextEdit.
79 79 """
80 80 )
81 81 # NOTE: this value can only be specified during initialization.
82 82 paging = Enum(['inside', 'hsplit', 'vsplit', 'custom', 'none'],
83 83 default_value='inside', config=True,
84 84 help="""
85 85 The type of paging to use. Valid values are:
86 86
87 87 'inside' : The widget pages like a traditional terminal.
88 88 'hsplit' : When paging is requested, the widget is split
89 89 horizontally. The top pane contains the console, and the
90 90 bottom pane contains the paged text.
91 91 'vsplit' : Similar to 'hsplit', except that a vertical splitter
92 92 used.
93 93 'custom' : No action is taken by the widget beyond emitting a
94 94 'custom_page_requested(str)' signal.
95 95 'none' : The text is written directly to the console.
96 96 """)
97 97
98 98 font_family = Unicode(config=True,
99 99 help="""The font family to use for the console.
100 100 On OSX this defaults to Monaco, on Windows the default is
101 101 Consolas with fallback of Courier, and on other platforms
102 102 the default is Monospace.
103 103 """)
104 104 def _font_family_default(self):
105 105 if sys.platform == 'win32':
106 106 # Consolas ships with Vista/Win7, fallback to Courier if needed
107 107 return 'Consolas'
108 108 elif sys.platform == 'darwin':
109 109 # OSX always has Monaco, no need for a fallback
110 110 return 'Monaco'
111 111 else:
112 112 # Monospace should always exist, no need for a fallback
113 113 return 'Monospace'
114 114
115 115 font_size = Integer(config=True,
116 116 help="""The font size. If unconfigured, Qt will be entrusted
117 117 with the size of the font.
118 118 """)
119 119
120 120 # Whether to override ShortcutEvents for the keybindings defined by this
121 121 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
122 122 # priority (when it has focus) over, e.g., window-level menu shortcuts.
123 123 override_shortcuts = Bool(False)
124 124
125 125 #------ Signals ------------------------------------------------------------
126 126
127 127 # Signals that indicate ConsoleWidget state.
128 128 copy_available = QtCore.Signal(bool)
129 129 redo_available = QtCore.Signal(bool)
130 130 undo_available = QtCore.Signal(bool)
131 131
132 132 # Signal emitted when paging is needed and the paging style has been
133 133 # specified as 'custom'.
134 134 custom_page_requested = QtCore.Signal(object)
135 135
136 136 # Signal emitted when the font is changed.
137 137 font_changed = QtCore.Signal(QtGui.QFont)
138 138
139 139 #------ Protected class variables ------------------------------------------
140 140
141 141 # When the control key is down, these keys are mapped.
142 142 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
143 143 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
144 144 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
145 145 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
146 146 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
147 147 QtCore.Qt.Key_H : QtCore.Qt.Key_Backspace,
148 148 QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, }
149 149 if not sys.platform == 'darwin':
150 150 # On OS X, Ctrl-E already does the right thing, whereas End moves the
151 151 # cursor to the bottom of the buffer.
152 152 _ctrl_down_remap[QtCore.Qt.Key_E] = QtCore.Qt.Key_End
153 153
154 154 # The shortcuts defined by this widget. We need to keep track of these to
155 155 # support 'override_shortcuts' above.
156 156 _shortcuts = set(_ctrl_down_remap.keys() +
157 157 [ QtCore.Qt.Key_C, QtCore.Qt.Key_G, QtCore.Qt.Key_O,
158 158 QtCore.Qt.Key_V ])
159 159
160 160 #---------------------------------------------------------------------------
161 161 # 'QObject' interface
162 162 #---------------------------------------------------------------------------
163 163
164 164 def __init__(self, parent=None, **kw):
165 165 """ Create a ConsoleWidget.
166 166
167 167 Parameters:
168 168 -----------
169 169 parent : QWidget, optional [default None]
170 170 The parent for this widget.
171 171 """
172 172 QtGui.QWidget.__init__(self, parent)
173 173 LoggingConfigurable.__init__(self, **kw)
174 174
175 175 # Create the layout and underlying text widget.
176 176 layout = QtGui.QStackedLayout(self)
177 177 layout.setContentsMargins(0, 0, 0, 0)
178 178 self._control = self._create_control()
179 179 self._page_control = None
180 180 self._splitter = None
181 181 if self.paging in ('hsplit', 'vsplit'):
182 182 self._splitter = QtGui.QSplitter()
183 183 if self.paging == 'hsplit':
184 184 self._splitter.setOrientation(QtCore.Qt.Horizontal)
185 185 else:
186 186 self._splitter.setOrientation(QtCore.Qt.Vertical)
187 187 self._splitter.addWidget(self._control)
188 188 layout.addWidget(self._splitter)
189 189 else:
190 190 layout.addWidget(self._control)
191 191
192 192 # Create the paging widget, if necessary.
193 193 if self.paging in ('inside', 'hsplit', 'vsplit'):
194 194 self._page_control = self._create_page_control()
195 195 if self._splitter:
196 196 self._page_control.hide()
197 197 self._splitter.addWidget(self._page_control)
198 198 else:
199 199 layout.addWidget(self._page_control)
200 200
201 201 # Initialize protected variables. Some variables contain useful state
202 202 # information for subclasses; they should be considered read-only.
203 203 self._append_before_prompt_pos = 0
204 204 self._ansi_processor = QtAnsiCodeProcessor()
205 205 self._completion_widget = CompletionWidget(self._control)
206 206 self._continuation_prompt = '> '
207 207 self._continuation_prompt_html = None
208 208 self._executing = False
209 209 self._filter_drag = False
210 210 self._filter_resize = False
211 211 self._html_exporter = HtmlExporter(self._control)
212 212 self._input_buffer_executing = ''
213 213 self._input_buffer_pending = ''
214 214 self._kill_ring = QtKillRing(self._control)
215 215 self._prompt = ''
216 216 self._prompt_html = None
217 217 self._prompt_pos = 0
218 218 self._prompt_sep = ''
219 219 self._reading = False
220 220 self._reading_callback = None
221 221 self._tab_width = 8
222 222 self._text_completing_pos = 0
223 223
224 224 # Set a monospaced font.
225 225 self.reset_font()
226 226
227 227 # Configure actions.
228 228 action = QtGui.QAction('Print', None)
229 229 action.setEnabled(True)
230 230 printkey = QtGui.QKeySequence(QtGui.QKeySequence.Print)
231 231 if printkey.matches("Ctrl+P") and sys.platform != 'darwin':
232 232 # Only override the default if there is a collision.
233 233 # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
234 234 printkey = "Ctrl+Shift+P"
235 235 action.setShortcut(printkey)
236 236 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
237 237 action.triggered.connect(self.print_)
238 238 self.addAction(action)
239 239 self.print_action = action
240 240
241 241 action = QtGui.QAction('Save as HTML/XML', None)
242 242 action.setShortcut(QtGui.QKeySequence.Save)
243 243 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
244 244 action.triggered.connect(self.export_html)
245 245 self.addAction(action)
246 246 self.export_action = action
247 247
248 248 action = QtGui.QAction('Select All', None)
249 249 action.setEnabled(True)
250 250 selectall = QtGui.QKeySequence(QtGui.QKeySequence.SelectAll)
251 251 if selectall.matches("Ctrl+A") and sys.platform != 'darwin':
252 252 # Only override the default if there is a collision.
253 253 # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
254 254 selectall = "Ctrl+Shift+A"
255 255 action.setShortcut(selectall)
256 256 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
257 257 action.triggered.connect(self.select_all)
258 258 self.addAction(action)
259 259 self.select_all_action = action
260 260
261 261 self.increase_font_size = QtGui.QAction("Bigger Font",
262 262 self,
263 263 shortcut=QtGui.QKeySequence.ZoomIn,
264 264 shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut,
265 265 statusTip="Increase the font size by one point",
266 266 triggered=self._increase_font_size)
267 267 self.addAction(self.increase_font_size)
268 268
269 269 self.decrease_font_size = QtGui.QAction("Smaller Font",
270 270 self,
271 271 shortcut=QtGui.QKeySequence.ZoomOut,
272 272 shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut,
273 273 statusTip="Decrease the font size by one point",
274 274 triggered=self._decrease_font_size)
275 275 self.addAction(self.decrease_font_size)
276 276
277 277 self.reset_font_size = QtGui.QAction("Normal Font",
278 278 self,
279 279 shortcut="Ctrl+0",
280 280 shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut,
281 281 statusTip="Restore the Normal font size",
282 282 triggered=self.reset_font)
283 283 self.addAction(self.reset_font_size)
284 284
285 285
286 286
287 287 def eventFilter(self, obj, event):
288 288 """ Reimplemented to ensure a console-like behavior in the underlying
289 289 text widgets.
290 290 """
291 291 etype = event.type()
292 292 if etype == QtCore.QEvent.KeyPress:
293 293
294 294 # Re-map keys for all filtered widgets.
295 295 key = event.key()
296 296 if self._control_key_down(event.modifiers()) and \
297 297 key in self._ctrl_down_remap:
298 298 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
299 299 self._ctrl_down_remap[key],
300 300 QtCore.Qt.NoModifier)
301 301 QtGui.qApp.sendEvent(obj, new_event)
302 302 return True
303 303
304 304 elif obj == self._control:
305 305 return self._event_filter_console_keypress(event)
306 306
307 307 elif obj == self._page_control:
308 308 return self._event_filter_page_keypress(event)
309 309
310 310 # Make middle-click paste safe.
311 311 elif etype == QtCore.QEvent.MouseButtonRelease and \
312 312 event.button() == QtCore.Qt.MidButton and \
313 313 obj == self._control.viewport():
314 314 cursor = self._control.cursorForPosition(event.pos())
315 315 self._control.setTextCursor(cursor)
316 316 self.paste(QtGui.QClipboard.Selection)
317 317 return True
318 318
319 319 # Manually adjust the scrollbars *after* a resize event is dispatched.
320 320 elif etype == QtCore.QEvent.Resize and not self._filter_resize:
321 321 self._filter_resize = True
322 322 QtGui.qApp.sendEvent(obj, event)
323 323 self._adjust_scrollbars()
324 324 self._filter_resize = False
325 325 return True
326 326
327 327 # Override shortcuts for all filtered widgets.
328 328 elif etype == QtCore.QEvent.ShortcutOverride and \
329 329 self.override_shortcuts and \
330 330 self._control_key_down(event.modifiers()) and \
331 331 event.key() in self._shortcuts:
332 332 event.accept()
333 333
334 334 # Ensure that drags are safe. The problem is that the drag starting
335 335 # logic, which determines whether the drag is a Copy or Move, is locked
336 336 # down in QTextControl. If the widget is editable, which it must be if
337 337 # we're not executing, the drag will be a Move. The following hack
338 338 # prevents QTextControl from deleting the text by clearing the selection
339 339 # when a drag leave event originating from this widget is dispatched.
340 340 # The fact that we have to clear the user's selection is unfortunate,
341 341 # but the alternative--trying to prevent Qt from using its hardwired
342 342 # drag logic and writing our own--is worse.
343 343 elif etype == QtCore.QEvent.DragEnter and \
344 344 obj == self._control.viewport() and \
345 345 event.source() == self._control.viewport():
346 346 self._filter_drag = True
347 347 elif etype == QtCore.QEvent.DragLeave and \
348 348 obj == self._control.viewport() and \
349 349 self._filter_drag:
350 350 cursor = self._control.textCursor()
351 351 cursor.clearSelection()
352 352 self._control.setTextCursor(cursor)
353 353 self._filter_drag = False
354 354
355 355 # Ensure that drops are safe.
356 356 elif etype == QtCore.QEvent.Drop and obj == self._control.viewport():
357 357 cursor = self._control.cursorForPosition(event.pos())
358 358 if self._in_buffer(cursor.position()):
359 359 text = event.mimeData().text()
360 360 self._insert_plain_text_into_buffer(cursor, text)
361 361
362 362 # Qt is expecting to get something here--drag and drop occurs in its
363 363 # own event loop. Send a DragLeave event to end it.
364 364 QtGui.qApp.sendEvent(obj, QtGui.QDragLeaveEvent())
365 365 return True
366 366
367 367 return super(ConsoleWidget, self).eventFilter(obj, event)
368 368
369 369 #---------------------------------------------------------------------------
370 370 # 'QWidget' interface
371 371 #---------------------------------------------------------------------------
372 372
373 373 def sizeHint(self):
374 374 """ Reimplemented to suggest a size that is 80 characters wide and
375 375 25 lines high.
376 376 """
377 377 font_metrics = QtGui.QFontMetrics(self.font)
378 378 margin = (self._control.frameWidth() +
379 379 self._control.document().documentMargin()) * 2
380 380 style = self.style()
381 381 splitwidth = style.pixelMetric(QtGui.QStyle.PM_SplitterWidth)
382 382
383 383 # Note 1: Despite my best efforts to take the various margins into
384 384 # account, the width is still coming out a bit too small, so we include
385 385 # a fudge factor of one character here.
386 386 # Note 2: QFontMetrics.maxWidth is not used here or anywhere else due
387 387 # to a Qt bug on certain Mac OS systems where it returns 0.
388 388 width = font_metrics.width(' ') * 81 + margin
389 389 width += style.pixelMetric(QtGui.QStyle.PM_ScrollBarExtent)
390 390 if self.paging == 'hsplit':
391 391 width = width * 2 + splitwidth
392 392
393 393 height = font_metrics.height() * 25 + margin
394 394 if self.paging == 'vsplit':
395 395 height = height * 2 + splitwidth
396 396
397 397 return QtCore.QSize(width, height)
398 398
399 399 #---------------------------------------------------------------------------
400 400 # 'ConsoleWidget' public interface
401 401 #---------------------------------------------------------------------------
402 402
403 403 def can_copy(self):
404 404 """ Returns whether text can be copied to the clipboard.
405 405 """
406 406 return self._control.textCursor().hasSelection()
407 407
408 408 def can_cut(self):
409 409 """ Returns whether text can be cut to the clipboard.
410 410 """
411 411 cursor = self._control.textCursor()
412 412 return (cursor.hasSelection() and
413 413 self._in_buffer(cursor.anchor()) and
414 414 self._in_buffer(cursor.position()))
415 415
416 416 def can_paste(self):
417 417 """ Returns whether text can be pasted from the clipboard.
418 418 """
419 419 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
420 420 return bool(QtGui.QApplication.clipboard().text())
421 421 return False
422 422
423 423 def clear(self, keep_input=True):
424 424 """ Clear the console.
425 425
426 426 Parameters:
427 427 -----------
428 428 keep_input : bool, optional (default True)
429 429 If set, restores the old input buffer if a new prompt is written.
430 430 """
431 431 if self._executing:
432 432 self._control.clear()
433 433 else:
434 434 if keep_input:
435 435 input_buffer = self.input_buffer
436 436 self._control.clear()
437 437 self._show_prompt()
438 438 if keep_input:
439 439 self.input_buffer = input_buffer
440 440
441 441 def copy(self):
442 442 """ Copy the currently selected text to the clipboard.
443 443 """
444 self._control.copy()
444 if self.layout().currentWidget() == self._page_control :
445 self._page_control.copy()
446 elif self.layout().currentWidget() == self._control :
447 self._control.copy()
448 else :
449 self.log.debug("console widget: unknown copy target")
445 450
446 451 def cut(self):
447 452 """ Copy the currently selected text to the clipboard and delete it
448 453 if it's inside the input buffer.
449 454 """
450 455 self.copy()
451 456 if self.can_cut():
452 457 self._control.textCursor().removeSelectedText()
453 458
454 459 def execute(self, source=None, hidden=False, interactive=False):
455 460 """ Executes source or the input buffer, possibly prompting for more
456 461 input.
457 462
458 463 Parameters:
459 464 -----------
460 465 source : str, optional
461 466
462 467 The source to execute. If not specified, the input buffer will be
463 468 used. If specified and 'hidden' is False, the input buffer will be
464 469 replaced with the source before execution.
465 470
466 471 hidden : bool, optional (default False)
467 472
468 473 If set, no output will be shown and the prompt will not be modified.
469 474 In other words, it will be completely invisible to the user that
470 475 an execution has occurred.
471 476
472 477 interactive : bool, optional (default False)
473 478
474 479 Whether the console is to treat the source as having been manually
475 480 entered by the user. The effect of this parameter depends on the
476 481 subclass implementation.
477 482
478 483 Raises:
479 484 -------
480 485 RuntimeError
481 486 If incomplete input is given and 'hidden' is True. In this case,
482 487 it is not possible to prompt for more input.
483 488
484 489 Returns:
485 490 --------
486 491 A boolean indicating whether the source was executed.
487 492 """
488 493 # WARNING: The order in which things happen here is very particular, in
489 494 # large part because our syntax highlighting is fragile. If you change
490 495 # something, test carefully!
491 496
492 497 # Decide what to execute.
493 498 if source is None:
494 499 source = self.input_buffer
495 500 if not hidden:
496 501 # A newline is appended later, but it should be considered part
497 502 # of the input buffer.
498 503 source += '\n'
499 504 elif not hidden:
500 505 self.input_buffer = source
501 506
502 507 # Execute the source or show a continuation prompt if it is incomplete.
503 508 complete = self._is_complete(source, interactive)
504 509 if hidden:
505 510 if complete:
506 511 self._execute(source, hidden)
507 512 else:
508 513 error = 'Incomplete noninteractive input: "%s"'
509 514 raise RuntimeError(error % source)
510 515 else:
511 516 if complete:
512 517 self._append_plain_text('\n')
513 518 self._input_buffer_executing = self.input_buffer
514 519 self._executing = True
515 520 self._prompt_finished()
516 521
517 522 # The maximum block count is only in effect during execution.
518 523 # This ensures that _prompt_pos does not become invalid due to
519 524 # text truncation.
520 525 self._control.document().setMaximumBlockCount(self.buffer_size)
521 526
522 527 # Setting a positive maximum block count will automatically
523 528 # disable the undo/redo history, but just to be safe:
524 529 self._control.setUndoRedoEnabled(False)
525 530
526 531 # Perform actual execution.
527 532 self._execute(source, hidden)
528 533
529 534 else:
530 535 # Do this inside an edit block so continuation prompts are
531 536 # removed seamlessly via undo/redo.
532 537 cursor = self._get_end_cursor()
533 538 cursor.beginEditBlock()
534 539 cursor.insertText('\n')
535 540 self._insert_continuation_prompt(cursor)
536 541 cursor.endEditBlock()
537 542
538 543 # Do not do this inside the edit block. It works as expected
539 544 # when using a QPlainTextEdit control, but does not have an
540 545 # effect when using a QTextEdit. I believe this is a Qt bug.
541 546 self._control.moveCursor(QtGui.QTextCursor.End)
542 547
543 548 return complete
544 549
545 550 def export_html(self):
546 551 """ Shows a dialog to export HTML/XML in various formats.
547 552 """
548 553 self._html_exporter.export()
549 554
550 555 def _get_input_buffer(self, force=False):
551 556 """ The text that the user has entered entered at the current prompt.
552 557
553 558 If the console is currently executing, the text that is executing will
554 559 always be returned.
555 560 """
556 561 # If we're executing, the input buffer may not even exist anymore due to
557 562 # the limit imposed by 'buffer_size'. Therefore, we store it.
558 563 if self._executing and not force:
559 564 return self._input_buffer_executing
560 565
561 566 cursor = self._get_end_cursor()
562 567 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
563 568 input_buffer = cursor.selection().toPlainText()
564 569
565 570 # Strip out continuation prompts.
566 571 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
567 572
568 573 def _set_input_buffer(self, string):
569 574 """ Sets the text in the input buffer.
570 575
571 576 If the console is currently executing, this call has no *immediate*
572 577 effect. When the execution is finished, the input buffer will be updated
573 578 appropriately.
574 579 """
575 580 # If we're executing, store the text for later.
576 581 if self._executing:
577 582 self._input_buffer_pending = string
578 583 return
579 584
580 585 # Remove old text.
581 586 cursor = self._get_end_cursor()
582 587 cursor.beginEditBlock()
583 588 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
584 589 cursor.removeSelectedText()
585 590
586 591 # Insert new text with continuation prompts.
587 592 self._insert_plain_text_into_buffer(self._get_prompt_cursor(), string)
588 593 cursor.endEditBlock()
589 594 self._control.moveCursor(QtGui.QTextCursor.End)
590 595
591 596 input_buffer = property(_get_input_buffer, _set_input_buffer)
592 597
593 598 def _get_font(self):
594 599 """ The base font being used by the ConsoleWidget.
595 600 """
596 601 return self._control.document().defaultFont()
597 602
598 603 def _set_font(self, font):
599 604 """ Sets the base font for the ConsoleWidget to the specified QFont.
600 605 """
601 606 font_metrics = QtGui.QFontMetrics(font)
602 607 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
603 608
604 609 self._completion_widget.setFont(font)
605 610 self._control.document().setDefaultFont(font)
606 611 if self._page_control:
607 612 self._page_control.document().setDefaultFont(font)
608 613
609 614 self.font_changed.emit(font)
610 615
611 616 font = property(_get_font, _set_font)
612 617
613 618 def paste(self, mode=QtGui.QClipboard.Clipboard):
614 619 """ Paste the contents of the clipboard into the input region.
615 620
616 621 Parameters:
617 622 -----------
618 623 mode : QClipboard::Mode, optional [default QClipboard::Clipboard]
619 624
620 625 Controls which part of the system clipboard is used. This can be
621 626 used to access the selection clipboard in X11 and the Find buffer
622 627 in Mac OS. By default, the regular clipboard is used.
623 628 """
624 629 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
625 630 # Make sure the paste is safe.
626 631 self._keep_cursor_in_buffer()
627 632 cursor = self._control.textCursor()
628 633
629 634 # Remove any trailing newline, which confuses the GUI and forces the
630 635 # user to backspace.
631 636 text = QtGui.QApplication.clipboard().text(mode).rstrip()
632 637 self._insert_plain_text_into_buffer(cursor, dedent(text))
633 638
634 639 def print_(self, printer = None):
635 640 """ Print the contents of the ConsoleWidget to the specified QPrinter.
636 641 """
637 642 if (not printer):
638 643 printer = QtGui.QPrinter()
639 644 if(QtGui.QPrintDialog(printer).exec_() != QtGui.QDialog.Accepted):
640 645 return
641 646 self._control.print_(printer)
642 647
643 648 def prompt_to_top(self):
644 649 """ Moves the prompt to the top of the viewport.
645 650 """
646 651 if not self._executing:
647 652 prompt_cursor = self._get_prompt_cursor()
648 653 if self._get_cursor().blockNumber() < prompt_cursor.blockNumber():
649 654 self._set_cursor(prompt_cursor)
650 655 self._set_top_cursor(prompt_cursor)
651 656
652 657 def redo(self):
653 658 """ Redo the last operation. If there is no operation to redo, nothing
654 659 happens.
655 660 """
656 661 self._control.redo()
657 662
658 663 def reset_font(self):
659 664 """ Sets the font to the default fixed-width font for this platform.
660 665 """
661 666 if sys.platform == 'win32':
662 667 # Consolas ships with Vista/Win7, fallback to Courier if needed
663 668 fallback = 'Courier'
664 669 elif sys.platform == 'darwin':
665 670 # OSX always has Monaco
666 671 fallback = 'Monaco'
667 672 else:
668 673 # Monospace should always exist
669 674 fallback = 'Monospace'
670 675 font = get_font(self.font_family, fallback)
671 676 if self.font_size:
672 677 font.setPointSize(self.font_size)
673 678 else:
674 679 font.setPointSize(QtGui.qApp.font().pointSize())
675 680 font.setStyleHint(QtGui.QFont.TypeWriter)
676 681 self._set_font(font)
677 682
678 683 def change_font_size(self, delta):
679 684 """Change the font size by the specified amount (in points).
680 685 """
681 686 font = self.font
682 687 size = max(font.pointSize() + delta, 1) # minimum 1 point
683 688 font.setPointSize(size)
684 689 self._set_font(font)
685 690
686 691 def _increase_font_size(self):
687 692 self.change_font_size(1)
688 693
689 694 def _decrease_font_size(self):
690 695 self.change_font_size(-1)
691 696
692 697 def select_all(self):
693 698 """ Selects all the text in the buffer.
694 699 """
695 700 self._control.selectAll()
696 701
697 702 def _get_tab_width(self):
698 703 """ The width (in terms of space characters) for tab characters.
699 704 """
700 705 return self._tab_width
701 706
702 707 def _set_tab_width(self, tab_width):
703 708 """ Sets the width (in terms of space characters) for tab characters.
704 709 """
705 710 font_metrics = QtGui.QFontMetrics(self.font)
706 711 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
707 712
708 713 self._tab_width = tab_width
709 714
710 715 tab_width = property(_get_tab_width, _set_tab_width)
711 716
712 717 def undo(self):
713 718 """ Undo the last operation. If there is no operation to undo, nothing
714 719 happens.
715 720 """
716 721 self._control.undo()
717 722
718 723 #---------------------------------------------------------------------------
719 724 # 'ConsoleWidget' abstract interface
720 725 #---------------------------------------------------------------------------
721 726
722 727 def _is_complete(self, source, interactive):
723 728 """ Returns whether 'source' can be executed. When triggered by an
724 729 Enter/Return key press, 'interactive' is True; otherwise, it is
725 730 False.
726 731 """
727 732 raise NotImplementedError
728 733
729 734 def _execute(self, source, hidden):
730 735 """ Execute 'source'. If 'hidden', do not show any output.
731 736 """
732 737 raise NotImplementedError
733 738
734 739 def _prompt_started_hook(self):
735 740 """ Called immediately after a new prompt is displayed.
736 741 """
737 742 pass
738 743
739 744 def _prompt_finished_hook(self):
740 745 """ Called immediately after a prompt is finished, i.e. when some input
741 746 will be processed and a new prompt displayed.
742 747 """
743 748 pass
744 749
745 750 def _up_pressed(self, shift_modifier):
746 751 """ Called when the up key is pressed. Returns whether to continue
747 752 processing the event.
748 753 """
749 754 return True
750 755
751 756 def _down_pressed(self, shift_modifier):
752 757 """ Called when the down key is pressed. Returns whether to continue
753 758 processing the event.
754 759 """
755 760 return True
756 761
757 762 def _tab_pressed(self):
758 763 """ Called when the tab key is pressed. Returns whether to continue
759 764 processing the event.
760 765 """
761 766 return False
762 767
763 768 #--------------------------------------------------------------------------
764 769 # 'ConsoleWidget' protected interface
765 770 #--------------------------------------------------------------------------
766 771
767 772 def _append_custom(self, insert, input, before_prompt=False):
768 773 """ A low-level method for appending content to the end of the buffer.
769 774
770 775 If 'before_prompt' is enabled, the content will be inserted before the
771 776 current prompt, if there is one.
772 777 """
773 778 # Determine where to insert the content.
774 779 cursor = self._control.textCursor()
775 780 if before_prompt and not self._executing:
776 781 cursor.setPosition(self._append_before_prompt_pos)
777 782 else:
778 783 cursor.movePosition(QtGui.QTextCursor.End)
779 784 start_pos = cursor.position()
780 785
781 786 # Perform the insertion.
782 787 result = insert(cursor, input)
783 788
784 789 # Adjust the prompt position if we have inserted before it. This is safe
785 790 # because buffer truncation is disabled when not executing.
786 791 if before_prompt and not self._executing:
787 792 diff = cursor.position() - start_pos
788 793 self._append_before_prompt_pos += diff
789 794 self._prompt_pos += diff
790 795
791 796 return result
792 797
793 798 def _append_html(self, html, before_prompt=False):
794 799 """ Appends HTML at the end of the console buffer.
795 800 """
796 801 self._append_custom(self._insert_html, html, before_prompt)
797 802
798 803 def _append_html_fetching_plain_text(self, html, before_prompt=False):
799 804 """ Appends HTML, then returns the plain text version of it.
800 805 """
801 806 return self._append_custom(self._insert_html_fetching_plain_text,
802 807 html, before_prompt)
803 808
804 809 def _append_plain_text(self, text, before_prompt=False):
805 810 """ Appends plain text, processing ANSI codes if enabled.
806 811 """
807 812 self._append_custom(self._insert_plain_text, text, before_prompt)
808 813
809 814 def _cancel_text_completion(self):
810 815 """ If text completion is progress, cancel it.
811 816 """
812 817 if self._text_completing_pos:
813 818 self._clear_temporary_buffer()
814 819 self._text_completing_pos = 0
815 820
816 821 def _clear_temporary_buffer(self):
817 822 """ Clears the "temporary text" buffer, i.e. all the text following
818 823 the prompt region.
819 824 """
820 825 # Select and remove all text below the input buffer.
821 826 cursor = self._get_prompt_cursor()
822 827 prompt = self._continuation_prompt.lstrip()
823 828 while cursor.movePosition(QtGui.QTextCursor.NextBlock):
824 829 temp_cursor = QtGui.QTextCursor(cursor)
825 830 temp_cursor.select(QtGui.QTextCursor.BlockUnderCursor)
826 831 text = temp_cursor.selection().toPlainText().lstrip()
827 832 if not text.startswith(prompt):
828 833 break
829 834 else:
830 835 # We've reached the end of the input buffer and no text follows.
831 836 return
832 837 cursor.movePosition(QtGui.QTextCursor.Left) # Grab the newline.
833 838 cursor.movePosition(QtGui.QTextCursor.End,
834 839 QtGui.QTextCursor.KeepAnchor)
835 840 cursor.removeSelectedText()
836 841
837 842 # After doing this, we have no choice but to clear the undo/redo
838 843 # history. Otherwise, the text is not "temporary" at all, because it
839 844 # can be recalled with undo/redo. Unfortunately, Qt does not expose
840 845 # fine-grained control to the undo/redo system.
841 846 if self._control.isUndoRedoEnabled():
842 847 self._control.setUndoRedoEnabled(False)
843 848 self._control.setUndoRedoEnabled(True)
844 849
845 850 def _complete_with_items(self, cursor, items):
846 851 """ Performs completion with 'items' at the specified cursor location.
847 852 """
848 853 self._cancel_text_completion()
849 854
850 855 if len(items) == 1:
851 856 cursor.setPosition(self._control.textCursor().position(),
852 857 QtGui.QTextCursor.KeepAnchor)
853 858 cursor.insertText(items[0])
854 859
855 860 elif len(items) > 1:
856 861 current_pos = self._control.textCursor().position()
857 862 prefix = commonprefix(items)
858 863 if prefix:
859 864 cursor.setPosition(current_pos, QtGui.QTextCursor.KeepAnchor)
860 865 cursor.insertText(prefix)
861 866 current_pos = cursor.position()
862 867
863 868 if self.gui_completion:
864 869 cursor.movePosition(QtGui.QTextCursor.Left, n=len(prefix))
865 870 self._completion_widget.show_items(cursor, items)
866 871 else:
867 872 cursor.beginEditBlock()
868 873 self._append_plain_text('\n')
869 874 self._page(self._format_as_columns(items))
870 875 cursor.endEditBlock()
871 876
872 877 cursor.setPosition(current_pos)
873 878 self._control.moveCursor(QtGui.QTextCursor.End)
874 879 self._control.setTextCursor(cursor)
875 880 self._text_completing_pos = current_pos
876 881
877 882 def _context_menu_make(self, pos):
878 883 """ Creates a context menu for the given QPoint (in widget coordinates).
879 884 """
880 885 menu = QtGui.QMenu(self)
881 886
882 887 self.cut_action = menu.addAction('Cut', self.cut)
883 888 self.cut_action.setEnabled(self.can_cut())
884 889 self.cut_action.setShortcut(QtGui.QKeySequence.Cut)
885 890
886 891 self.copy_action = menu.addAction('Copy', self.copy)
887 892 self.copy_action.setEnabled(self.can_copy())
888 893 self.copy_action.setShortcut(QtGui.QKeySequence.Copy)
889 894
890 895 self.paste_action = menu.addAction('Paste', self.paste)
891 896 self.paste_action.setEnabled(self.can_paste())
892 897 self.paste_action.setShortcut(QtGui.QKeySequence.Paste)
893 898
894 899 menu.addSeparator()
895 900 menu.addAction(self.select_all_action)
896 901
897 902 menu.addSeparator()
898 903 menu.addAction(self.export_action)
899 904 menu.addAction(self.print_action)
900 905
901 906 return menu
902 907
903 908 def _control_key_down(self, modifiers, include_command=False):
904 909 """ Given a KeyboardModifiers flags object, return whether the Control
905 910 key is down.
906 911
907 912 Parameters:
908 913 -----------
909 914 include_command : bool, optional (default True)
910 915 Whether to treat the Command key as a (mutually exclusive) synonym
911 916 for Control when in Mac OS.
912 917 """
913 918 # Note that on Mac OS, ControlModifier corresponds to the Command key
914 919 # while MetaModifier corresponds to the Control key.
915 920 if sys.platform == 'darwin':
916 921 down = include_command and (modifiers & QtCore.Qt.ControlModifier)
917 922 return bool(down) ^ bool(modifiers & QtCore.Qt.MetaModifier)
918 923 else:
919 924 return bool(modifiers & QtCore.Qt.ControlModifier)
920 925
921 926 def _create_control(self):
922 927 """ Creates and connects the underlying text widget.
923 928 """
924 929 # Create the underlying control.
925 930 if self.kind == 'plain':
926 931 control = QtGui.QPlainTextEdit()
927 932 elif self.kind == 'rich':
928 933 control = QtGui.QTextEdit()
929 934 control.setAcceptRichText(False)
930 935
931 936 # Install event filters. The filter on the viewport is needed for
932 937 # mouse events and drag events.
933 938 control.installEventFilter(self)
934 939 control.viewport().installEventFilter(self)
935 940
936 941 # Connect signals.
937 942 control.cursorPositionChanged.connect(self._cursor_position_changed)
938 943 control.customContextMenuRequested.connect(
939 944 self._custom_context_menu_requested)
940 945 control.copyAvailable.connect(self.copy_available)
941 946 control.redoAvailable.connect(self.redo_available)
942 947 control.undoAvailable.connect(self.undo_available)
943 948
944 949 # Hijack the document size change signal to prevent Qt from adjusting
945 950 # the viewport's scrollbar. We are relying on an implementation detail
946 951 # of Q(Plain)TextEdit here, which is potentially dangerous, but without
947 952 # this functionality we cannot create a nice terminal interface.
948 953 layout = control.document().documentLayout()
949 954 layout.documentSizeChanged.disconnect()
950 955 layout.documentSizeChanged.connect(self._adjust_scrollbars)
951 956
952 957 # Configure the control.
953 958 control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
954 959 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
955 960 control.setReadOnly(True)
956 961 control.setUndoRedoEnabled(False)
957 962 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
958 963 return control
959 964
960 965 def _create_page_control(self):
961 966 """ Creates and connects the underlying paging widget.
962 967 """
963 968 if self.kind == 'plain':
964 969 control = QtGui.QPlainTextEdit()
965 970 elif self.kind == 'rich':
966 971 control = QtGui.QTextEdit()
967 972 control.installEventFilter(self)
968 973 control.setReadOnly(True)
969 974 control.setUndoRedoEnabled(False)
970 975 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
971 976 return control
972 977
973 978 def _event_filter_console_keypress(self, event):
974 979 """ Filter key events for the underlying text widget to create a
975 980 console-like interface.
976 981 """
977 982 intercepted = False
978 983 cursor = self._control.textCursor()
979 984 position = cursor.position()
980 985 key = event.key()
981 986 ctrl_down = self._control_key_down(event.modifiers())
982 987 alt_down = event.modifiers() & QtCore.Qt.AltModifier
983 988 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
984 989
985 990 #------ Special sequences ----------------------------------------------
986 991
987 992 if event.matches(QtGui.QKeySequence.Copy):
988 993 self.copy()
989 994 intercepted = True
990 995
991 996 elif event.matches(QtGui.QKeySequence.Cut):
992 997 self.cut()
993 998 intercepted = True
994 999
995 1000 elif event.matches(QtGui.QKeySequence.Paste):
996 1001 self.paste()
997 1002 intercepted = True
998 1003
999 1004 #------ Special modifier logic -----------------------------------------
1000 1005
1001 1006 elif key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
1002 1007 intercepted = True
1003 1008
1004 1009 # Special handling when tab completing in text mode.
1005 1010 self._cancel_text_completion()
1006 1011
1007 1012 if self._in_buffer(position):
1008 1013 # Special handling when a reading a line of raw input.
1009 1014 if self._reading:
1010 1015 self._append_plain_text('\n')
1011 1016 self._reading = False
1012 1017 if self._reading_callback:
1013 1018 self._reading_callback()
1014 1019
1015 1020 # If the input buffer is a single line or there is only
1016 1021 # whitespace after the cursor, execute. Otherwise, split the
1017 1022 # line with a continuation prompt.
1018 1023 elif not self._executing:
1019 1024 cursor.movePosition(QtGui.QTextCursor.End,
1020 1025 QtGui.QTextCursor.KeepAnchor)
1021 1026 at_end = len(cursor.selectedText().strip()) == 0
1022 1027 single_line = (self._get_end_cursor().blockNumber() ==
1023 1028 self._get_prompt_cursor().blockNumber())
1024 1029 if (at_end or shift_down or single_line) and not ctrl_down:
1025 1030 self.execute(interactive = not shift_down)
1026 1031 else:
1027 1032 # Do this inside an edit block for clean undo/redo.
1028 1033 cursor.beginEditBlock()
1029 1034 cursor.setPosition(position)
1030 1035 cursor.insertText('\n')
1031 1036 self._insert_continuation_prompt(cursor)
1032 1037 cursor.endEditBlock()
1033 1038
1034 1039 # Ensure that the whole input buffer is visible.
1035 1040 # FIXME: This will not be usable if the input buffer is
1036 1041 # taller than the console widget.
1037 1042 self._control.moveCursor(QtGui.QTextCursor.End)
1038 1043 self._control.setTextCursor(cursor)
1039 1044
1040 1045 #------ Control/Cmd modifier -------------------------------------------
1041 1046
1042 1047 elif ctrl_down:
1043 1048 if key == QtCore.Qt.Key_G:
1044 1049 self._keyboard_quit()
1045 1050 intercepted = True
1046 1051
1047 1052 elif key == QtCore.Qt.Key_K:
1048 1053 if self._in_buffer(position):
1049 1054 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
1050 1055 QtGui.QTextCursor.KeepAnchor)
1051 1056 if not cursor.hasSelection():
1052 1057 # Line deletion (remove continuation prompt)
1053 1058 cursor.movePosition(QtGui.QTextCursor.NextBlock,
1054 1059 QtGui.QTextCursor.KeepAnchor)
1055 1060 cursor.movePosition(QtGui.QTextCursor.Right,
1056 1061 QtGui.QTextCursor.KeepAnchor,
1057 1062 len(self._continuation_prompt))
1058 1063 self._kill_ring.kill_cursor(cursor)
1059 1064 intercepted = True
1060 1065
1061 1066 elif key == QtCore.Qt.Key_L:
1062 1067 self.prompt_to_top()
1063 1068 intercepted = True
1064 1069
1065 1070 elif key == QtCore.Qt.Key_O:
1066 1071 if self._page_control and self._page_control.isVisible():
1067 1072 self._page_control.setFocus()
1068 1073 intercepted = True
1069 1074
1070 1075 elif key == QtCore.Qt.Key_U:
1071 1076 if self._in_buffer(position):
1072 1077 start_line = cursor.blockNumber()
1073 1078 if start_line == self._get_prompt_cursor().blockNumber():
1074 1079 offset = len(self._prompt)
1075 1080 else:
1076 1081 offset = len(self._continuation_prompt)
1077 1082 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1078 1083 QtGui.QTextCursor.KeepAnchor)
1079 1084 cursor.movePosition(QtGui.QTextCursor.Right,
1080 1085 QtGui.QTextCursor.KeepAnchor, offset)
1081 1086 self._kill_ring.kill_cursor(cursor)
1082 1087 intercepted = True
1083 1088
1084 1089 elif key == QtCore.Qt.Key_Y:
1085 1090 self._keep_cursor_in_buffer()
1086 1091 self._kill_ring.yank()
1087 1092 intercepted = True
1088 1093
1089 1094 elif key in (QtCore.Qt.Key_Backspace, QtCore.Qt.Key_Delete):
1090 1095 if key == QtCore.Qt.Key_Backspace:
1091 1096 cursor = self._get_word_start_cursor(position)
1092 1097 else: # key == QtCore.Qt.Key_Delete
1093 1098 cursor = self._get_word_end_cursor(position)
1094 1099 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1095 1100 self._kill_ring.kill_cursor(cursor)
1096 1101 intercepted = True
1097 1102
1098 1103 #------ Alt modifier ---------------------------------------------------
1099 1104
1100 1105 elif alt_down:
1101 1106 if key == QtCore.Qt.Key_B:
1102 1107 self._set_cursor(self._get_word_start_cursor(position))
1103 1108 intercepted = True
1104 1109
1105 1110 elif key == QtCore.Qt.Key_F:
1106 1111 self._set_cursor(self._get_word_end_cursor(position))
1107 1112 intercepted = True
1108 1113
1109 1114 elif key == QtCore.Qt.Key_Y:
1110 1115 self._kill_ring.rotate()
1111 1116 intercepted = True
1112 1117
1113 1118 elif key == QtCore.Qt.Key_Backspace:
1114 1119 cursor = self._get_word_start_cursor(position)
1115 1120 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1116 1121 self._kill_ring.kill_cursor(cursor)
1117 1122 intercepted = True
1118 1123
1119 1124 elif key == QtCore.Qt.Key_D:
1120 1125 cursor = self._get_word_end_cursor(position)
1121 1126 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1122 1127 self._kill_ring.kill_cursor(cursor)
1123 1128 intercepted = True
1124 1129
1125 1130 elif key == QtCore.Qt.Key_Delete:
1126 1131 intercepted = True
1127 1132
1128 1133 elif key == QtCore.Qt.Key_Greater:
1129 1134 self._control.moveCursor(QtGui.QTextCursor.End)
1130 1135 intercepted = True
1131 1136
1132 1137 elif key == QtCore.Qt.Key_Less:
1133 1138 self._control.setTextCursor(self._get_prompt_cursor())
1134 1139 intercepted = True
1135 1140
1136 1141 #------ No modifiers ---------------------------------------------------
1137 1142
1138 1143 else:
1139 1144 if shift_down:
1140 1145 anchormode = QtGui.QTextCursor.KeepAnchor
1141 1146 else:
1142 1147 anchormode = QtGui.QTextCursor.MoveAnchor
1143 1148
1144 1149 if key == QtCore.Qt.Key_Escape:
1145 1150 self._keyboard_quit()
1146 1151 intercepted = True
1147 1152
1148 1153 elif key == QtCore.Qt.Key_Up:
1149 1154 if self._reading or not self._up_pressed(shift_down):
1150 1155 intercepted = True
1151 1156 else:
1152 1157 prompt_line = self._get_prompt_cursor().blockNumber()
1153 1158 intercepted = cursor.blockNumber() <= prompt_line
1154 1159
1155 1160 elif key == QtCore.Qt.Key_Down:
1156 1161 if self._reading or not self._down_pressed(shift_down):
1157 1162 intercepted = True
1158 1163 else:
1159 1164 end_line = self._get_end_cursor().blockNumber()
1160 1165 intercepted = cursor.blockNumber() == end_line
1161 1166
1162 1167 elif key == QtCore.Qt.Key_Tab:
1163 1168 if not self._reading:
1164 1169 if self._tab_pressed():
1165 1170 # real tab-key, insert four spaces
1166 1171 cursor.insertText(' '*4)
1167 1172 intercepted = True
1168 1173
1169 1174 elif key == QtCore.Qt.Key_Left:
1170 1175
1171 1176 # Move to the previous line
1172 1177 line, col = cursor.blockNumber(), cursor.columnNumber()
1173 1178 if line > self._get_prompt_cursor().blockNumber() and \
1174 1179 col == len(self._continuation_prompt):
1175 1180 self._control.moveCursor(QtGui.QTextCursor.PreviousBlock,
1176 1181 mode=anchormode)
1177 1182 self._control.moveCursor(QtGui.QTextCursor.EndOfBlock,
1178 1183 mode=anchormode)
1179 1184 intercepted = True
1180 1185
1181 1186 # Regular left movement
1182 1187 else:
1183 1188 intercepted = not self._in_buffer(position - 1)
1184 1189
1185 1190 elif key == QtCore.Qt.Key_Right:
1186 1191 original_block_number = cursor.blockNumber()
1187 1192 cursor.movePosition(QtGui.QTextCursor.Right,
1188 1193 mode=anchormode)
1189 1194 if cursor.blockNumber() != original_block_number:
1190 1195 cursor.movePosition(QtGui.QTextCursor.Right,
1191 1196 n=len(self._continuation_prompt),
1192 1197 mode=anchormode)
1193 1198 self._set_cursor(cursor)
1194 1199 intercepted = True
1195 1200
1196 1201 elif key == QtCore.Qt.Key_Home:
1197 1202 start_line = cursor.blockNumber()
1198 1203 if start_line == self._get_prompt_cursor().blockNumber():
1199 1204 start_pos = self._prompt_pos
1200 1205 else:
1201 1206 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1202 1207 QtGui.QTextCursor.KeepAnchor)
1203 1208 start_pos = cursor.position()
1204 1209 start_pos += len(self._continuation_prompt)
1205 1210 cursor.setPosition(position)
1206 1211 if shift_down and self._in_buffer(position):
1207 1212 cursor.setPosition(start_pos, QtGui.QTextCursor.KeepAnchor)
1208 1213 else:
1209 1214 cursor.setPosition(start_pos)
1210 1215 self._set_cursor(cursor)
1211 1216 intercepted = True
1212 1217
1213 1218 elif key == QtCore.Qt.Key_Backspace:
1214 1219
1215 1220 # Line deletion (remove continuation prompt)
1216 1221 line, col = cursor.blockNumber(), cursor.columnNumber()
1217 1222 if not self._reading and \
1218 1223 col == len(self._continuation_prompt) and \
1219 1224 line > self._get_prompt_cursor().blockNumber():
1220 1225 cursor.beginEditBlock()
1221 1226 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1222 1227 QtGui.QTextCursor.KeepAnchor)
1223 1228 cursor.removeSelectedText()
1224 1229 cursor.deletePreviousChar()
1225 1230 cursor.endEditBlock()
1226 1231 intercepted = True
1227 1232
1228 1233 # Regular backwards deletion
1229 1234 else:
1230 1235 anchor = cursor.anchor()
1231 1236 if anchor == position:
1232 1237 intercepted = not self._in_buffer(position - 1)
1233 1238 else:
1234 1239 intercepted = not self._in_buffer(min(anchor, position))
1235 1240
1236 1241 elif key == QtCore.Qt.Key_Delete:
1237 1242
1238 1243 # Line deletion (remove continuation prompt)
1239 1244 if not self._reading and self._in_buffer(position) and \
1240 1245 cursor.atBlockEnd() and not cursor.hasSelection():
1241 1246 cursor.movePosition(QtGui.QTextCursor.NextBlock,
1242 1247 QtGui.QTextCursor.KeepAnchor)
1243 1248 cursor.movePosition(QtGui.QTextCursor.Right,
1244 1249 QtGui.QTextCursor.KeepAnchor,
1245 1250 len(self._continuation_prompt))
1246 1251 cursor.removeSelectedText()
1247 1252 intercepted = True
1248 1253
1249 1254 # Regular forwards deletion:
1250 1255 else:
1251 1256 anchor = cursor.anchor()
1252 1257 intercepted = (not self._in_buffer(anchor) or
1253 1258 not self._in_buffer(position))
1254 1259
1255 1260 # Don't move the cursor if Control/Cmd is pressed to allow copy-paste
1256 1261 # using the keyboard in any part of the buffer. Also, permit scrolling
1257 1262 # with Page Up/Down keys. Finally, if we're executing, don't move the
1258 1263 # cursor (if even this made sense, we can't guarantee that the prompt
1259 1264 # position is still valid due to text truncation).
1260 1265 if not (self._control_key_down(event.modifiers(), include_command=True)
1261 1266 or key in (QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown)
1262 1267 or (self._executing and not self._reading)):
1263 1268 self._keep_cursor_in_buffer()
1264 1269
1265 1270 return intercepted
1266 1271
1267 1272 def _event_filter_page_keypress(self, event):
1268 1273 """ Filter key events for the paging widget to create console-like
1269 1274 interface.
1270 1275 """
1271 1276 key = event.key()
1272 1277 ctrl_down = self._control_key_down(event.modifiers())
1273 1278 alt_down = event.modifiers() & QtCore.Qt.AltModifier
1274 1279
1275 1280 if ctrl_down:
1276 1281 if key == QtCore.Qt.Key_O:
1277 1282 self._control.setFocus()
1278 1283 intercept = True
1279 1284
1280 1285 elif alt_down:
1281 1286 if key == QtCore.Qt.Key_Greater:
1282 1287 self._page_control.moveCursor(QtGui.QTextCursor.End)
1283 1288 intercepted = True
1284 1289
1285 1290 elif key == QtCore.Qt.Key_Less:
1286 1291 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1287 1292 intercepted = True
1288 1293
1289 1294 elif key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
1290 1295 if self._splitter:
1291 1296 self._page_control.hide()
1292 1297 self._control.setFocus()
1293 1298 else:
1294 1299 self.layout().setCurrentWidget(self._control)
1295 1300 return True
1296 1301
1297 1302 elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return,
1298 1303 QtCore.Qt.Key_Tab):
1299 1304 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1300 1305 QtCore.Qt.Key_PageDown,
1301 1306 QtCore.Qt.NoModifier)
1302 1307 QtGui.qApp.sendEvent(self._page_control, new_event)
1303 1308 return True
1304 1309
1305 1310 elif key == QtCore.Qt.Key_Backspace:
1306 1311 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1307 1312 QtCore.Qt.Key_PageUp,
1308 1313 QtCore.Qt.NoModifier)
1309 1314 QtGui.qApp.sendEvent(self._page_control, new_event)
1310 1315 return True
1311 1316
1312 1317 return False
1313 1318
1314 1319 def _format_as_columns(self, items, separator=' '):
1315 1320 """ Transform a list of strings into a single string with columns.
1316 1321
1317 1322 Parameters
1318 1323 ----------
1319 1324 items : sequence of strings
1320 1325 The strings to process.
1321 1326
1322 1327 separator : str, optional [default is two spaces]
1323 1328 The string that separates columns.
1324 1329
1325 1330 Returns
1326 1331 -------
1327 1332 The formatted string.
1328 1333 """
1329 1334 # Calculate the number of characters available.
1330 1335 width = self._control.viewport().width()
1331 1336 char_width = QtGui.QFontMetrics(self.font).width(' ')
1332 1337 displaywidth = max(10, (width / char_width) - 1)
1333 1338
1334 1339 return columnize(items, separator, displaywidth)
1335 1340
1336 1341 def _get_block_plain_text(self, block):
1337 1342 """ Given a QTextBlock, return its unformatted text.
1338 1343 """
1339 1344 cursor = QtGui.QTextCursor(block)
1340 1345 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1341 1346 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
1342 1347 QtGui.QTextCursor.KeepAnchor)
1343 1348 return cursor.selection().toPlainText()
1344 1349
1345 1350 def _get_cursor(self):
1346 1351 """ Convenience method that returns a cursor for the current position.
1347 1352 """
1348 1353 return self._control.textCursor()
1349 1354
1350 1355 def _get_end_cursor(self):
1351 1356 """ Convenience method that returns a cursor for the last character.
1352 1357 """
1353 1358 cursor = self._control.textCursor()
1354 1359 cursor.movePosition(QtGui.QTextCursor.End)
1355 1360 return cursor
1356 1361
1357 1362 def _get_input_buffer_cursor_column(self):
1358 1363 """ Returns the column of the cursor in the input buffer, excluding the
1359 1364 contribution by the prompt, or -1 if there is no such column.
1360 1365 """
1361 1366 prompt = self._get_input_buffer_cursor_prompt()
1362 1367 if prompt is None:
1363 1368 return -1
1364 1369 else:
1365 1370 cursor = self._control.textCursor()
1366 1371 return cursor.columnNumber() - len(prompt)
1367 1372
1368 1373 def _get_input_buffer_cursor_line(self):
1369 1374 """ Returns the text of the line of the input buffer that contains the
1370 1375 cursor, or None if there is no such line.
1371 1376 """
1372 1377 prompt = self._get_input_buffer_cursor_prompt()
1373 1378 if prompt is None:
1374 1379 return None
1375 1380 else:
1376 1381 cursor = self._control.textCursor()
1377 1382 text = self._get_block_plain_text(cursor.block())
1378 1383 return text[len(prompt):]
1379 1384
1380 1385 def _get_input_buffer_cursor_prompt(self):
1381 1386 """ Returns the (plain text) prompt for line of the input buffer that
1382 1387 contains the cursor, or None if there is no such line.
1383 1388 """
1384 1389 if self._executing:
1385 1390 return None
1386 1391 cursor = self._control.textCursor()
1387 1392 if cursor.position() >= self._prompt_pos:
1388 1393 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
1389 1394 return self._prompt
1390 1395 else:
1391 1396 return self._continuation_prompt
1392 1397 else:
1393 1398 return None
1394 1399
1395 1400 def _get_prompt_cursor(self):
1396 1401 """ Convenience method that returns a cursor for the prompt position.
1397 1402 """
1398 1403 cursor = self._control.textCursor()
1399 1404 cursor.setPosition(self._prompt_pos)
1400 1405 return cursor
1401 1406
1402 1407 def _get_selection_cursor(self, start, end):
1403 1408 """ Convenience method that returns a cursor with text selected between
1404 1409 the positions 'start' and 'end'.
1405 1410 """
1406 1411 cursor = self._control.textCursor()
1407 1412 cursor.setPosition(start)
1408 1413 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
1409 1414 return cursor
1410 1415
1411 1416 def _get_word_start_cursor(self, position):
1412 1417 """ Find the start of the word to the left the given position. If a
1413 1418 sequence of non-word characters precedes the first word, skip over
1414 1419 them. (This emulates the behavior of bash, emacs, etc.)
1415 1420 """
1416 1421 document = self._control.document()
1417 1422 position -= 1
1418 1423 while position >= self._prompt_pos and \
1419 1424 not is_letter_or_number(document.characterAt(position)):
1420 1425 position -= 1
1421 1426 while position >= self._prompt_pos and \
1422 1427 is_letter_or_number(document.characterAt(position)):
1423 1428 position -= 1
1424 1429 cursor = self._control.textCursor()
1425 1430 cursor.setPosition(position + 1)
1426 1431 return cursor
1427 1432
1428 1433 def _get_word_end_cursor(self, position):
1429 1434 """ Find the end of the word to the right the given position. If a
1430 1435 sequence of non-word characters precedes the first word, skip over
1431 1436 them. (This emulates the behavior of bash, emacs, etc.)
1432 1437 """
1433 1438 document = self._control.document()
1434 1439 end = self._get_end_cursor().position()
1435 1440 while position < end and \
1436 1441 not is_letter_or_number(document.characterAt(position)):
1437 1442 position += 1
1438 1443 while position < end and \
1439 1444 is_letter_or_number(document.characterAt(position)):
1440 1445 position += 1
1441 1446 cursor = self._control.textCursor()
1442 1447 cursor.setPosition(position)
1443 1448 return cursor
1444 1449
1445 1450 def _insert_continuation_prompt(self, cursor):
1446 1451 """ Inserts new continuation prompt using the specified cursor.
1447 1452 """
1448 1453 if self._continuation_prompt_html is None:
1449 1454 self._insert_plain_text(cursor, self._continuation_prompt)
1450 1455 else:
1451 1456 self._continuation_prompt = self._insert_html_fetching_plain_text(
1452 1457 cursor, self._continuation_prompt_html)
1453 1458
1454 1459 def _insert_html(self, cursor, html):
1455 1460 """ Inserts HTML using the specified cursor in such a way that future
1456 1461 formatting is unaffected.
1457 1462 """
1458 1463 cursor.beginEditBlock()
1459 1464 cursor.insertHtml(html)
1460 1465
1461 1466 # After inserting HTML, the text document "remembers" it's in "html
1462 1467 # mode", which means that subsequent calls adding plain text will result
1463 1468 # in unwanted formatting, lost tab characters, etc. The following code
1464 1469 # hacks around this behavior, which I consider to be a bug in Qt, by
1465 1470 # (crudely) resetting the document's style state.
1466 1471 cursor.movePosition(QtGui.QTextCursor.Left,
1467 1472 QtGui.QTextCursor.KeepAnchor)
1468 1473 if cursor.selection().toPlainText() == ' ':
1469 1474 cursor.removeSelectedText()
1470 1475 else:
1471 1476 cursor.movePosition(QtGui.QTextCursor.Right)
1472 1477 cursor.insertText(' ', QtGui.QTextCharFormat())
1473 1478 cursor.endEditBlock()
1474 1479
1475 1480 def _insert_html_fetching_plain_text(self, cursor, html):
1476 1481 """ Inserts HTML using the specified cursor, then returns its plain text
1477 1482 version.
1478 1483 """
1479 1484 cursor.beginEditBlock()
1480 1485 cursor.removeSelectedText()
1481 1486
1482 1487 start = cursor.position()
1483 1488 self._insert_html(cursor, html)
1484 1489 end = cursor.position()
1485 1490 cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
1486 1491 text = cursor.selection().toPlainText()
1487 1492
1488 1493 cursor.setPosition(end)
1489 1494 cursor.endEditBlock()
1490 1495 return text
1491 1496
1492 1497 def _insert_plain_text(self, cursor, text):
1493 1498 """ Inserts plain text using the specified cursor, processing ANSI codes
1494 1499 if enabled.
1495 1500 """
1496 1501 cursor.beginEditBlock()
1497 1502 if self.ansi_codes:
1498 1503 for substring in self._ansi_processor.split_string(text):
1499 1504 for act in self._ansi_processor.actions:
1500 1505
1501 1506 # Unlike real terminal emulators, we don't distinguish
1502 1507 # between the screen and the scrollback buffer. A screen
1503 1508 # erase request clears everything.
1504 1509 if act.action == 'erase' and act.area == 'screen':
1505 1510 cursor.select(QtGui.QTextCursor.Document)
1506 1511 cursor.removeSelectedText()
1507 1512
1508 1513 # Simulate a form feed by scrolling just past the last line.
1509 1514 elif act.action == 'scroll' and act.unit == 'page':
1510 1515 cursor.insertText('\n')
1511 1516 cursor.endEditBlock()
1512 1517 self._set_top_cursor(cursor)
1513 1518 cursor.joinPreviousEditBlock()
1514 1519 cursor.deletePreviousChar()
1515 1520
1516 1521 format = self._ansi_processor.get_format()
1517 1522 cursor.insertText(substring, format)
1518 1523 else:
1519 1524 cursor.insertText(text)
1520 1525 cursor.endEditBlock()
1521 1526
1522 1527 def _insert_plain_text_into_buffer(self, cursor, text):
1523 1528 """ Inserts text into the input buffer using the specified cursor (which
1524 1529 must be in the input buffer), ensuring that continuation prompts are
1525 1530 inserted as necessary.
1526 1531 """
1527 1532 lines = text.splitlines(True)
1528 1533 if lines:
1529 1534 cursor.beginEditBlock()
1530 1535 cursor.insertText(lines[0])
1531 1536 for line in lines[1:]:
1532 1537 if self._continuation_prompt_html is None:
1533 1538 cursor.insertText(self._continuation_prompt)
1534 1539 else:
1535 1540 self._continuation_prompt = \
1536 1541 self._insert_html_fetching_plain_text(
1537 1542 cursor, self._continuation_prompt_html)
1538 1543 cursor.insertText(line)
1539 1544 cursor.endEditBlock()
1540 1545
1541 1546 def _in_buffer(self, position=None):
1542 1547 """ Returns whether the current cursor (or, if specified, a position) is
1543 1548 inside the editing region.
1544 1549 """
1545 1550 cursor = self._control.textCursor()
1546 1551 if position is None:
1547 1552 position = cursor.position()
1548 1553 else:
1549 1554 cursor.setPosition(position)
1550 1555 line = cursor.blockNumber()
1551 1556 prompt_line = self._get_prompt_cursor().blockNumber()
1552 1557 if line == prompt_line:
1553 1558 return position >= self._prompt_pos
1554 1559 elif line > prompt_line:
1555 1560 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1556 1561 prompt_pos = cursor.position() + len(self._continuation_prompt)
1557 1562 return position >= prompt_pos
1558 1563 return False
1559 1564
1560 1565 def _keep_cursor_in_buffer(self):
1561 1566 """ Ensures that the cursor is inside the editing region. Returns
1562 1567 whether the cursor was moved.
1563 1568 """
1564 1569 moved = not self._in_buffer()
1565 1570 if moved:
1566 1571 cursor = self._control.textCursor()
1567 1572 cursor.movePosition(QtGui.QTextCursor.End)
1568 1573 self._control.setTextCursor(cursor)
1569 1574 return moved
1570 1575
1571 1576 def _keyboard_quit(self):
1572 1577 """ Cancels the current editing task ala Ctrl-G in Emacs.
1573 1578 """
1574 1579 if self._text_completing_pos:
1575 1580 self._cancel_text_completion()
1576 1581 else:
1577 1582 self.input_buffer = ''
1578 1583
1579 1584 def _page(self, text, html=False):
1580 1585 """ Displays text using the pager if it exceeds the height of the
1581 1586 viewport.
1582 1587
1583 1588 Parameters:
1584 1589 -----------
1585 1590 html : bool, optional (default False)
1586 1591 If set, the text will be interpreted as HTML instead of plain text.
1587 1592 """
1588 1593 line_height = QtGui.QFontMetrics(self.font).height()
1589 1594 minlines = self._control.viewport().height() / line_height
1590 1595 if self.paging != 'none' and \
1591 1596 re.match("(?:[^\n]*\n){%i}" % minlines, text):
1592 1597 if self.paging == 'custom':
1593 1598 self.custom_page_requested.emit(text)
1594 1599 else:
1595 1600 self._page_control.clear()
1596 1601 cursor = self._page_control.textCursor()
1597 1602 if html:
1598 1603 self._insert_html(cursor, text)
1599 1604 else:
1600 1605 self._insert_plain_text(cursor, text)
1601 1606 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1602 1607
1603 1608 self._page_control.viewport().resize(self._control.size())
1604 1609 if self._splitter:
1605 1610 self._page_control.show()
1606 1611 self._page_control.setFocus()
1607 1612 else:
1608 1613 self.layout().setCurrentWidget(self._page_control)
1609 1614 elif html:
1610 1615 self._append_plain_html(text)
1611 1616 else:
1612 1617 self._append_plain_text(text)
1613 1618
1614 1619 def _prompt_finished(self):
1615 1620 """ Called immediately after a prompt is finished, i.e. when some input
1616 1621 will be processed and a new prompt displayed.
1617 1622 """
1618 1623 self._control.setReadOnly(True)
1619 1624 self._prompt_finished_hook()
1620 1625
1621 1626 def _prompt_started(self):
1622 1627 """ Called immediately after a new prompt is displayed.
1623 1628 """
1624 1629 # Temporarily disable the maximum block count to permit undo/redo and
1625 1630 # to ensure that the prompt position does not change due to truncation.
1626 1631 self._control.document().setMaximumBlockCount(0)
1627 1632 self._control.setUndoRedoEnabled(True)
1628 1633
1629 1634 # Work around bug in QPlainTextEdit: input method is not re-enabled
1630 1635 # when read-only is disabled.
1631 1636 self._control.setReadOnly(False)
1632 1637 self._control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
1633 1638
1634 1639 if not self._reading:
1635 1640 self._executing = False
1636 1641 self._prompt_started_hook()
1637 1642
1638 1643 # If the input buffer has changed while executing, load it.
1639 1644 if self._input_buffer_pending:
1640 1645 self.input_buffer = self._input_buffer_pending
1641 1646 self._input_buffer_pending = ''
1642 1647
1643 1648 self._control.moveCursor(QtGui.QTextCursor.End)
1644 1649
1645 1650 def _readline(self, prompt='', callback=None):
1646 1651 """ Reads one line of input from the user.
1647 1652
1648 1653 Parameters
1649 1654 ----------
1650 1655 prompt : str, optional
1651 1656 The prompt to print before reading the line.
1652 1657
1653 1658 callback : callable, optional
1654 1659 A callback to execute with the read line. If not specified, input is
1655 1660 read *synchronously* and this method does not return until it has
1656 1661 been read.
1657 1662
1658 1663 Returns
1659 1664 -------
1660 1665 If a callback is specified, returns nothing. Otherwise, returns the
1661 1666 input string with the trailing newline stripped.
1662 1667 """
1663 1668 if self._reading:
1664 1669 raise RuntimeError('Cannot read a line. Widget is already reading.')
1665 1670
1666 1671 if not callback and not self.isVisible():
1667 1672 # If the user cannot see the widget, this function cannot return.
1668 1673 raise RuntimeError('Cannot synchronously read a line if the widget '
1669 1674 'is not visible!')
1670 1675
1671 1676 self._reading = True
1672 1677 self._show_prompt(prompt, newline=False)
1673 1678
1674 1679 if callback is None:
1675 1680 self._reading_callback = None
1676 1681 while self._reading:
1677 1682 QtCore.QCoreApplication.processEvents()
1678 1683 return self._get_input_buffer(force=True).rstrip('\n')
1679 1684
1680 1685 else:
1681 1686 self._reading_callback = lambda: \
1682 1687 callback(self._get_input_buffer(force=True).rstrip('\n'))
1683 1688
1684 1689 def _set_continuation_prompt(self, prompt, html=False):
1685 1690 """ Sets the continuation prompt.
1686 1691
1687 1692 Parameters
1688 1693 ----------
1689 1694 prompt : str
1690 1695 The prompt to show when more input is needed.
1691 1696
1692 1697 html : bool, optional (default False)
1693 1698 If set, the prompt will be inserted as formatted HTML. Otherwise,
1694 1699 the prompt will be treated as plain text, though ANSI color codes
1695 1700 will be handled.
1696 1701 """
1697 1702 if html:
1698 1703 self._continuation_prompt_html = prompt
1699 1704 else:
1700 1705 self._continuation_prompt = prompt
1701 1706 self._continuation_prompt_html = None
1702 1707
1703 1708 def _set_cursor(self, cursor):
1704 1709 """ Convenience method to set the current cursor.
1705 1710 """
1706 1711 self._control.setTextCursor(cursor)
1707 1712
1708 1713 def _set_top_cursor(self, cursor):
1709 1714 """ Scrolls the viewport so that the specified cursor is at the top.
1710 1715 """
1711 1716 scrollbar = self._control.verticalScrollBar()
1712 1717 scrollbar.setValue(scrollbar.maximum())
1713 1718 original_cursor = self._control.textCursor()
1714 1719 self._control.setTextCursor(cursor)
1715 1720 self._control.ensureCursorVisible()
1716 1721 self._control.setTextCursor(original_cursor)
1717 1722
1718 1723 def _show_prompt(self, prompt=None, html=False, newline=True):
1719 1724 """ Writes a new prompt at the end of the buffer.
1720 1725
1721 1726 Parameters
1722 1727 ----------
1723 1728 prompt : str, optional
1724 1729 The prompt to show. If not specified, the previous prompt is used.
1725 1730
1726 1731 html : bool, optional (default False)
1727 1732 Only relevant when a prompt is specified. If set, the prompt will
1728 1733 be inserted as formatted HTML. Otherwise, the prompt will be treated
1729 1734 as plain text, though ANSI color codes will be handled.
1730 1735
1731 1736 newline : bool, optional (default True)
1732 1737 If set, a new line will be written before showing the prompt if
1733 1738 there is not already a newline at the end of the buffer.
1734 1739 """
1735 1740 # Save the current end position to support _append*(before_prompt=True).
1736 1741 cursor = self._get_end_cursor()
1737 1742 self._append_before_prompt_pos = cursor.position()
1738 1743
1739 1744 # Insert a preliminary newline, if necessary.
1740 1745 if newline and cursor.position() > 0:
1741 1746 cursor.movePosition(QtGui.QTextCursor.Left,
1742 1747 QtGui.QTextCursor.KeepAnchor)
1743 1748 if cursor.selection().toPlainText() != '\n':
1744 1749 self._append_plain_text('\n')
1745 1750
1746 1751 # Write the prompt.
1747 1752 self._append_plain_text(self._prompt_sep)
1748 1753 if prompt is None:
1749 1754 if self._prompt_html is None:
1750 1755 self._append_plain_text(self._prompt)
1751 1756 else:
1752 1757 self._append_html(self._prompt_html)
1753 1758 else:
1754 1759 if html:
1755 1760 self._prompt = self._append_html_fetching_plain_text(prompt)
1756 1761 self._prompt_html = prompt
1757 1762 else:
1758 1763 self._append_plain_text(prompt)
1759 1764 self._prompt = prompt
1760 1765 self._prompt_html = None
1761 1766
1762 1767 self._prompt_pos = self._get_end_cursor().position()
1763 1768 self._prompt_started()
1764 1769
1765 1770 #------ Signal handlers ----------------------------------------------------
1766 1771
1767 1772 def _adjust_scrollbars(self):
1768 1773 """ Expands the vertical scrollbar beyond the range set by Qt.
1769 1774 """
1770 1775 # This code is adapted from _q_adjustScrollbars in qplaintextedit.cpp
1771 1776 # and qtextedit.cpp.
1772 1777 document = self._control.document()
1773 1778 scrollbar = self._control.verticalScrollBar()
1774 1779 viewport_height = self._control.viewport().height()
1775 1780 if isinstance(self._control, QtGui.QPlainTextEdit):
1776 1781 maximum = max(0, document.lineCount() - 1)
1777 1782 step = viewport_height / self._control.fontMetrics().lineSpacing()
1778 1783 else:
1779 1784 # QTextEdit does not do line-based layout and blocks will not in
1780 1785 # general have the same height. Therefore it does not make sense to
1781 1786 # attempt to scroll in line height increments.
1782 1787 maximum = document.size().height()
1783 1788 step = viewport_height
1784 1789 diff = maximum - scrollbar.maximum()
1785 1790 scrollbar.setRange(0, maximum)
1786 1791 scrollbar.setPageStep(step)
1787 1792
1788 1793 # Compensate for undesirable scrolling that occurs automatically due to
1789 1794 # maximumBlockCount() text truncation.
1790 1795 if diff < 0 and document.blockCount() == document.maximumBlockCount():
1791 1796 scrollbar.setValue(scrollbar.value() + diff)
1792 1797
1793 1798 def _cursor_position_changed(self):
1794 1799 """ Clears the temporary buffer based on the cursor position.
1795 1800 """
1796 1801 if self._text_completing_pos:
1797 1802 document = self._control.document()
1798 1803 if self._text_completing_pos < document.characterCount():
1799 1804 cursor = self._control.textCursor()
1800 1805 pos = cursor.position()
1801 1806 text_cursor = self._control.textCursor()
1802 1807 text_cursor.setPosition(self._text_completing_pos)
1803 1808 if pos < self._text_completing_pos or \
1804 1809 cursor.blockNumber() > text_cursor.blockNumber():
1805 1810 self._clear_temporary_buffer()
1806 1811 self._text_completing_pos = 0
1807 1812 else:
1808 1813 self._clear_temporary_buffer()
1809 1814 self._text_completing_pos = 0
1810 1815
1811 1816 def _custom_context_menu_requested(self, pos):
1812 1817 """ Shows a context menu at the given QPoint (in widget coordinates).
1813 1818 """
1814 1819 menu = self._context_menu_make(pos)
1815 1820 menu.exec_(self._control.mapToGlobal(pos))
@@ -1,730 +1,735 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._request_info['execute'] = {};
142 142 self._callback_dict = {}
143 143
144 144 # Configure the ConsoleWidget.
145 145 self.tab_width = 4
146 146 self._set_continuation_prompt('... ')
147 147
148 148 # Configure the CallTipWidget.
149 149 self._call_tip_widget.setFont(self.font)
150 150 self.font_changed.connect(self._call_tip_widget.setFont)
151 151
152 152 # Configure actions.
153 153 action = self._copy_raw_action
154 154 key = QtCore.Qt.CTRL | QtCore.Qt.SHIFT | QtCore.Qt.Key_C
155 155 action.setEnabled(False)
156 156 action.setShortcut(QtGui.QKeySequence(key))
157 157 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
158 158 action.triggered.connect(self.copy_raw)
159 159 self.copy_available.connect(action.setEnabled)
160 160 self.addAction(action)
161 161
162 162 # Connect signal handlers.
163 163 document = self._control.document()
164 164 document.contentsChange.connect(self._document_contents_change)
165 165
166 166 # Set flag for whether we are connected via localhost.
167 167 self._local_kernel = kw.get('local_kernel',
168 168 FrontendWidget._local_kernel)
169 169
170 170 #---------------------------------------------------------------------------
171 171 # 'ConsoleWidget' public interface
172 172 #---------------------------------------------------------------------------
173 173
174 174 def copy(self):
175 175 """ Copy the currently selected text to the clipboard, removing prompts.
176 176 """
177 text = self._control.textCursor().selection().toPlainText()
178 if text:
179 lines = map(transform_classic_prompt, text.splitlines())
180 text = '\n'.join(lines)
181 QtGui.QApplication.clipboard().setText(text)
177 if self.layout().currentWidget() == self._page_control :
178 self._page_control.copy()
179 elif self.layout().currentWidget() == self._control :
180 text = self._control.textCursor().selection().toPlainText()
181 if text:
182 lines = map(transform_classic_prompt, text.splitlines())
183 text = '\n'.join(lines)
184 QtGui.QApplication.clipboard().setText(text)
185 else:
186 self.log.debug("frontend widget : unknown copy target")
182 187
183 188 #---------------------------------------------------------------------------
184 189 # 'ConsoleWidget' abstract interface
185 190 #---------------------------------------------------------------------------
186 191
187 192 def _is_complete(self, source, interactive):
188 193 """ Returns whether 'source' can be completely processed and a new
189 194 prompt created. When triggered by an Enter/Return key press,
190 195 'interactive' is True; otherwise, it is False.
191 196 """
192 197 complete = self._input_splitter.push(source)
193 198 if interactive:
194 199 complete = not self._input_splitter.push_accepts_more()
195 200 return complete
196 201
197 202 def _execute(self, source, hidden):
198 203 """ Execute 'source'. If 'hidden', do not show any output.
199 204
200 205 See parent class :meth:`execute` docstring for full details.
201 206 """
202 207 msg_id = self.kernel_manager.shell_channel.execute(source, hidden)
203 208 self._request_info['execute'][msg_id] = self._ExecutionRequest(msg_id, 'user')
204 209 self._hidden = hidden
205 210 if not hidden:
206 211 self.executing.emit(source)
207 212
208 213 def _prompt_started_hook(self):
209 214 """ Called immediately after a new prompt is displayed.
210 215 """
211 216 if not self._reading:
212 217 self._highlighter.highlighting_on = True
213 218
214 219 def _prompt_finished_hook(self):
215 220 """ Called immediately after a prompt is finished, i.e. when some input
216 221 will be processed and a new prompt displayed.
217 222 """
218 223 # Flush all state from the input splitter so the next round of
219 224 # reading input starts with a clean buffer.
220 225 self._input_splitter.reset()
221 226
222 227 if not self._reading:
223 228 self._highlighter.highlighting_on = False
224 229
225 230 def _tab_pressed(self):
226 231 """ Called when the tab key is pressed. Returns whether to continue
227 232 processing the event.
228 233 """
229 234 # Perform tab completion if:
230 235 # 1) The cursor is in the input buffer.
231 236 # 2) There is a non-whitespace character before the cursor.
232 237 text = self._get_input_buffer_cursor_line()
233 238 if text is None:
234 239 return False
235 240 complete = bool(text[:self._get_input_buffer_cursor_column()].strip())
236 241 if complete:
237 242 self._complete()
238 243 return not complete
239 244
240 245 #---------------------------------------------------------------------------
241 246 # 'ConsoleWidget' protected interface
242 247 #---------------------------------------------------------------------------
243 248
244 249 def _context_menu_make(self, pos):
245 250 """ Reimplemented to add an action for raw copy.
246 251 """
247 252 menu = super(FrontendWidget, self)._context_menu_make(pos)
248 253 for before_action in menu.actions():
249 254 if before_action.shortcut().matches(QtGui.QKeySequence.Paste) == \
250 255 QtGui.QKeySequence.ExactMatch:
251 256 menu.insertAction(before_action, self._copy_raw_action)
252 257 break
253 258 return menu
254 259
255 260 def request_interrupt_kernel(self):
256 261 if self._executing:
257 262 self.interrupt_kernel()
258 263
259 264 def request_restart_kernel(self):
260 265 message = 'Are you sure you want to restart the kernel?'
261 266 self.restart_kernel(message, now=False)
262 267
263 268 def _event_filter_console_keypress(self, event):
264 269 """ Reimplemented for execution interruption and smart backspace.
265 270 """
266 271 key = event.key()
267 272 if self._control_key_down(event.modifiers(), include_command=False):
268 273
269 274 if key == QtCore.Qt.Key_C and self._executing:
270 275 self.request_interrupt_kernel()
271 276 return True
272 277
273 278 elif key == QtCore.Qt.Key_Period:
274 279 self.request_restart_kernel()
275 280 return True
276 281
277 282 elif not event.modifiers() & QtCore.Qt.AltModifier:
278 283
279 284 # Smart backspace: remove four characters in one backspace if:
280 285 # 1) everything left of the cursor is whitespace
281 286 # 2) the four characters immediately left of the cursor are spaces
282 287 if key == QtCore.Qt.Key_Backspace:
283 288 col = self._get_input_buffer_cursor_column()
284 289 cursor = self._control.textCursor()
285 290 if col > 3 and not cursor.hasSelection():
286 291 text = self._get_input_buffer_cursor_line()[:col]
287 292 if text.endswith(' ') and not text.strip():
288 293 cursor.movePosition(QtGui.QTextCursor.Left,
289 294 QtGui.QTextCursor.KeepAnchor, 4)
290 295 cursor.removeSelectedText()
291 296 return True
292 297
293 298 return super(FrontendWidget, self)._event_filter_console_keypress(event)
294 299
295 300 def _insert_continuation_prompt(self, cursor):
296 301 """ Reimplemented for auto-indentation.
297 302 """
298 303 super(FrontendWidget, self)._insert_continuation_prompt(cursor)
299 304 cursor.insertText(' ' * self._input_splitter.indent_spaces)
300 305
301 306 #---------------------------------------------------------------------------
302 307 # 'BaseFrontendMixin' abstract interface
303 308 #---------------------------------------------------------------------------
304 309
305 310 def _handle_complete_reply(self, rep):
306 311 """ Handle replies for tab completion.
307 312 """
308 313 self.log.debug("complete: %s", rep.get('content', ''))
309 314 cursor = self._get_cursor()
310 315 info = self._request_info.get('complete')
311 316 if info and info.id == rep['parent_header']['msg_id'] and \
312 317 info.pos == cursor.position():
313 318 text = '.'.join(self._get_context())
314 319 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
315 320 self._complete_with_items(cursor, rep['content']['matches'])
316 321
317 322 def _silent_exec_callback(self, expr, callback):
318 323 """Silently execute `expr` in the kernel and call `callback` with reply
319 324
320 325 the `expr` is evaluated silently in the kernel (without) output in
321 326 the frontend. Call `callback` with the
322 327 `repr <http://docs.python.org/library/functions.html#repr> `_ as first argument
323 328
324 329 Parameters
325 330 ----------
326 331 expr : string
327 332 valid string to be executed by the kernel.
328 333 callback : function
329 334 function accepting one arguement, as a string. The string will be
330 335 the `repr` of the result of evaluating `expr`
331 336
332 337 The `callback` is called with the 'repr()' of the result of `expr` as
333 338 first argument. To get the object, do 'eval()' onthe passed value.
334 339
335 340 See Also
336 341 --------
337 342 _handle_exec_callback : private method, deal with calling callback with reply
338 343
339 344 """
340 345
341 346 # generate uuid, which would be used as a indication of wether or not
342 347 # the unique request originate from here (can use msg id ?)
343 348 local_uuid = str(uuid.uuid1())
344 349 msg_id = self.kernel_manager.shell_channel.execute('',
345 350 silent=True, user_expressions={ local_uuid:expr })
346 351 self._callback_dict[local_uuid] = callback
347 352 self._request_info['execute'][msg_id] = self._ExecutionRequest(msg_id, 'silent_exec_callback')
348 353
349 354 def _handle_exec_callback(self, msg):
350 355 """Execute `callback` corresonding to `msg` reply, after ``_silent_exec_callback``
351 356
352 357 Parameters
353 358 ----------
354 359 msg : raw message send by the kernel containing an `user_expressions`
355 360 and having a 'silent_exec_callback' kind.
356 361
357 362 Notes
358 363 -----
359 364 This fonction will look for a `callback` associated with the
360 365 corresponding message id. Association has been made by
361 366 `_silent_exec_callback`. `callback` is then called with the `repr()`
362 367 of the value of corresponding `user_expressions` as argument.
363 368 `callback` is then removed from the known list so that any message
364 369 coming again with the same id won't trigger it.
365 370
366 371 """
367 372
368 373 user_exp = msg['content']['user_expressions']
369 374 for expression in user_exp:
370 375 if expression in self._callback_dict:
371 376 self._callback_dict.pop(expression)(user_exp[expression])
372 377
373 378 def _handle_execute_reply(self, msg):
374 379 """ Handles replies for code execution.
375 380 """
376 381 self.log.debug("execute: %s", msg.get('content', ''))
377 382 msg_id = msg['parent_header']['msg_id']
378 383 info = self._request_info['execute'].get(msg_id)
379 384 # unset reading flag, because if execute finished, raw_input can't
380 385 # still be pending.
381 386 self._reading = False
382 387 if info and info.kind == 'user' and not self._hidden:
383 388 # Make sure that all output from the SUB channel has been processed
384 389 # before writing a new prompt.
385 390 self.kernel_manager.sub_channel.flush()
386 391
387 392 # Reset the ANSI style information to prevent bad text in stdout
388 393 # from messing up our colors. We're not a true terminal so we're
389 394 # allowed to do this.
390 395 if self.ansi_codes:
391 396 self._ansi_processor.reset_sgr()
392 397
393 398 content = msg['content']
394 399 status = content['status']
395 400 if status == 'ok':
396 401 self._process_execute_ok(msg)
397 402 elif status == 'error':
398 403 self._process_execute_error(msg)
399 404 elif status == 'aborted':
400 405 self._process_execute_abort(msg)
401 406
402 407 self._show_interpreter_prompt_for_reply(msg)
403 408 self.executed.emit(msg)
404 409 self._request_info['execute'].pop(msg_id)
405 410 elif info and info.kind == 'silent_exec_callback' and not self._hidden:
406 411 self._handle_exec_callback(msg)
407 412 self._request_info['execute'].pop(msg_id)
408 413 else:
409 414 super(FrontendWidget, self)._handle_execute_reply(msg)
410 415
411 416 def _handle_input_request(self, msg):
412 417 """ Handle requests for raw_input.
413 418 """
414 419 self.log.debug("input: %s", msg.get('content', ''))
415 420 if self._hidden:
416 421 raise RuntimeError('Request for raw input during hidden execution.')
417 422
418 423 # Make sure that all output from the SUB channel has been processed
419 424 # before entering readline mode.
420 425 self.kernel_manager.sub_channel.flush()
421 426
422 427 def callback(line):
423 428 self.kernel_manager.stdin_channel.input(line)
424 429 if self._reading:
425 430 self.log.debug("Got second input request, assuming first was interrupted.")
426 431 self._reading = False
427 432 self._readline(msg['content']['prompt'], callback=callback)
428 433
429 434 def _handle_kernel_died(self, since_last_heartbeat):
430 435 """ Handle the kernel's death by asking if the user wants to restart.
431 436 """
432 437 self.log.debug("kernel died: %s", since_last_heartbeat)
433 438 if self.custom_restart:
434 439 self.custom_restart_kernel_died.emit(since_last_heartbeat)
435 440 else:
436 441 message = 'The kernel heartbeat has been inactive for %.2f ' \
437 442 'seconds. Do you want to restart the kernel? You may ' \
438 443 'first want to check the network connection.' % \
439 444 since_last_heartbeat
440 445 self.restart_kernel(message, now=True)
441 446
442 447 def _handle_object_info_reply(self, rep):
443 448 """ Handle replies for call tips.
444 449 """
445 450 self.log.debug("oinfo: %s", rep.get('content', ''))
446 451 cursor = self._get_cursor()
447 452 info = self._request_info.get('call_tip')
448 453 if info and info.id == rep['parent_header']['msg_id'] and \
449 454 info.pos == cursor.position():
450 455 # Get the information for a call tip. For now we format the call
451 456 # line as string, later we can pass False to format_call and
452 457 # syntax-highlight it ourselves for nicer formatting in the
453 458 # calltip.
454 459 content = rep['content']
455 460 # if this is from pykernel, 'docstring' will be the only key
456 461 if content.get('ismagic', False):
457 462 # Don't generate a call-tip for magics. Ideally, we should
458 463 # generate a tooltip, but not on ( like we do for actual
459 464 # callables.
460 465 call_info, doc = None, None
461 466 else:
462 467 call_info, doc = call_tip(content, format_call=True)
463 468 if call_info or doc:
464 469 self._call_tip_widget.show_call_info(call_info, doc)
465 470
466 471 def _handle_pyout(self, msg):
467 472 """ Handle display hook output.
468 473 """
469 474 self.log.debug("pyout: %s", msg.get('content', ''))
470 475 if not self._hidden and self._is_from_this_session(msg):
471 476 text = msg['content']['data']
472 477 self._append_plain_text(text + '\n', before_prompt=True)
473 478
474 479 def _handle_stream(self, msg):
475 480 """ Handle stdout, stderr, and stdin.
476 481 """
477 482 self.log.debug("stream: %s", msg.get('content', ''))
478 483 if not self._hidden and self._is_from_this_session(msg):
479 484 # Most consoles treat tabs as being 8 space characters. Convert tabs
480 485 # to spaces so that output looks as expected regardless of this
481 486 # widget's tab width.
482 487 text = msg['content']['data'].expandtabs(8)
483 488
484 489 self._append_plain_text(text, before_prompt=True)
485 490 self._control.moveCursor(QtGui.QTextCursor.End)
486 491
487 492 def _handle_shutdown_reply(self, msg):
488 493 """ Handle shutdown signal, only if from other console.
489 494 """
490 495 self.log.debug("shutdown: %s", msg.get('content', ''))
491 496 if not self._hidden and not self._is_from_this_session(msg):
492 497 if self._local_kernel:
493 498 if not msg['content']['restart']:
494 499 self.exit_requested.emit(self)
495 500 else:
496 501 # we just got notified of a restart!
497 502 time.sleep(0.25) # wait 1/4 sec to reset
498 503 # lest the request for a new prompt
499 504 # goes to the old kernel
500 505 self.reset()
501 506 else: # remote kernel, prompt on Kernel shutdown/reset
502 507 title = self.window().windowTitle()
503 508 if not msg['content']['restart']:
504 509 reply = QtGui.QMessageBox.question(self, title,
505 510 "Kernel has been shutdown permanently. "
506 511 "Close the Console?",
507 512 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
508 513 if reply == QtGui.QMessageBox.Yes:
509 514 self.exit_requested.emit(self)
510 515 else:
511 516 reply = QtGui.QMessageBox.question(self, title,
512 517 "Kernel has been reset. Clear the Console?",
513 518 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
514 519 if reply == QtGui.QMessageBox.Yes:
515 520 time.sleep(0.25) # wait 1/4 sec to reset
516 521 # lest the request for a new prompt
517 522 # goes to the old kernel
518 523 self.reset()
519 524
520 525 def _started_channels(self):
521 526 """ Called when the KernelManager channels have started listening or
522 527 when the frontend is assigned an already listening KernelManager.
523 528 """
524 529 self.reset()
525 530
526 531 #---------------------------------------------------------------------------
527 532 # 'FrontendWidget' public interface
528 533 #---------------------------------------------------------------------------
529 534
530 535 def copy_raw(self):
531 536 """ Copy the currently selected text to the clipboard without attempting
532 537 to remove prompts or otherwise alter the text.
533 538 """
534 539 self._control.copy()
535 540
536 541 def execute_file(self, path, hidden=False):
537 542 """ Attempts to execute file with 'path'. If 'hidden', no output is
538 543 shown.
539 544 """
540 545 self.execute('execfile(%r)' % path, hidden=hidden)
541 546
542 547 def interrupt_kernel(self):
543 548 """ Attempts to interrupt the running kernel.
544 549
545 550 Also unsets _reading flag, to avoid runtime errors
546 551 if raw_input is called again.
547 552 """
548 553 if self.custom_interrupt:
549 554 self._reading = False
550 555 self.custom_interrupt_requested.emit()
551 556 elif self.kernel_manager.has_kernel:
552 557 self._reading = False
553 558 self.kernel_manager.interrupt_kernel()
554 559 else:
555 560 self._append_plain_text('Kernel process is either remote or '
556 561 'unspecified. Cannot interrupt.\n')
557 562
558 563 def reset(self):
559 564 """ Resets the widget to its initial state. Similar to ``clear``, but
560 565 also re-writes the banner and aborts execution if necessary.
561 566 """
562 567 if self._executing:
563 568 self._executing = False
564 569 self._request_info['execute'] = {}
565 570 self._reading = False
566 571 self._highlighter.highlighting_on = False
567 572
568 573 self._control.clear()
569 574 self._append_plain_text(self.banner)
570 575 # update output marker for stdout/stderr, so that startup
571 576 # messages appear after banner:
572 577 self._append_before_prompt_pos = self._get_cursor().position()
573 578 self._show_interpreter_prompt()
574 579
575 580 def restart_kernel(self, message, now=False):
576 581 """ Attempts to restart the running kernel.
577 582 """
578 583 # FIXME: now should be configurable via a checkbox in the dialog. Right
579 584 # now at least the heartbeat path sets it to True and the manual restart
580 585 # to False. But those should just be the pre-selected states of a
581 586 # checkbox that the user could override if so desired. But I don't know
582 587 # enough Qt to go implementing the checkbox now.
583 588
584 589 if self.custom_restart:
585 590 self.custom_restart_requested.emit()
586 591
587 592 elif self.kernel_manager.has_kernel:
588 593 # Pause the heart beat channel to prevent further warnings.
589 594 self.kernel_manager.hb_channel.pause()
590 595
591 596 # Prompt the user to restart the kernel. Un-pause the heartbeat if
592 597 # they decline. (If they accept, the heartbeat will be un-paused
593 598 # automatically when the kernel is restarted.)
594 599 buttons = QtGui.QMessageBox.Yes | QtGui.QMessageBox.No
595 600 result = QtGui.QMessageBox.question(self, 'Restart kernel?',
596 601 message, buttons)
597 602 if result == QtGui.QMessageBox.Yes:
598 603 try:
599 604 self.kernel_manager.restart_kernel(now=now)
600 605 except RuntimeError:
601 606 self._append_plain_text('Kernel started externally. '
602 607 'Cannot restart.\n')
603 608 else:
604 609 self.reset()
605 610 else:
606 611 self.kernel_manager.hb_channel.unpause()
607 612
608 613 else:
609 614 self._append_plain_text('Kernel process is either remote or '
610 615 'unspecified. Cannot restart.\n')
611 616
612 617 #---------------------------------------------------------------------------
613 618 # 'FrontendWidget' protected interface
614 619 #---------------------------------------------------------------------------
615 620
616 621 def _call_tip(self):
617 622 """ Shows a call tip, if appropriate, at the current cursor location.
618 623 """
619 624 # Decide if it makes sense to show a call tip
620 625 if not self.enable_calltips:
621 626 return False
622 627 cursor = self._get_cursor()
623 628 cursor.movePosition(QtGui.QTextCursor.Left)
624 629 if cursor.document().characterAt(cursor.position()) != '(':
625 630 return False
626 631 context = self._get_context(cursor)
627 632 if not context:
628 633 return False
629 634
630 635 # Send the metadata request to the kernel
631 636 name = '.'.join(context)
632 637 msg_id = self.kernel_manager.shell_channel.object_info(name)
633 638 pos = self._get_cursor().position()
634 639 self._request_info['call_tip'] = self._CallTipRequest(msg_id, pos)
635 640 return True
636 641
637 642 def _complete(self):
638 643 """ Performs completion at the current cursor location.
639 644 """
640 645 context = self._get_context()
641 646 if context:
642 647 # Send the completion request to the kernel
643 648 msg_id = self.kernel_manager.shell_channel.complete(
644 649 '.'.join(context), # text
645 650 self._get_input_buffer_cursor_line(), # line
646 651 self._get_input_buffer_cursor_column(), # cursor_pos
647 652 self.input_buffer) # block
648 653 pos = self._get_cursor().position()
649 654 info = self._CompletionRequest(msg_id, pos)
650 655 self._request_info['complete'] = info
651 656
652 657 def _get_context(self, cursor=None):
653 658 """ Gets the context for the specified cursor (or the current cursor
654 659 if none is specified).
655 660 """
656 661 if cursor is None:
657 662 cursor = self._get_cursor()
658 663 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
659 664 QtGui.QTextCursor.KeepAnchor)
660 665 text = cursor.selection().toPlainText()
661 666 return self._completion_lexer.get_context(text)
662 667
663 668 def _process_execute_abort(self, msg):
664 669 """ Process a reply for an aborted execution request.
665 670 """
666 671 self._append_plain_text("ERROR: execution aborted\n")
667 672
668 673 def _process_execute_error(self, msg):
669 674 """ Process a reply for an execution request that resulted in an error.
670 675 """
671 676 content = msg['content']
672 677 # If a SystemExit is passed along, this means exit() was called - also
673 678 # all the ipython %exit magic syntax of '-k' to be used to keep
674 679 # the kernel running
675 680 if content['ename']=='SystemExit':
676 681 keepkernel = content['evalue']=='-k' or content['evalue']=='True'
677 682 self._keep_kernel_on_exit = keepkernel
678 683 self.exit_requested.emit(self)
679 684 else:
680 685 traceback = ''.join(content['traceback'])
681 686 self._append_plain_text(traceback)
682 687
683 688 def _process_execute_ok(self, msg):
684 689 """ Process a reply for a successful execution equest.
685 690 """
686 691 payload = msg['content']['payload']
687 692 for item in payload:
688 693 if not self._process_execute_payload(item):
689 694 warning = 'Warning: received unknown payload of type %s'
690 695 print(warning % repr(item['source']))
691 696
692 697 def _process_execute_payload(self, item):
693 698 """ Process a single payload item from the list of payload items in an
694 699 execution reply. Returns whether the payload was handled.
695 700 """
696 701 # The basic FrontendWidget doesn't handle payloads, as they are a
697 702 # mechanism for going beyond the standard Python interpreter model.
698 703 return False
699 704
700 705 def _show_interpreter_prompt(self):
701 706 """ Shows a prompt for the interpreter.
702 707 """
703 708 self._show_prompt('>>> ')
704 709
705 710 def _show_interpreter_prompt_for_reply(self, msg):
706 711 """ Shows a prompt for the interpreter given an 'execute_reply' message.
707 712 """
708 713 self._show_interpreter_prompt()
709 714
710 715 #------ Signal handlers ----------------------------------------------------
711 716
712 717 def _document_contents_change(self, position, removed, added):
713 718 """ Called whenever the document's content changes. Display a call tip
714 719 if appropriate.
715 720 """
716 721 # Calculate where the cursor should be *after* the change:
717 722 position += added
718 723
719 724 document = self._control.document()
720 725 if position == self._get_cursor().position():
721 726 self._call_tip()
722 727
723 728 #------ Trait default initializers -----------------------------------------
724 729
725 730 def _banner_default(self):
726 731 """ Returns the standard Python banner.
727 732 """
728 733 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
729 734 '"license" for more information.'
730 735 return banner % (sys.version, sys.platform)
@@ -1,563 +1,569 b''
1 1 """ A FrontendWidget that emulates the interface of the console IPython and
2 2 supports the additional functionality provided by the IPython kernel.
3 3 """
4 4
5 5 #-----------------------------------------------------------------------------
6 6 # Imports
7 7 #-----------------------------------------------------------------------------
8 8
9 9 # Standard library imports
10 10 from collections import namedtuple
11 11 import os.path
12 12 import re
13 13 from subprocess import Popen
14 14 import sys
15 15 import time
16 16 from textwrap import dedent
17 17
18 18 # System library imports
19 19 from IPython.external.qt import QtCore, QtGui
20 20
21 21 # Local imports
22 22 from IPython.core.inputsplitter import IPythonInputSplitter, \
23 23 transform_ipy_prompt
24 24 from IPython.utils.traitlets import Bool, Unicode
25 25 from frontend_widget import FrontendWidget
26 26 import styles
27 27
28 28 #-----------------------------------------------------------------------------
29 29 # Constants
30 30 #-----------------------------------------------------------------------------
31 31
32 32 # Default strings to build and display input and output prompts (and separators
33 33 # in between)
34 34 default_in_prompt = 'In [<span class="in-prompt-number">%i</span>]: '
35 35 default_out_prompt = 'Out[<span class="out-prompt-number">%i</span>]: '
36 36 default_input_sep = '\n'
37 37 default_output_sep = ''
38 38 default_output_sep2 = ''
39 39
40 40 # Base path for most payload sources.
41 41 zmq_shell_source = 'IPython.zmq.zmqshell.ZMQInteractiveShell'
42 42
43 43 if sys.platform.startswith('win'):
44 44 default_editor = 'notepad'
45 45 else:
46 46 default_editor = ''
47 47
48 48 #-----------------------------------------------------------------------------
49 49 # IPythonWidget class
50 50 #-----------------------------------------------------------------------------
51 51
52 52 class IPythonWidget(FrontendWidget):
53 53 """ A FrontendWidget for an IPython kernel.
54 54 """
55 55
56 56 # If set, the 'custom_edit_requested(str, int)' signal will be emitted when
57 57 # an editor is needed for a file. This overrides 'editor' and 'editor_line'
58 58 # settings.
59 59 custom_edit = Bool(False)
60 60 custom_edit_requested = QtCore.Signal(object, object)
61 61
62 62 editor = Unicode(default_editor, config=True,
63 63 help="""
64 64 A command for invoking a system text editor. If the string contains a
65 65 {filename} format specifier, it will be used. Otherwise, the filename
66 66 will be appended to the end the command.
67 67 """)
68 68
69 69 editor_line = Unicode(config=True,
70 70 help="""
71 71 The editor command to use when a specific line number is requested. The
72 72 string should contain two format specifiers: {line} and {filename}. If
73 73 this parameter is not specified, the line number option to the %edit
74 74 magic will be ignored.
75 75 """)
76 76
77 77 style_sheet = Unicode(config=True,
78 78 help="""
79 79 A CSS stylesheet. The stylesheet can contain classes for:
80 80 1. Qt: QPlainTextEdit, QFrame, QWidget, etc
81 81 2. Pygments: .c, .k, .o, etc. (see PygmentsHighlighter)
82 82 3. IPython: .error, .in-prompt, .out-prompt, etc
83 83 """)
84 84
85 85 syntax_style = Unicode(config=True,
86 86 help="""
87 87 If not empty, use this Pygments style for syntax highlighting.
88 88 Otherwise, the style sheet is queried for Pygments style
89 89 information.
90 90 """)
91 91
92 92 # Prompts.
93 93 in_prompt = Unicode(default_in_prompt, config=True)
94 94 out_prompt = Unicode(default_out_prompt, config=True)
95 95 input_sep = Unicode(default_input_sep, config=True)
96 96 output_sep = Unicode(default_output_sep, config=True)
97 97 output_sep2 = Unicode(default_output_sep2, config=True)
98 98
99 99 # FrontendWidget protected class variables.
100 100 _input_splitter_class = IPythonInputSplitter
101 101
102 102 # IPythonWidget protected class variables.
103 103 _PromptBlock = namedtuple('_PromptBlock', ['block', 'length', 'number'])
104 104 _payload_source_edit = zmq_shell_source + '.edit_magic'
105 105 _payload_source_exit = zmq_shell_source + '.ask_exit'
106 106 _payload_source_next_input = zmq_shell_source + '.set_next_input'
107 107 _payload_source_page = 'IPython.zmq.page.page'
108 108 _retrying_history_request = False
109 109
110 110 #---------------------------------------------------------------------------
111 111 # 'object' interface
112 112 #---------------------------------------------------------------------------
113 113
114 114 def __init__(self, *args, **kw):
115 115 super(IPythonWidget, self).__init__(*args, **kw)
116 116
117 117 # IPythonWidget protected variables.
118 118 self._payload_handlers = {
119 119 self._payload_source_edit : self._handle_payload_edit,
120 120 self._payload_source_exit : self._handle_payload_exit,
121 121 self._payload_source_page : self._handle_payload_page,
122 122 self._payload_source_next_input : self._handle_payload_next_input }
123 123 self._previous_prompt_obj = None
124 124 self._keep_kernel_on_exit = None
125 125
126 126 # Initialize widget styling.
127 127 if self.style_sheet:
128 128 self._style_sheet_changed()
129 129 self._syntax_style_changed()
130 130 else:
131 131 self.set_default_style()
132 132
133 133 #---------------------------------------------------------------------------
134 134 # 'BaseFrontendMixin' abstract interface
135 135 #---------------------------------------------------------------------------
136 136
137 137 def _handle_complete_reply(self, rep):
138 138 """ Reimplemented to support IPython's improved completion machinery.
139 139 """
140 140 self.log.debug("complete: %s", rep.get('content', ''))
141 141 cursor = self._get_cursor()
142 142 info = self._request_info.get('complete')
143 143 if info and info.id == rep['parent_header']['msg_id'] and \
144 144 info.pos == cursor.position():
145 145 matches = rep['content']['matches']
146 146 text = rep['content']['matched_text']
147 147 offset = len(text)
148 148
149 149 # Clean up matches with period and path separators if the matched
150 150 # text has not been transformed. This is done by truncating all
151 151 # but the last component and then suitably decreasing the offset
152 152 # between the current cursor position and the start of completion.
153 153 if len(matches) > 1 and matches[0][:offset] == text:
154 154 parts = re.split(r'[./\\]', text)
155 155 sep_count = len(parts) - 1
156 156 if sep_count:
157 157 chop_length = sum(map(len, parts[:sep_count])) + sep_count
158 158 matches = [ match[chop_length:] for match in matches ]
159 159 offset -= chop_length
160 160
161 161 # Move the cursor to the start of the match and complete.
162 162 cursor.movePosition(QtGui.QTextCursor.Left, n=offset)
163 163 self._complete_with_items(cursor, matches)
164 164
165 165 def _handle_execute_reply(self, msg):
166 166 """ Reimplemented to support prompt requests.
167 167 """
168 168 msg_id = msg['parent_header'].get('msg_id')
169 169 info = self._request_info['execute'].get(msg_id)
170 170 if info and info.kind == 'prompt':
171 171 number = msg['content']['execution_count'] + 1
172 172 self._show_interpreter_prompt(number)
173 173 self._request_info['execute'].pop(msg_id)
174 174 else:
175 175 super(IPythonWidget, self)._handle_execute_reply(msg)
176 176
177 177 def _handle_history_reply(self, msg):
178 178 """ Implemented to handle history tail replies, which are only supported
179 179 by the IPython kernel.
180 180 """
181 181 self.log.debug("history: %s", msg.get('content', ''))
182 182 content = msg['content']
183 183 if 'history' not in content:
184 184 self.log.error("History request failed: %r"%content)
185 185 if content.get('status', '') == 'aborted' and \
186 186 not self._retrying_history_request:
187 187 # a *different* action caused this request to be aborted, so
188 188 # we should try again.
189 189 self.log.error("Retrying aborted history request")
190 190 # prevent multiple retries of aborted requests:
191 191 self._retrying_history_request = True
192 192 # wait out the kernel's queue flush, which is currently timed at 0.1s
193 193 time.sleep(0.25)
194 194 self.kernel_manager.shell_channel.history(hist_access_type='tail',n=1000)
195 195 else:
196 196 self._retrying_history_request = False
197 197 return
198 198 # reset retry flag
199 199 self._retrying_history_request = False
200 200 history_items = content['history']
201 201 items = []
202 202 last_cell = u""
203 203 for _, _, cell in history_items:
204 204 cell = cell.rstrip()
205 205 if cell != last_cell:
206 206 items.append(cell)
207 207 last_cell = cell
208 208 self._set_history(items)
209 209
210 210 def _handle_pyout(self, msg):
211 211 """ Reimplemented for IPython-style "display hook".
212 212 """
213 213 self.log.debug("pyout: %s", msg.get('content', ''))
214 214 if not self._hidden and self._is_from_this_session(msg):
215 215 content = msg['content']
216 216 prompt_number = content['execution_count']
217 217 data = content['data']
218 218 if data.has_key('text/html'):
219 219 self._append_plain_text(self.output_sep, True)
220 220 self._append_html(self._make_out_prompt(prompt_number), True)
221 221 html = data['text/html']
222 222 self._append_plain_text('\n', True)
223 223 self._append_html(html + self.output_sep2, True)
224 224 elif data.has_key('text/plain'):
225 225 self._append_plain_text(self.output_sep, True)
226 226 self._append_html(self._make_out_prompt(prompt_number), True)
227 227 text = data['text/plain']
228 228 # If the repr is multiline, make sure we start on a new line,
229 229 # so that its lines are aligned.
230 230 if "\n" in text and not self.output_sep.endswith("\n"):
231 231 self._append_plain_text('\n', True)
232 232 self._append_plain_text(text + self.output_sep2, True)
233 233
234 234 def _handle_display_data(self, msg):
235 235 """ The base handler for the ``display_data`` message.
236 236 """
237 237 self.log.debug("display: %s", msg.get('content', ''))
238 238 # For now, we don't display data from other frontends, but we
239 239 # eventually will as this allows all frontends to monitor the display
240 240 # data. But we need to figure out how to handle this in the GUI.
241 241 if not self._hidden and self._is_from_this_session(msg):
242 242 source = msg['content']['source']
243 243 data = msg['content']['data']
244 244 metadata = msg['content']['metadata']
245 245 # In the regular IPythonWidget, we simply print the plain text
246 246 # representation.
247 247 if data.has_key('text/html'):
248 248 html = data['text/html']
249 249 self._append_html(html, True)
250 250 elif data.has_key('text/plain'):
251 251 text = data['text/plain']
252 252 self._append_plain_text(text, True)
253 253 # This newline seems to be needed for text and html output.
254 254 self._append_plain_text(u'\n', True)
255 255
256 256 def _started_channels(self):
257 257 """ Reimplemented to make a history request.
258 258 """
259 259 super(IPythonWidget, self)._started_channels()
260 260 self.kernel_manager.shell_channel.history(hist_access_type='tail',
261 261 n=1000)
262 262 #---------------------------------------------------------------------------
263 263 # 'ConsoleWidget' public interface
264 264 #---------------------------------------------------------------------------
265 265
266 266 def copy(self):
267 267 """ Copy the currently selected text to the clipboard, removing prompts
268 268 if possible.
269 269 """
270 text = self._control.textCursor().selection().toPlainText()
271 if text:
272 lines = map(transform_ipy_prompt, text.splitlines())
273 text = '\n'.join(lines)
274 QtGui.QApplication.clipboard().setText(text)
270 if self.layout().currentWidget() == self._page_control :
271 self._page_control.copy()
272 elif self.layout().currentWidget() == self._control :
273 text = self._control.textCursor().selection().toPlainText()
274 if text:
275 lines = map(transform_ipy_prompt, text.splitlines())
276 text = '\n'.join(lines)
277 QtGui.QApplication.clipboard().setText(text)
278 else :
279 self.log.debug("ipython_widget : unknown copy taget")
280
275 281
276 282 #---------------------------------------------------------------------------
277 283 # 'FrontendWidget' public interface
278 284 #---------------------------------------------------------------------------
279 285
280 286 def execute_file(self, path, hidden=False):
281 287 """ Reimplemented to use the 'run' magic.
282 288 """
283 289 # Use forward slashes on Windows to avoid escaping each separator.
284 290 if sys.platform == 'win32':
285 291 path = os.path.normpath(path).replace('\\', '/')
286 292
287 293 # Perhaps we should not be using %run directly, but while we
288 294 # are, it is necessary to quote filenames containing spaces or quotes.
289 295 # Escaping quotes in filename in %run seems tricky and inconsistent,
290 296 # so not trying it at present.
291 297 if '"' in path:
292 298 if "'" in path:
293 299 raise ValueError("Can't run filename containing both single "
294 300 "and double quotes: %s" % path)
295 301 path = "'%s'" % path
296 302 elif ' ' in path or "'" in path:
297 303 path = '"%s"' % path
298 304
299 305 self.execute('%%run %s' % path, hidden=hidden)
300 306
301 307 #---------------------------------------------------------------------------
302 308 # 'FrontendWidget' protected interface
303 309 #---------------------------------------------------------------------------
304 310
305 311 def _complete(self):
306 312 """ Reimplemented to support IPython's improved completion machinery.
307 313 """
308 314 # We let the kernel split the input line, so we *always* send an empty
309 315 # text field. Readline-based frontends do get a real text field which
310 316 # they can use.
311 317 text = ''
312 318
313 319 # Send the completion request to the kernel
314 320 msg_id = self.kernel_manager.shell_channel.complete(
315 321 text, # text
316 322 self._get_input_buffer_cursor_line(), # line
317 323 self._get_input_buffer_cursor_column(), # cursor_pos
318 324 self.input_buffer) # block
319 325 pos = self._get_cursor().position()
320 326 info = self._CompletionRequest(msg_id, pos)
321 327 self._request_info['complete'] = info
322 328
323 329 def _process_execute_error(self, msg):
324 330 """ Reimplemented for IPython-style traceback formatting.
325 331 """
326 332 content = msg['content']
327 333 traceback = '\n'.join(content['traceback']) + '\n'
328 334 if False:
329 335 # FIXME: For now, tracebacks come as plain text, so we can't use
330 336 # the html renderer yet. Once we refactor ultratb to produce
331 337 # properly styled tracebacks, this branch should be the default
332 338 traceback = traceback.replace(' ', '&nbsp;')
333 339 traceback = traceback.replace('\n', '<br/>')
334 340
335 341 ename = content['ename']
336 342 ename_styled = '<span class="error">%s</span>' % ename
337 343 traceback = traceback.replace(ename, ename_styled)
338 344
339 345 self._append_html(traceback)
340 346 else:
341 347 # This is the fallback for now, using plain text with ansi escapes
342 348 self._append_plain_text(traceback)
343 349
344 350 def _process_execute_payload(self, item):
345 351 """ Reimplemented to dispatch payloads to handler methods.
346 352 """
347 353 handler = self._payload_handlers.get(item['source'])
348 354 if handler is None:
349 355 # We have no handler for this type of payload, simply ignore it
350 356 return False
351 357 else:
352 358 handler(item)
353 359 return True
354 360
355 361 def _show_interpreter_prompt(self, number=None):
356 362 """ Reimplemented for IPython-style prompts.
357 363 """
358 364 # If a number was not specified, make a prompt number request.
359 365 if number is None:
360 366 msg_id = self.kernel_manager.shell_channel.execute('', silent=True)
361 367 info = self._ExecutionRequest(msg_id, 'prompt')
362 368 self._request_info['execute'][msg_id] = info
363 369 return
364 370
365 371 # Show a new prompt and save information about it so that it can be
366 372 # updated later if the prompt number turns out to be wrong.
367 373 self._prompt_sep = self.input_sep
368 374 self._show_prompt(self._make_in_prompt(number), html=True)
369 375 block = self._control.document().lastBlock()
370 376 length = len(self._prompt)
371 377 self._previous_prompt_obj = self._PromptBlock(block, length, number)
372 378
373 379 # Update continuation prompt to reflect (possibly) new prompt length.
374 380 self._set_continuation_prompt(
375 381 self._make_continuation_prompt(self._prompt), html=True)
376 382
377 383 def _show_interpreter_prompt_for_reply(self, msg):
378 384 """ Reimplemented for IPython-style prompts.
379 385 """
380 386 # Update the old prompt number if necessary.
381 387 content = msg['content']
382 388 # abort replies do not have any keys:
383 389 if content['status'] == 'aborted':
384 390 if self._previous_prompt_obj:
385 391 previous_prompt_number = self._previous_prompt_obj.number
386 392 else:
387 393 previous_prompt_number = 0
388 394 else:
389 395 previous_prompt_number = content['execution_count']
390 396 if self._previous_prompt_obj and \
391 397 self._previous_prompt_obj.number != previous_prompt_number:
392 398 block = self._previous_prompt_obj.block
393 399
394 400 # Make sure the prompt block has not been erased.
395 401 if block.isValid() and block.text():
396 402
397 403 # Remove the old prompt and insert a new prompt.
398 404 cursor = QtGui.QTextCursor(block)
399 405 cursor.movePosition(QtGui.QTextCursor.Right,
400 406 QtGui.QTextCursor.KeepAnchor,
401 407 self._previous_prompt_obj.length)
402 408 prompt = self._make_in_prompt(previous_prompt_number)
403 409 self._prompt = self._insert_html_fetching_plain_text(
404 410 cursor, prompt)
405 411
406 412 # When the HTML is inserted, Qt blows away the syntax
407 413 # highlighting for the line, so we need to rehighlight it.
408 414 self._highlighter.rehighlightBlock(cursor.block())
409 415
410 416 self._previous_prompt_obj = None
411 417
412 418 # Show a new prompt with the kernel's estimated prompt number.
413 419 self._show_interpreter_prompt(previous_prompt_number + 1)
414 420
415 421 #---------------------------------------------------------------------------
416 422 # 'IPythonWidget' interface
417 423 #---------------------------------------------------------------------------
418 424
419 425 def set_default_style(self, colors='lightbg'):
420 426 """ Sets the widget style to the class defaults.
421 427
422 428 Parameters:
423 429 -----------
424 430 colors : str, optional (default lightbg)
425 431 Whether to use the default IPython light background or dark
426 432 background or B&W style.
427 433 """
428 434 colors = colors.lower()
429 435 if colors=='lightbg':
430 436 self.style_sheet = styles.default_light_style_sheet
431 437 self.syntax_style = styles.default_light_syntax_style
432 438 elif colors=='linux':
433 439 self.style_sheet = styles.default_dark_style_sheet
434 440 self.syntax_style = styles.default_dark_syntax_style
435 441 elif colors=='nocolor':
436 442 self.style_sheet = styles.default_bw_style_sheet
437 443 self.syntax_style = styles.default_bw_syntax_style
438 444 else:
439 445 raise KeyError("No such color scheme: %s"%colors)
440 446
441 447 #---------------------------------------------------------------------------
442 448 # 'IPythonWidget' protected interface
443 449 #---------------------------------------------------------------------------
444 450
445 451 def _edit(self, filename, line=None):
446 452 """ Opens a Python script for editing.
447 453
448 454 Parameters:
449 455 -----------
450 456 filename : str
451 457 A path to a local system file.
452 458
453 459 line : int, optional
454 460 A line of interest in the file.
455 461 """
456 462 if self.custom_edit:
457 463 self.custom_edit_requested.emit(filename, line)
458 464 elif not self.editor:
459 465 self._append_plain_text('No default editor available.\n'
460 466 'Specify a GUI text editor in the `IPythonWidget.editor` '
461 467 'configurable to enable the %edit magic')
462 468 else:
463 469 try:
464 470 filename = '"%s"' % filename
465 471 if line and self.editor_line:
466 472 command = self.editor_line.format(filename=filename,
467 473 line=line)
468 474 else:
469 475 try:
470 476 command = self.editor.format()
471 477 except KeyError:
472 478 command = self.editor.format(filename=filename)
473 479 else:
474 480 command += ' ' + filename
475 481 except KeyError:
476 482 self._append_plain_text('Invalid editor command.\n')
477 483 else:
478 484 try:
479 485 Popen(command, shell=True)
480 486 except OSError:
481 487 msg = 'Opening editor with command "%s" failed.\n'
482 488 self._append_plain_text(msg % command)
483 489
484 490 def _make_in_prompt(self, number):
485 491 """ Given a prompt number, returns an HTML In prompt.
486 492 """
487 493 try:
488 494 body = self.in_prompt % number
489 495 except TypeError:
490 496 # allow in_prompt to leave out number, e.g. '>>> '
491 497 body = self.in_prompt
492 498 return '<span class="in-prompt">%s</span>' % body
493 499
494 500 def _make_continuation_prompt(self, prompt):
495 501 """ Given a plain text version of an In prompt, returns an HTML
496 502 continuation prompt.
497 503 """
498 504 end_chars = '...: '
499 505 space_count = len(prompt.lstrip('\n')) - len(end_chars)
500 506 body = '&nbsp;' * space_count + end_chars
501 507 return '<span class="in-prompt">%s</span>' % body
502 508
503 509 def _make_out_prompt(self, number):
504 510 """ Given a prompt number, returns an HTML Out prompt.
505 511 """
506 512 body = self.out_prompt % number
507 513 return '<span class="out-prompt">%s</span>' % body
508 514
509 515 #------ Payload handlers --------------------------------------------------
510 516
511 517 # Payload handlers with a generic interface: each takes the opaque payload
512 518 # dict, unpacks it and calls the underlying functions with the necessary
513 519 # arguments.
514 520
515 521 def _handle_payload_edit(self, item):
516 522 self._edit(item['filename'], item['line_number'])
517 523
518 524 def _handle_payload_exit(self, item):
519 525 self._keep_kernel_on_exit = item['keepkernel']
520 526 self.exit_requested.emit(self)
521 527
522 528 def _handle_payload_next_input(self, item):
523 529 self.input_buffer = dedent(item['text'].rstrip())
524 530
525 531 def _handle_payload_page(self, item):
526 532 # Since the plain text widget supports only a very small subset of HTML
527 533 # and we have no control over the HTML source, we only page HTML
528 534 # payloads in the rich text widget.
529 535 if item['html'] and self.kind == 'rich':
530 536 self._page(item['html'], html=True)
531 537 else:
532 538 self._page(item['text'], html=False)
533 539
534 540 #------ Trait change handlers --------------------------------------------
535 541
536 542 def _style_sheet_changed(self):
537 543 """ Set the style sheets of the underlying widgets.
538 544 """
539 545 self.setStyleSheet(self.style_sheet)
540 546 self._control.document().setDefaultStyleSheet(self.style_sheet)
541 547 if self._page_control:
542 548 self._page_control.document().setDefaultStyleSheet(self.style_sheet)
543 549
544 550 bg_color = self._control.palette().window().color()
545 551 self._ansi_processor.set_background_color(bg_color)
546 552
547 553
548 554 def _syntax_style_changed(self):
549 555 """ Set the style for the syntax highlighter.
550 556 """
551 557 if self._highlighter is None:
552 558 # ignore premature calls
553 559 return
554 560 if self.syntax_style:
555 561 self._highlighter.set_style(self.syntax_style)
556 562 else:
557 563 self._highlighter.set_style_sheet(self.style_sheet)
558 564
559 565 #------ Trait default initializers -----------------------------------------
560 566
561 567 def _banner_default(self):
562 568 from IPython.core.usage import default_gui_banner
563 569 return default_gui_banner
@@ -1,909 +1,908 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 re
24 24 import webbrowser
25 25 from threading import Thread
26 26
27 27 # System library imports
28 28 from IPython.external.qt import QtGui,QtCore
29 29
30 30 def background(f):
31 31 """call a function in a simple thread, to prevent blocking"""
32 32 t = Thread(target=f)
33 33 t.start()
34 34 return t
35 35
36 36 #-----------------------------------------------------------------------------
37 37 # Classes
38 38 #-----------------------------------------------------------------------------
39 39
40 40 class MainWindow(QtGui.QMainWindow):
41 41
42 42 #---------------------------------------------------------------------------
43 43 # 'object' interface
44 44 #---------------------------------------------------------------------------
45 45
46 46 def __init__(self, app,
47 47 confirm_exit=True,
48 48 new_frontend_factory=None, slave_frontend_factory=None,
49 49 ):
50 50 """ Create a tabbed MainWindow for managing IPython FrontendWidgets
51 51
52 52 Parameters
53 53 ----------
54 54
55 55 app : reference to QApplication parent
56 56 confirm_exit : bool, optional
57 57 Whether we should prompt on close of tabs
58 58 new_frontend_factory : callable
59 59 A callable that returns a new IPythonWidget instance, attached to
60 60 its own running kernel.
61 61 slave_frontend_factory : callable
62 62 A callable that takes an existing IPythonWidget, and returns a new
63 63 IPythonWidget instance, attached to the same kernel.
64 64 """
65 65
66 66 super(MainWindow, self).__init__()
67 67 self._kernel_counter = 0
68 68 self._app = app
69 69 self.confirm_exit = confirm_exit
70 70 self.new_frontend_factory = new_frontend_factory
71 71 self.slave_frontend_factory = slave_frontend_factory
72 72
73 73 self.tab_widget = QtGui.QTabWidget(self)
74 74 self.tab_widget.setDocumentMode(True)
75 75 self.tab_widget.setTabsClosable(True)
76 76 self.tab_widget.tabCloseRequested[int].connect(self.close_tab)
77 77
78 78 self.setCentralWidget(self.tab_widget)
79 79 # hide tab bar at first, since we have no tabs:
80 80 self.tab_widget.tabBar().setVisible(False)
81 81 # prevent focus in tab bar
82 82 self.tab_widget.setFocusPolicy(QtCore.Qt.NoFocus)
83 83
84 84 def update_tab_bar_visibility(self):
85 85 """ update visibility of the tabBar depending of the number of tab
86 86
87 87 0 or 1 tab, tabBar hidden
88 88 2+ tabs, tabBar visible
89 89
90 90 send a self.close if number of tab ==0
91 91
92 92 need to be called explicitely, or be connected to tabInserted/tabRemoved
93 93 """
94 94 if self.tab_widget.count() <= 1:
95 95 self.tab_widget.tabBar().setVisible(False)
96 96 else:
97 97 self.tab_widget.tabBar().setVisible(True)
98 98 if self.tab_widget.count()==0 :
99 99 self.close()
100 100
101 101 @property
102 102 def next_kernel_id(self):
103 103 """constantly increasing counter for kernel IDs"""
104 104 c = self._kernel_counter
105 105 self._kernel_counter += 1
106 106 return c
107 107
108 108 @property
109 109 def active_frontend(self):
110 110 return self.tab_widget.currentWidget()
111 111
112 112 def create_tab_with_new_frontend(self):
113 113 """create a new frontend and attach it to a new tab"""
114 114 widget = self.new_frontend_factory()
115 115 self.add_tab_with_frontend(widget)
116 116
117 117 def create_tab_with_current_kernel(self):
118 118 """create a new frontend attached to the same kernel as the current tab"""
119 119 current_widget = self.tab_widget.currentWidget()
120 120 current_widget_index = self.tab_widget.indexOf(current_widget)
121 121 current_widget_name = self.tab_widget.tabText(current_widget_index)
122 122 widget = self.slave_frontend_factory(current_widget)
123 123 if 'slave' in current_widget_name:
124 124 # don't keep stacking slaves
125 125 name = current_widget_name
126 126 else:
127 127 name = '(%s) slave' % current_widget_name
128 128 self.add_tab_with_frontend(widget,name=name)
129 129
130 130 def close_tab(self,current_tab):
131 131 """ Called when you need to try to close a tab.
132 132
133 133 It takes the number of the tab to be closed as argument, or a referece
134 134 to the wiget insite this tab
135 135 """
136 136
137 137 # let's be sure "tab" and "closing widget are respectivey the index of the tab to close
138 138 # and a reference to the trontend to close
139 139 if type(current_tab) is not int :
140 140 current_tab = self.tab_widget.indexOf(current_tab)
141 141 closing_widget=self.tab_widget.widget(current_tab)
142 142
143 143
144 144 # when trying to be closed, widget might re-send a request to be closed again, but will
145 145 # be deleted when event will be processed. So need to check that widget still exist and
146 146 # skip if not. One example of this is when 'exit' is send in a slave tab. 'exit' will be
147 147 # re-send by this fonction on the master widget, which ask all slaves widget to exit
148 148 if closing_widget==None:
149 149 return
150 150
151 151 #get a list of all slave widgets on the same kernel.
152 152 slave_tabs = self.find_slave_widgets(closing_widget)
153 153
154 154 keepkernel = None #Use the prompt by default
155 155 if hasattr(closing_widget,'_keep_kernel_on_exit'): #set by exit magic
156 156 keepkernel = closing_widget._keep_kernel_on_exit
157 157 # If signal sent by exit magic (_keep_kernel_on_exit, exist and not None)
158 158 # we set local slave tabs._hidden to True to avoid prompting for kernel
159 159 # restart when they get the signal. and then "forward" the 'exit'
160 160 # to the main window
161 161 if keepkernel is not None:
162 162 for tab in slave_tabs:
163 163 tab._hidden = True
164 164 if closing_widget in slave_tabs:
165 165 try :
166 166 self.find_master_tab(closing_widget).execute('exit')
167 167 except AttributeError:
168 168 self.log.info("Master already closed or not local, closing only current tab")
169 169 self.tab_widget.removeTab(current_tab)
170 170 self.update_tab_bar_visibility()
171 171 return
172 172
173 173 kernel_manager = closing_widget.kernel_manager
174 174
175 175 if keepkernel is None and not closing_widget._confirm_exit:
176 176 # don't prompt, just terminate the kernel if we own it
177 177 # or leave it alone if we don't
178 178 keepkernel = closing_widget._existing
179 179 if keepkernel is None: #show prompt
180 180 if kernel_manager and kernel_manager.channels_running:
181 181 title = self.window().windowTitle()
182 182 cancel = QtGui.QMessageBox.Cancel
183 183 okay = QtGui.QMessageBox.Ok
184 184 if closing_widget._may_close:
185 185 msg = "You are closing the tab : "+'"'+self.tab_widget.tabText(current_tab)+'"'
186 186 info = "Would you like to quit the Kernel and close all attached Consoles as well?"
187 187 justthis = QtGui.QPushButton("&No, just this Tab", self)
188 188 justthis.setShortcut('N')
189 189 closeall = QtGui.QPushButton("&Yes, close all", self)
190 190 closeall.setShortcut('Y')
191 191 box = QtGui.QMessageBox(QtGui.QMessageBox.Question,
192 192 title, msg)
193 193 box.setInformativeText(info)
194 194 box.addButton(cancel)
195 195 box.addButton(justthis, QtGui.QMessageBox.NoRole)
196 196 box.addButton(closeall, QtGui.QMessageBox.YesRole)
197 197 box.setDefaultButton(closeall)
198 198 box.setEscapeButton(cancel)
199 199 pixmap = QtGui.QPixmap(self._app.icon.pixmap(QtCore.QSize(64,64)))
200 200 box.setIconPixmap(pixmap)
201 201 reply = box.exec_()
202 202 if reply == 1: # close All
203 203 for slave in slave_tabs:
204 204 background(slave.kernel_manager.stop_channels)
205 205 self.tab_widget.removeTab(self.tab_widget.indexOf(slave))
206 206 closing_widget.execute("exit")
207 207 self.tab_widget.removeTab(current_tab)
208 208 background(kernel_manager.stop_channels)
209 209 elif reply == 0: # close Console
210 210 if not closing_widget._existing:
211 211 # Have kernel: don't quit, just close the tab
212 212 closing_widget.execute("exit True")
213 213 self.tab_widget.removeTab(current_tab)
214 214 background(kernel_manager.stop_channels)
215 215 else:
216 216 reply = QtGui.QMessageBox.question(self, title,
217 217 "Are you sure you want to close this Console?"+
218 218 "\nThe Kernel and other Consoles will remain active.",
219 219 okay|cancel,
220 220 defaultButton=okay
221 221 )
222 222 if reply == okay:
223 223 self.tab_widget.removeTab(current_tab)
224 224 elif keepkernel: #close console but leave kernel running (no prompt)
225 225 self.tab_widget.removeTab(current_tab)
226 226 background(kernel_manager.stop_channels)
227 227 else: #close console and kernel (no prompt)
228 228 self.tab_widget.removeTab(current_tab)
229 229 if kernel_manager and kernel_manager.channels_running:
230 230 for slave in slave_tabs:
231 231 background(slave.kernel_manager.stop_channels)
232 232 self.tab_widget.removeTab(self.tab_widget.indexOf(slave))
233 233 kernel_manager.shutdown_kernel()
234 234 background(kernel_manager.stop_channels)
235 235
236 236 self.update_tab_bar_visibility()
237 237
238 238 def add_tab_with_frontend(self,frontend,name=None):
239 239 """ insert a tab with a given frontend in the tab bar, and give it a name
240 240
241 241 """
242 242 if not name:
243 243 name = 'kernel %i' % self.next_kernel_id
244 244 self.tab_widget.addTab(frontend,name)
245 245 self.update_tab_bar_visibility()
246 246 self.make_frontend_visible(frontend)
247 247 frontend.exit_requested.connect(self.close_tab)
248 248
249 249 def next_tab(self):
250 250 self.tab_widget.setCurrentIndex((self.tab_widget.currentIndex()+1))
251 251
252 252 def prev_tab(self):
253 253 self.tab_widget.setCurrentIndex((self.tab_widget.currentIndex()-1))
254 254
255 255 def make_frontend_visible(self,frontend):
256 256 widget_index=self.tab_widget.indexOf(frontend)
257 257 if widget_index > 0 :
258 258 self.tab_widget.setCurrentIndex(widget_index)
259 259
260 260 def find_master_tab(self,tab,as_list=False):
261 261 """
262 262 Try to return the frontend that own the kernel attached to the given widget/tab.
263 263
264 264 Only find frontend owed by the current application. Selection
265 265 based on port of the kernel, might be inacurate if several kernel
266 266 on different ip use same port number.
267 267
268 268 This fonction does the conversion tabNumber/widget if needed.
269 269 Might return None if no master widget (non local kernel)
270 270 Will crash IPython if more than 1 masterWidget
271 271
272 272 When asList set to True, always return a list of widget(s) owning
273 273 the kernel. The list might be empty or containing several Widget.
274 274 """
275 275
276 276 #convert from/to int/richIpythonWidget if needed
277 277 if isinstance(tab, int):
278 278 tab = self.tab_widget.widget(tab)
279 279 km=tab.kernel_manager
280 280
281 281 #build list of all widgets
282 282 widget_list = [self.tab_widget.widget(i) for i in range(self.tab_widget.count())]
283 283
284 284 # widget that are candidate to be the owner of the kernel does have all the same port of the curent widget
285 285 # And should have a _may_close attribute
286 286 filtered_widget_list = [ widget for widget in widget_list if
287 287 widget.kernel_manager.connection_file == km.connection_file and
288 288 hasattr(widget,'_may_close') ]
289 289 # the master widget is the one that may close the kernel
290 290 master_widget= [ widget for widget in filtered_widget_list if widget._may_close]
291 291 if as_list:
292 292 return master_widget
293 293 assert(len(master_widget)<=1 )
294 294 if len(master_widget)==0:
295 295 return None
296 296
297 297 return master_widget[0]
298 298
299 299 def find_slave_widgets(self,tab):
300 300 """return all the frontends that do not own the kernel attached to the given widget/tab.
301 301
302 302 Only find frontends owned by the current application. Selection
303 303 based on connection file of the kernel.
304 304
305 305 This function does the conversion tabNumber/widget if needed.
306 306 """
307 307 #convert from/to int/richIpythonWidget if needed
308 308 if isinstance(tab, int):
309 309 tab = self.tab_widget.widget(tab)
310 310 km=tab.kernel_manager
311 311
312 312 #build list of all widgets
313 313 widget_list = [self.tab_widget.widget(i) for i in range(self.tab_widget.count())]
314 314
315 315 # widget that are candidate not to be the owner of the kernel does have all the same port of the curent widget
316 316 filtered_widget_list = ( widget for widget in widget_list if
317 317 widget.kernel_manager.connection_file == km.connection_file)
318 318 # Get a list of all widget owning the same kernel and removed it from
319 319 # the previous cadidate. (better using sets ?)
320 320 master_widget_list = self.find_master_tab(tab, as_list=True)
321 321 slave_list = [widget for widget in filtered_widget_list if widget not in master_widget_list]
322 322
323 323 return slave_list
324 324
325 325 # Populate the menu bar with common actions and shortcuts
326 326 def add_menu_action(self, menu, action, defer_shortcut=False):
327 327 """Add action to menu as well as self
328 328
329 329 So that when the menu bar is invisible, its actions are still available.
330 330
331 331 If defer_shortcut is True, set the shortcut context to widget-only,
332 332 where it will avoid conflict with shortcuts already bound to the
333 333 widgets themselves.
334 334 """
335 335 menu.addAction(action)
336 336 self.addAction(action)
337 337
338 338 if defer_shortcut:
339 339 action.setShortcutContext(QtCore.Qt.WidgetShortcut)
340 340
341 341 def init_menu_bar(self):
342 342 #create menu in the order they should appear in the menu bar
343 343 self.init_file_menu()
344 344 self.init_edit_menu()
345 345 self.init_view_menu()
346 346 self.init_kernel_menu()
347 347 self.init_magic_menu()
348 348 self.init_window_menu()
349 349 self.init_help_menu()
350 350
351 351 def init_file_menu(self):
352 352 self.file_menu = self.menuBar().addMenu("&File")
353 353
354 354 self.new_kernel_tab_act = QtGui.QAction("New Tab with &New kernel",
355 355 self,
356 356 shortcut="Ctrl+T",
357 357 triggered=self.create_tab_with_new_frontend)
358 358 self.add_menu_action(self.file_menu, self.new_kernel_tab_act)
359 359
360 360 self.slave_kernel_tab_act = QtGui.QAction("New Tab with Sa&me kernel",
361 361 self,
362 362 shortcut="Ctrl+Shift+T",
363 363 triggered=self.create_tab_with_current_kernel)
364 364 self.add_menu_action(self.file_menu, self.slave_kernel_tab_act)
365 365
366 366 self.file_menu.addSeparator()
367 367
368 368 self.close_action=QtGui.QAction("&Close Tab",
369 369 self,
370 370 shortcut=QtGui.QKeySequence.Close,
371 371 triggered=self.close_active_frontend
372 372 )
373 373 self.add_menu_action(self.file_menu, self.close_action)
374 374
375 375 self.export_action=QtGui.QAction("&Save to HTML/XHTML",
376 376 self,
377 377 shortcut=QtGui.QKeySequence.Save,
378 378 triggered=self.export_action_active_frontend
379 379 )
380 380 self.add_menu_action(self.file_menu, self.export_action, True)
381 381
382 382 self.file_menu.addSeparator()
383 383
384 384 printkey = QtGui.QKeySequence(QtGui.QKeySequence.Print)
385 385 if printkey.matches("Ctrl+P") and sys.platform != 'darwin':
386 386 # Only override the default if there is a collision.
387 387 # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
388 388 printkey = "Ctrl+Shift+P"
389 389 self.print_action = QtGui.QAction("&Print",
390 390 self,
391 391 shortcut=printkey,
392 392 triggered=self.print_action_active_frontend)
393 393 self.add_menu_action(self.file_menu, self.print_action, True)
394 394
395 395 if sys.platform != 'darwin':
396 396 # OSX always has Quit in the Application menu, only add it
397 397 # to the File menu elsewhere.
398 398
399 399 self.file_menu.addSeparator()
400 400
401 401 self.quit_action = QtGui.QAction("&Quit",
402 402 self,
403 403 shortcut=QtGui.QKeySequence.Quit,
404 404 triggered=self.close,
405 405 )
406 406 self.add_menu_action(self.file_menu, self.quit_action)
407 407
408 408
409 409 def init_edit_menu(self):
410 410 self.edit_menu = self.menuBar().addMenu("&Edit")
411 411
412 412 self.undo_action = QtGui.QAction("&Undo",
413 413 self,
414 414 shortcut=QtGui.QKeySequence.Undo,
415 415 statusTip="Undo last action if possible",
416 416 triggered=self.undo_active_frontend
417 417 )
418 418 self.add_menu_action(self.edit_menu, self.undo_action)
419 419
420 420 self.redo_action = QtGui.QAction("&Redo",
421 421 self,
422 422 shortcut=QtGui.QKeySequence.Redo,
423 423 statusTip="Redo last action if possible",
424 424 triggered=self.redo_active_frontend)
425 425 self.add_menu_action(self.edit_menu, self.redo_action)
426 426
427 427 self.edit_menu.addSeparator()
428 428
429 429 self.cut_action = QtGui.QAction("&Cut",
430 430 self,
431 431 shortcut=QtGui.QKeySequence.Cut,
432 432 triggered=self.cut_active_frontend
433 433 )
434 434 self.add_menu_action(self.edit_menu, self.cut_action, True)
435 435
436 436 self.copy_action = QtGui.QAction("&Copy",
437 437 self,
438 438 shortcut=QtGui.QKeySequence.Copy,
439 439 triggered=self.copy_active_frontend
440 440 )
441 441 self.add_menu_action(self.edit_menu, self.copy_action, True)
442 442
443 443 self.copy_raw_action = QtGui.QAction("Copy (&Raw Text)",
444 444 self,
445 445 shortcut="Ctrl+Shift+C",
446 446 triggered=self.copy_raw_active_frontend
447 447 )
448 448 self.add_menu_action(self.edit_menu, self.copy_raw_action, True)
449 449
450 450 self.paste_action = QtGui.QAction("&Paste",
451 451 self,
452 452 shortcut=QtGui.QKeySequence.Paste,
453 453 triggered=self.paste_active_frontend
454 454 )
455 455 self.add_menu_action(self.edit_menu, self.paste_action, True)
456 456
457 457 self.edit_menu.addSeparator()
458 458
459 459 selectall = QtGui.QKeySequence(QtGui.QKeySequence.SelectAll)
460 460 if selectall.matches("Ctrl+A") and sys.platform != 'darwin':
461 461 # Only override the default if there is a collision.
462 462 # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
463 463 selectall = "Ctrl+Shift+A"
464 464 self.select_all_action = QtGui.QAction("Select &All",
465 465 self,
466 466 shortcut=selectall,
467 467 triggered=self.select_all_active_frontend
468 468 )
469 469 self.add_menu_action(self.edit_menu, self.select_all_action, True)
470 470
471 471
472 472 def init_view_menu(self):
473 473 self.view_menu = self.menuBar().addMenu("&View")
474 474
475 475 if sys.platform != 'darwin':
476 476 # disable on OSX, where there is always a menu bar
477 477 self.toggle_menu_bar_act = QtGui.QAction("Toggle &Menu Bar",
478 478 self,
479 479 shortcut="Ctrl+Shift+M",
480 480 statusTip="Toggle visibility of menubar",
481 481 triggered=self.toggle_menu_bar)
482 482 self.add_menu_action(self.view_menu, self.toggle_menu_bar_act)
483 483
484 484 fs_key = "Ctrl+Meta+F" if sys.platform == 'darwin' else "F11"
485 485 self.full_screen_act = QtGui.QAction("&Full Screen",
486 486 self,
487 487 shortcut=fs_key,
488 488 statusTip="Toggle between Fullscreen and Normal Size",
489 489 triggered=self.toggleFullScreen)
490 490 self.add_menu_action(self.view_menu, self.full_screen_act)
491 491
492 492 self.view_menu.addSeparator()
493 493
494 494 self.increase_font_size = QtGui.QAction("Zoom &In",
495 495 self,
496 496 shortcut=QtGui.QKeySequence.ZoomIn,
497 497 triggered=self.increase_font_size_active_frontend
498 498 )
499 499 self.add_menu_action(self.view_menu, self.increase_font_size, True)
500 500
501 501 self.decrease_font_size = QtGui.QAction("Zoom &Out",
502 502 self,
503 503 shortcut=QtGui.QKeySequence.ZoomOut,
504 504 triggered=self.decrease_font_size_active_frontend
505 505 )
506 506 self.add_menu_action(self.view_menu, self.decrease_font_size, True)
507 507
508 508 self.reset_font_size = QtGui.QAction("Zoom &Reset",
509 509 self,
510 510 shortcut="Ctrl+0",
511 511 triggered=self.reset_font_size_active_frontend
512 512 )
513 513 self.add_menu_action(self.view_menu, self.reset_font_size, True)
514 514
515 515 self.view_menu.addSeparator()
516 516
517 517 self.clear_action = QtGui.QAction("&Clear Screen",
518 518 self,
519 519 shortcut='Ctrl+L',
520 520 statusTip="Clear the console",
521 521 triggered=self.clear_magic_active_frontend)
522 522 self.add_menu_action(self.view_menu, self.clear_action)
523 523
524 524 def init_kernel_menu(self):
525 525 self.kernel_menu = self.menuBar().addMenu("&Kernel")
526 526 # Qt on OSX maps Ctrl to Cmd, and Meta to Ctrl
527 527 # keep the signal shortcuts to ctrl, rather than
528 528 # platform-default like we do elsewhere.
529 529
530 530 ctrl = "Meta" if sys.platform == 'darwin' else "Ctrl"
531 531
532 532 self.interrupt_kernel_action = QtGui.QAction("Interrupt current Kernel",
533 533 self,
534 534 triggered=self.interrupt_kernel_active_frontend,
535 535 shortcut=ctrl+"+C",
536 536 )
537 537 self.add_menu_action(self.kernel_menu, self.interrupt_kernel_action)
538 538
539 539 self.restart_kernel_action = QtGui.QAction("Restart current Kernel",
540 540 self,
541 541 triggered=self.restart_kernel_active_frontend,
542 542 shortcut=ctrl+"+.",
543 543 )
544 544 self.add_menu_action(self.kernel_menu, self.restart_kernel_action)
545 545
546 546 self.kernel_menu.addSeparator()
547 547
548 548 def _make_dynamic_magic(self,magic):
549 549 """Return a function `fun` that will execute `magic` on active frontend.
550 550
551 551 Parameters
552 552 ----------
553 553 magic : string
554 554 string that will be executed as is when the returned function is called
555 555
556 556 Returns
557 557 -------
558 558 fun : function
559 559 function with no parameters, when called will execute `magic` on the
560 560 current active frontend at call time
561 561
562 562 See Also
563 563 --------
564 564 populate_all_magic_menu : generate the "All Magics..." menu
565 565
566 566 Notes
567 567 -----
568 568 `fun` execute `magic` an active frontend at the moment it is triggerd,
569 569 not the active frontend at the moment it has been created.
570 570
571 571 This function is mostly used to create the "All Magics..." Menu at run time.
572 572 """
573 573 # need to level nested function to be sure to past magic
574 574 # on active frontend **at run time**.
575 575 def inner_dynamic_magic():
576 576 self.active_frontend.execute(magic)
577 577 inner_dynamic_magic.__name__ = "dynamics_magic_s"
578 578 return inner_dynamic_magic
579 579
580 580 def populate_all_magic_menu(self, listofmagic=None):
581 581 """Clean "All Magics..." menu and repopulate it with `listofmagic`
582 582
583 583 Parameters
584 584 ----------
585 585 listofmagic : string,
586 586 repr() of a list of strings, send back by the kernel
587 587
588 588 Notes
589 589 -----
590 590 `listofmagic`is a repr() of list because it is fed with the result of
591 591 a 'user_expression'
592 592 """
593 593 alm_magic_menu = self.all_magic_menu
594 594 alm_magic_menu.clear()
595 595
596 596 # list of protected magic that don't like to be called without argument
597 597 # append '?' to the end to print the docstring when called from the menu
598 598 protected_magic = set(["more","less","load_ext","pycat","loadpy","save"])
599 599 magics=re.findall('\w+', listofmagic)
600 600 for magic in magics:
601 601 if magic in protected_magic:
602 602 pmagic = '%s%s%s'%('%',magic,'?')
603 603 else:
604 604 pmagic = '%s%s'%('%',magic)
605 605 xaction = QtGui.QAction(pmagic,
606 606 self,
607 607 triggered=self._make_dynamic_magic(pmagic)
608 608 )
609 609 alm_magic_menu.addAction(xaction)
610 610
611 611 def update_all_magic_menu(self):
612 612 """ Update the list on magic in the "All Magics..." Menu
613 613
614 614 Request the kernel with the list of availlable magic and populate the
615 615 menu with the list received back
616 616
617 617 """
618 618 # first define a callback which will get the list of all magic and put it in the menu.
619 619 self.active_frontend._silent_exec_callback('get_ipython().lsmagic()', self.populate_all_magic_menu)
620 620
621 621 def init_magic_menu(self):
622 622 self.magic_menu = self.menuBar().addMenu("&Magic")
623 623 self.all_magic_menu = self.magic_menu.addMenu("&All Magics")
624 624
625 625 # This action should usually not appear as it will be cleared when menu
626 626 # is updated at first kernel response. Though, it is necessary when
627 627 # connecting through X-forwarding, as in this case, the menu is not
628 628 # auto updated, SO DO NOT DELETE.
629 629 self.pop = QtGui.QAction("&Update All Magic Menu ",
630 630 self, triggered=self.update_all_magic_menu)
631 631 self.add_menu_action(self.all_magic_menu, self.pop)
632 632 # we need to populate the 'Magic Menu' once the kernel has answer at
633 633 # least once let's do it immedialy, but it's assured to works
634 634 self.pop.trigger()
635 635
636 636 self.reset_action = QtGui.QAction("&Reset",
637 637 self,
638 638 statusTip="Clear all varible from workspace",
639 639 triggered=self.reset_magic_active_frontend)
640 640 self.add_menu_action(self.magic_menu, self.reset_action)
641 641
642 642 self.history_action = QtGui.QAction("&History",
643 643 self,
644 644 statusTip="show command history",
645 645 triggered=self.history_magic_active_frontend)
646 646 self.add_menu_action(self.magic_menu, self.history_action)
647 647
648 648 self.save_action = QtGui.QAction("E&xport History ",
649 649 self,
650 650 statusTip="Export History as Python File",
651 651 triggered=self.save_magic_active_frontend)
652 652 self.add_menu_action(self.magic_menu, self.save_action)
653 653
654 654 self.who_action = QtGui.QAction("&Who",
655 655 self,
656 656 statusTip="List interactive variable",
657 657 triggered=self.who_magic_active_frontend)
658 658 self.add_menu_action(self.magic_menu, self.who_action)
659 659
660 660 self.who_ls_action = QtGui.QAction("Wh&o ls",
661 661 self,
662 662 statusTip="Return a list of interactive variable",
663 663 triggered=self.who_ls_magic_active_frontend)
664 664 self.add_menu_action(self.magic_menu, self.who_ls_action)
665 665
666 666 self.whos_action = QtGui.QAction("Who&s",
667 667 self,
668 668 statusTip="List interactive variable with detail",
669 669 triggered=self.whos_magic_active_frontend)
670 670 self.add_menu_action(self.magic_menu, self.whos_action)
671 671
672 672 def init_window_menu(self):
673 673 self.window_menu = self.menuBar().addMenu("&Window")
674 674 if sys.platform == 'darwin':
675 675 # add min/maximize actions to OSX, which lacks default bindings.
676 676 self.minimizeAct = QtGui.QAction("Mini&mize",
677 677 self,
678 678 shortcut="Ctrl+m",
679 679 statusTip="Minimize the window/Restore Normal Size",
680 680 triggered=self.toggleMinimized)
681 681 # maximize is called 'Zoom' on OSX for some reason
682 682 self.maximizeAct = QtGui.QAction("&Zoom",
683 683 self,
684 684 shortcut="Ctrl+Shift+M",
685 685 statusTip="Maximize the window/Restore Normal Size",
686 686 triggered=self.toggleMaximized)
687 687
688 688 self.add_menu_action(self.window_menu, self.minimizeAct)
689 689 self.add_menu_action(self.window_menu, self.maximizeAct)
690 690 self.window_menu.addSeparator()
691 691
692 692 prev_key = "Ctrl+Shift+Left" if sys.platform == 'darwin' else "Ctrl+PgUp"
693 693 self.prev_tab_act = QtGui.QAction("Pre&vious Tab",
694 694 self,
695 695 shortcut=prev_key,
696 696 statusTip="Select previous tab",
697 697 triggered=self.prev_tab)
698 698 self.add_menu_action(self.window_menu, self.prev_tab_act)
699 699
700 700 next_key = "Ctrl+Shift+Right" if sys.platform == 'darwin' else "Ctrl+PgDown"
701 701 self.next_tab_act = QtGui.QAction("Ne&xt Tab",
702 702 self,
703 703 shortcut=next_key,
704 704 statusTip="Select next tab",
705 705 triggered=self.next_tab)
706 706 self.add_menu_action(self.window_menu, self.next_tab_act)
707 707
708 708 def init_help_menu(self):
709 709 # please keep the Help menu in Mac Os even if empty. It will
710 710 # automatically contain a search field to search inside menus and
711 711 # please keep it spelled in English, as long as Qt Doesn't support
712 712 # a QAction.MenuRole like HelpMenuRole otherwise it will loose
713 713 # this search field fonctionality
714 714
715 715 self.help_menu = self.menuBar().addMenu("&Help")
716 716
717 717
718 718 # Help Menu
719 719
720 720 self.intro_active_frontend_action = QtGui.QAction("&Intro to IPython",
721 721 self,
722 722 triggered=self.intro_active_frontend
723 723 )
724 724 self.add_menu_action(self.help_menu, self.intro_active_frontend_action)
725 725
726 726 self.quickref_active_frontend_action = QtGui.QAction("IPython &Cheat Sheet",
727 727 self,
728 728 triggered=self.quickref_active_frontend
729 729 )
730 730 self.add_menu_action(self.help_menu, self.quickref_active_frontend_action)
731 731
732 732 self.guiref_active_frontend_action = QtGui.QAction("&Qt Console",
733 733 self,
734 734 triggered=self.guiref_active_frontend
735 735 )
736 736 self.add_menu_action(self.help_menu, self.guiref_active_frontend_action)
737 737
738 738 self.onlineHelpAct = QtGui.QAction("Open Online &Help",
739 739 self,
740 740 triggered=self._open_online_help)
741 741 self.add_menu_action(self.help_menu, self.onlineHelpAct)
742 742
743 743 # minimize/maximize/fullscreen actions:
744 744
745 745 def toggle_menu_bar(self):
746 746 menu_bar = self.menuBar()
747 747 if menu_bar.isVisible():
748 748 menu_bar.setVisible(False)
749 749 else:
750 750 menu_bar.setVisible(True)
751 751
752 752 def toggleMinimized(self):
753 753 if not self.isMinimized():
754 754 self.showMinimized()
755 755 else:
756 756 self.showNormal()
757 757
758 758 def _open_online_help(self):
759 759 filename="http://ipython.org/ipython-doc/stable/index.html"
760 760 webbrowser.open(filename, new=1, autoraise=True)
761 761
762 762 def toggleMaximized(self):
763 763 if not self.isMaximized():
764 764 self.showMaximized()
765 765 else:
766 766 self.showNormal()
767 767
768 768 # Min/Max imizing while in full screen give a bug
769 769 # when going out of full screen, at least on OSX
770 770 def toggleFullScreen(self):
771 771 if not self.isFullScreen():
772 772 self.showFullScreen()
773 773 if sys.platform == 'darwin':
774 774 self.maximizeAct.setEnabled(False)
775 775 self.minimizeAct.setEnabled(False)
776 776 else:
777 777 self.showNormal()
778 778 if sys.platform == 'darwin':
779 779 self.maximizeAct.setEnabled(True)
780 780 self.minimizeAct.setEnabled(True)
781 781
782 782 def close_active_frontend(self):
783 783 self.close_tab(self.active_frontend)
784 784
785 785 def restart_kernel_active_frontend(self):
786 786 self.active_frontend.request_restart_kernel()
787 787
788 788 def interrupt_kernel_active_frontend(self):
789 789 self.active_frontend.request_interrupt_kernel()
790 790
791 791 def cut_active_frontend(self):
792 792 widget = self.active_frontend
793 793 if widget.can_cut():
794 794 widget.cut()
795 795
796 796 def copy_active_frontend(self):
797 797 widget = self.active_frontend
798 if widget.can_copy():
799 widget.copy()
798 widget.copy()
800 799
801 800 def copy_raw_active_frontend(self):
802 801 self.active_frontend._copy_raw_action.trigger()
803 802
804 803 def paste_active_frontend(self):
805 804 widget = self.active_frontend
806 805 if widget.can_paste():
807 806 widget.paste()
808 807
809 808 def undo_active_frontend(self):
810 809 self.active_frontend.undo()
811 810
812 811 def redo_active_frontend(self):
813 812 self.active_frontend.redo()
814 813
815 814 def reset_magic_active_frontend(self):
816 815 self.active_frontend.execute("%reset")
817 816
818 817 def history_magic_active_frontend(self):
819 818 self.active_frontend.execute("%history")
820 819
821 820 def save_magic_active_frontend(self):
822 821 self.active_frontend.save_magic()
823 822
824 823 def clear_magic_active_frontend(self):
825 824 self.active_frontend.execute("%clear")
826 825
827 826 def who_magic_active_frontend(self):
828 827 self.active_frontend.execute("%who")
829 828
830 829 def who_ls_magic_active_frontend(self):
831 830 self.active_frontend.execute("%who_ls")
832 831
833 832 def whos_magic_active_frontend(self):
834 833 self.active_frontend.execute("%whos")
835 834
836 835 def print_action_active_frontend(self):
837 836 self.active_frontend.print_action.trigger()
838 837
839 838 def export_action_active_frontend(self):
840 839 self.active_frontend.export_action.trigger()
841 840
842 841 def select_all_active_frontend(self):
843 842 self.active_frontend.select_all_action.trigger()
844 843
845 844 def increase_font_size_active_frontend(self):
846 845 self.active_frontend.increase_font_size.trigger()
847 846
848 847 def decrease_font_size_active_frontend(self):
849 848 self.active_frontend.decrease_font_size.trigger()
850 849
851 850 def reset_font_size_active_frontend(self):
852 851 self.active_frontend.reset_font_size.trigger()
853 852
854 853 def guiref_active_frontend(self):
855 854 self.active_frontend.execute("%guiref")
856 855
857 856 def intro_active_frontend(self):
858 857 self.active_frontend.execute("?")
859 858
860 859 def quickref_active_frontend(self):
861 860 self.active_frontend.execute("%quickref")
862 861 #---------------------------------------------------------------------------
863 862 # QWidget interface
864 863 #---------------------------------------------------------------------------
865 864
866 865 def closeEvent(self, event):
867 866 """ Forward the close event to every tabs contained by the windows
868 867 """
869 868 if self.tab_widget.count() == 0:
870 869 # no tabs, just close
871 870 event.accept()
872 871 return
873 872 # Do Not loop on the widget count as it change while closing
874 873 title = self.window().windowTitle()
875 874 cancel = QtGui.QMessageBox.Cancel
876 875 okay = QtGui.QMessageBox.Ok
877 876
878 877 if self.confirm_exit:
879 878 if self.tab_widget.count() > 1:
880 879 msg = "Close all tabs, stop all kernels, and Quit?"
881 880 else:
882 881 msg = "Close console, stop kernel, and Quit?"
883 882 info = "Kernels not started here (e.g. notebooks) will be left alone."
884 883 closeall = QtGui.QPushButton("&Yes, quit everything", self)
885 884 closeall.setShortcut('Y')
886 885 box = QtGui.QMessageBox(QtGui.QMessageBox.Question,
887 886 title, msg)
888 887 box.setInformativeText(info)
889 888 box.addButton(cancel)
890 889 box.addButton(closeall, QtGui.QMessageBox.YesRole)
891 890 box.setDefaultButton(closeall)
892 891 box.setEscapeButton(cancel)
893 892 pixmap = QtGui.QPixmap(self._app.icon.pixmap(QtCore.QSize(64,64)))
894 893 box.setIconPixmap(pixmap)
895 894 reply = box.exec_()
896 895 else:
897 896 reply = okay
898 897
899 898 if reply == cancel:
900 899 event.ignore()
901 900 return
902 901 if reply == okay:
903 902 while self.tab_widget.count() >= 1:
904 903 # prevent further confirmations:
905 904 widget = self.active_frontend
906 905 widget._confirm_exit = False
907 906 self.close_tab(widget)
908 907 event.accept()
909 908
General Comments 0
You need to be logged in to leave comments. Login now