1"""An abstract base class for console-type widgets."""
2
3# Copyright (c) Jupyter Development Team.
4# Distributed under the terms of the Modified BSD License.
5
6from functools import partial
7import os
8import os.path
9import re
10import sys
11from textwrap import dedent
12import time
13from unicodedata import category
14import webbrowser
15
16from qtpy import QtCore, QtGui, QtPrintSupport, QtWidgets
17
18from traitlets.config.configurable import LoggingConfigurable
19from qtconsole.rich_text import HtmlExporter
20from qtconsole.util import MetaQObjectHasTraits, get_font, superQ
21from ipython_genutils.text import columnize
22from traitlets import Bool, Enum, Integer, Unicode
23from .ansi_code_processor import QtAnsiCodeProcessor
24from .completion_widget import CompletionWidget
25from .completion_html import CompletionHtml
26from .completion_plain import CompletionPlain
27from .kill_ring import QtKillRing
28
29
30def is_letter_or_number(char):
31    """ Returns whether the specified unicode character is a letter or a number.
32    """
33    cat = category(char)
34    return cat.startswith('L') or cat.startswith('N')
35
36def is_whitespace(char):
37    """Check whether a given char counts as white space."""
38    return category(char).startswith('Z')
39
40#-----------------------------------------------------------------------------
41# Classes
42#-----------------------------------------------------------------------------
43
44class ConsoleWidget(MetaQObjectHasTraits('NewBase', (LoggingConfigurable, superQ(QtWidgets.QWidget)), {})):
45    """ An abstract base class for console-type widgets. This class has
46        functionality for:
47
48            * Maintaining a prompt and editing region
49            * Providing the traditional Unix-style console keyboard shortcuts
50            * Performing tab completion
51            * Paging text
52            * Handling ANSI escape codes
53
54        ConsoleWidget also provides a number of utility methods that will be
55        convenient to implementors of a console-style widget.
56    """
57
58    #------ Configuration ------------------------------------------------------
59
60    ansi_codes = Bool(True, config=True,
61        help="Whether to process ANSI escape codes."
62    )
63    buffer_size = Integer(500, config=True,
64        help="""
65        The maximum number of lines of text before truncation. Specifying a
66        non-positive number disables text truncation (not recommended).
67        """
68    )
69    execute_on_complete_input = Bool(True, config=True,
70        help="""Whether to automatically execute on syntactically complete input.
71
72        If False, Shift-Enter is required to submit each execution.
73        Disabling this is mainly useful for non-Python kernels,
74        where the completion check would be wrong.
75        """
76    )
77    gui_completion = Enum(['plain', 'droplist', 'ncurses'], config=True,
78                    default_value = 'ncurses',
79                    help="""
80                    The type of completer to use. Valid values are:
81
82                    'plain'   : Show the available completion as a text list
83                                Below the editing area.
84                    'droplist': Show the completion in a drop down list navigable
85                                by the arrow keys, and from which you can select
86                                completion by pressing Return.
87                    'ncurses' : Show the completion as a text list which is navigable by
88                                `tab` and arrow keys.
89                    """
90    )
91    # NOTE: this value can only be specified during initialization.
92    kind = Enum(['plain', 'rich'], default_value='plain', config=True,
93        help="""
94        The type of underlying text widget to use. Valid values are 'plain',
95        which specifies a QPlainTextEdit, and 'rich', which specifies a
96        QTextEdit.
97        """
98    )
99    # NOTE: this value can only be specified during initialization.
100    paging = Enum(['inside', 'hsplit', 'vsplit', 'custom', 'none'],
101                  default_value='inside', config=True,
102        help="""
103        The type of paging to use. Valid values are:
104
105        'inside'
106           The widget pages like a traditional terminal.
107        'hsplit'
108           When paging is requested, the widget is split horizontally. The top
109           pane contains the console, and the bottom pane contains the paged text.
110        'vsplit'
111           Similar to 'hsplit', except that a vertical splitter is used.
112        'custom'
113           No action is taken by the widget beyond emitting a
114           'custom_page_requested(str)' signal.
115        'none'
116           The text is written directly to the console.
117        """)
118
119    scrollbar_visibility = Bool(True, config=True,
120        help="""The visibility of the scrollar. If False then the scrollbar will be
121        invisible."""
122    )
123
124    font_family = Unicode(config=True,
125        help="""The font family to use for the console.
126        On OSX this defaults to Monaco, on Windows the default is
127        Consolas with fallback of Courier, and on other platforms
128        the default is Monospace.
129        """)
130    def _font_family_default(self):
131        if sys.platform == 'win32':
132            # Consolas ships with Vista/Win7, fallback to Courier if needed
133            return 'Consolas'
134        elif sys.platform == 'darwin':
135            # OSX always has Monaco, no need for a fallback
136            return 'Monaco'
137        else:
138            # Monospace should always exist, no need for a fallback
139            return 'Monospace'
140
141    font_size = Integer(config=True,
142        help="""The font size. If unconfigured, Qt will be entrusted
143        with the size of the font.
144        """)
145
146    console_width = Integer(81, config=True,
147        help="""The width of the console at start time in number
148        of characters (will double with `hsplit` paging)
149        """)
150
151    console_height = Integer(25, config=True,
152        help="""The height of the console at start time in number
153        of characters (will double with `vsplit` paging)
154        """)
155
156    # Whether to override ShortcutEvents for the keybindings defined by this
157    # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
158    # priority (when it has focus) over, e.g., window-level menu shortcuts.
159    override_shortcuts = Bool(False)
160
161    # ------ Custom Qt Widgets -------------------------------------------------
162
163    # For other projects to easily override the Qt widgets used by the console
164    # (e.g. Spyder)
165    custom_control = None
166    custom_page_control = None
167
168    #------ Signals ------------------------------------------------------------
169
170    # Signals that indicate ConsoleWidget state.
171    copy_available = QtCore.Signal(bool)
172    redo_available = QtCore.Signal(bool)
173    undo_available = QtCore.Signal(bool)
174
175    # Signal emitted when paging is needed and the paging style has been
176    # specified as 'custom'.
177    custom_page_requested = QtCore.Signal(object)
178
179    # Signal emitted when the font is changed.
180    font_changed = QtCore.Signal(QtGui.QFont)
181
182    #------ Protected class variables ------------------------------------------
183
184    # control handles
185    _control = None
186    _page_control = None
187    _splitter = None
188
189    # When the control key is down, these keys are mapped.
190    _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
191                         QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
192                         QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
193                         QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
194                         QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
195                         QtCore.Qt.Key_H : QtCore.Qt.Key_Backspace, }
196    if not sys.platform == 'darwin':
197        # On OS X, Ctrl-E already does the right thing, whereas End moves the
198        # cursor to the bottom of the buffer.
199        _ctrl_down_remap[QtCore.Qt.Key_E] = QtCore.Qt.Key_End
200
201    # The shortcuts defined by this widget. We need to keep track of these to
202    # support 'override_shortcuts' above.
203    _shortcuts = set(_ctrl_down_remap.keys()) | \
204                     { QtCore.Qt.Key_C, QtCore.Qt.Key_G, QtCore.Qt.Key_O,
205                       QtCore.Qt.Key_V }
206
207    _temp_buffer_filled = False
208
209    #---------------------------------------------------------------------------
210    # 'QObject' interface
211    #---------------------------------------------------------------------------
212
213    def __init__(self, parent=None, **kw):
214        """ Create a ConsoleWidget.
215
216        Parameters
217        ----------
218        parent : QWidget, optional [default None]
219            The parent for this widget.
220        """
221        super().__init__(**kw)
222        if parent:
223            self.setParent(parent)
224
225        self._is_complete_msg_id = None
226        self._is_complete_timeout = 0.1
227        self._is_complete_max_time = None
228
229        # While scrolling the pager on Mac OS X, it tears badly.  The
230        # NativeGesture is platform and perhaps build-specific hence
231        # we take adequate precautions here.
232        self._pager_scroll_events = [QtCore.QEvent.Wheel]
233        if hasattr(QtCore.QEvent, 'NativeGesture'):
234            self._pager_scroll_events.append(QtCore.QEvent.NativeGesture)
235
236        # Create the layout and underlying text widget.
237        layout = QtWidgets.QStackedLayout(self)
238        layout.setContentsMargins(0, 0, 0, 0)
239        self._control = self._create_control()
240        if self.paging in ('hsplit', 'vsplit'):
241            self._splitter = QtWidgets.QSplitter()
242            if self.paging == 'hsplit':
243                self._splitter.setOrientation(QtCore.Qt.Horizontal)
244            else:
245                self._splitter.setOrientation(QtCore.Qt.Vertical)
246            self._splitter.addWidget(self._control)
247            layout.addWidget(self._splitter)
248        else:
249            layout.addWidget(self._control)
250
251        # Create the paging widget, if necessary.
252        if self.paging in ('inside', 'hsplit', 'vsplit'):
253            self._page_control = self._create_page_control()
254            if self._splitter:
255                self._page_control.hide()
256                self._splitter.addWidget(self._page_control)
257            else:
258                layout.addWidget(self._page_control)
259
260        # Initialize protected variables. Some variables contain useful state
261        # information for subclasses; they should be considered read-only.
262        self._append_before_prompt_cursor = self._control.textCursor()
263        self._ansi_processor = QtAnsiCodeProcessor()
264        if self.gui_completion == 'ncurses':
265            self._completion_widget = CompletionHtml(self)
266        elif self.gui_completion == 'droplist':
267            self._completion_widget = CompletionWidget(self)
268        elif self.gui_completion == 'plain':
269            self._completion_widget = CompletionPlain(self)
270
271        self._continuation_prompt = '> '
272        self._continuation_prompt_html = None
273        self._executing = False
274        self._filter_resize = False
275        self._html_exporter = HtmlExporter(self._control)
276        self._input_buffer_executing = ''
277        self._input_buffer_pending = ''
278        self._kill_ring = QtKillRing(self._control)
279        self._prompt = ''
280        self._prompt_html = None
281        self._prompt_cursor = self._control.textCursor()
282        self._prompt_sep = ''
283        self._reading = False
284        self._reading_callback = None
285        self._tab_width = 4
286
287        # List of strings pending to be appended as plain text in the widget.
288        # The text is not immediately inserted when available to not
289        # choke the Qt event loop with paint events for the widget in
290        # case of lots of output from kernel.
291        self._pending_insert_text = []
292
293        # Timer to flush the pending stream messages. The interval is adjusted
294        # later based on actual time taken for flushing a screen (buffer_size)
295        # of output text.
296        self._pending_text_flush_interval = QtCore.QTimer(self._control)
297        self._pending_text_flush_interval.setInterval(100)
298        self._pending_text_flush_interval.setSingleShot(True)
299        self._pending_text_flush_interval.timeout.connect(
300                                            self._on_flush_pending_stream_timer)
301
302        # Set a monospaced font.
303        self.reset_font()
304
305        # Configure actions.
306        action = QtWidgets.QAction('Print', None)
307        action.setEnabled(True)
308        printkey = QtGui.QKeySequence(QtGui.QKeySequence.Print)
309        if printkey.matches("Ctrl+P") and sys.platform != 'darwin':
310            # Only override the default if there is a collision.
311            # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
312            printkey = "Ctrl+Shift+P"
313        action.setShortcut(printkey)
314        action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
315        action.triggered.connect(self.print_)
316        self.addAction(action)
317        self.print_action = action
318
319        action = QtWidgets.QAction('Save as HTML/XML', None)
320        action.setShortcut(QtGui.QKeySequence.Save)
321        action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
322        action.triggered.connect(self.export_html)
323        self.addAction(action)
324        self.export_action = action
325
326        action = QtWidgets.QAction('Select All', None)
327        action.setEnabled(True)
328        selectall = QtGui.QKeySequence(QtGui.QKeySequence.SelectAll)
329        if selectall.matches("Ctrl+A") and sys.platform != 'darwin':
330            # Only override the default if there is a collision.
331            # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
332            selectall = "Ctrl+Shift+A"
333        action.setShortcut(selectall)
334        action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
335        action.triggered.connect(self.select_all_smart)
336        self.addAction(action)
337        self.select_all_action = action
338
339        self.increase_font_size = QtWidgets.QAction("Bigger Font",
340                self,
341                shortcut=QtGui.QKeySequence.ZoomIn,
342                shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut,
343                statusTip="Increase the font size by one point",
344                triggered=self._increase_font_size)
345        self.addAction(self.increase_font_size)
346
347        self.decrease_font_size = QtWidgets.QAction("Smaller Font",
348                self,
349                shortcut=QtGui.QKeySequence.ZoomOut,
350                shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut,
351                statusTip="Decrease the font size by one point",
352                triggered=self._decrease_font_size)
353        self.addAction(self.decrease_font_size)
354
355        self.reset_font_size = QtWidgets.QAction("Normal Font",
356                self,
357                shortcut="Ctrl+0",
358                shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut,
359                statusTip="Restore the Normal font size",
360                triggered=self.reset_font)
361        self.addAction(self.reset_font_size)
362
363        # Accept drag and drop events here. Drops were already turned off
364        # in self._control when that widget was created.
365        self.setAcceptDrops(True)
366
367    #---------------------------------------------------------------------------
368    # Drag and drop support
369    #---------------------------------------------------------------------------
370
371    def dragEnterEvent(self, e):
372        if e.mimeData().hasUrls():
373            # The link action should indicate to that the drop will insert
374            # the file anme.
375            e.setDropAction(QtCore.Qt.LinkAction)
376            e.accept()
377        elif e.mimeData().hasText():
378            # By changing the action to copy we don't need to worry about
379            # the user accidentally moving text around in the widget.
380            e.setDropAction(QtCore.Qt.CopyAction)
381            e.accept()
382
383    def dragMoveEvent(self, e):
384        if e.mimeData().hasUrls():
385            pass
386        elif e.mimeData().hasText():
387            cursor = self._control.cursorForPosition(e.pos())
388            if self._in_buffer(cursor.position()):
389                e.setDropAction(QtCore.Qt.CopyAction)
390                self._control.setTextCursor(cursor)
391            else:
392                e.setDropAction(QtCore.Qt.IgnoreAction)
393            e.accept()
394
395    def dropEvent(self, e):
396        if e.mimeData().hasUrls():
397            self._keep_cursor_in_buffer()
398            cursor = self._control.textCursor()
399            filenames = [url.toLocalFile() for url in e.mimeData().urls()]
400            text = ', '.join("'" + f.replace("'", "'\"'\"'") + "'"
401                             for f in filenames)
402            self._insert_plain_text_into_buffer(cursor, text)
403        elif e.mimeData().hasText():
404            cursor = self._control.cursorForPosition(e.pos())
405            if self._in_buffer(cursor.position()):
406                text = e.mimeData().text()
407                self._insert_plain_text_into_buffer(cursor, text)
408
409    def eventFilter(self, obj, event):
410        """ Reimplemented to ensure a console-like behavior in the underlying
411            text widgets.
412        """
413        etype = event.type()
414        if etype == QtCore.QEvent.KeyPress:
415
416            # Re-map keys for all filtered widgets.
417            key = event.key()
418            if self._control_key_down(event.modifiers()) and \
419                    key in self._ctrl_down_remap:
420                new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
421                                            self._ctrl_down_remap[key],
422                                            QtCore.Qt.NoModifier)
423                QtWidgets.QApplication.instance().sendEvent(obj, new_event)
424                return True
425
426            elif obj == self._control:
427                return self._event_filter_console_keypress(event)
428
429            elif obj == self._page_control:
430                return self._event_filter_page_keypress(event)
431
432        # Make middle-click paste safe.
433        elif getattr(event, 'button', False) and \
434                etype == QtCore.QEvent.MouseButtonRelease and \
435                event.button() == QtCore.Qt.MidButton and \
436                obj == self._control.viewport():
437            cursor = self._control.cursorForPosition(event.pos())
438            self._control.setTextCursor(cursor)
439            self.paste(QtGui.QClipboard.Selection)
440            return True
441
442        # Manually adjust the scrollbars *after* a resize event is dispatched.
443        elif etype == QtCore.QEvent.Resize and not self._filter_resize:
444            self._filter_resize = True
445            QtWidgets.QApplication.instance().sendEvent(obj, event)
446            self._adjust_scrollbars()
447            self._filter_resize = False
448            return True
449
450        # Override shortcuts for all filtered widgets.
451        elif etype == QtCore.QEvent.ShortcutOverride and \
452                self.override_shortcuts and \
453                self._control_key_down(event.modifiers()) and \
454                event.key() in self._shortcuts:
455            event.accept()
456
457        # Handle scrolling of the vsplit pager. This hack attempts to solve
458        # problems with tearing of the help text inside the pager window.  This
459        # happens only on Mac OS X with both PySide and PyQt. This fix isn't
460        # perfect but makes the pager more usable.
461        elif etype in self._pager_scroll_events and \
462                obj == self._page_control:
463            self._page_control.repaint()
464            return True
465
466        elif etype == QtCore.QEvent.MouseMove:
467            anchor = self._control.anchorAt(event.pos())
468            QtWidgets.QToolTip.showText(event.globalPos(), anchor)
469
470        return super().eventFilter(obj, event)
471
472    #---------------------------------------------------------------------------
473    # 'QWidget' interface
474    #---------------------------------------------------------------------------
475
476    def sizeHint(self):
477        """ Reimplemented to suggest a size that is 80 characters wide and
478            25 lines high.
479        """
480        font_metrics = QtGui.QFontMetrics(self.font)
481        margin = (self._control.frameWidth() +
482                  self._control.document().documentMargin()) * 2
483        style = self.style()
484        splitwidth = style.pixelMetric(QtWidgets.QStyle.PM_SplitterWidth)
485
486        # Note 1: Despite my best efforts to take the various margins into
487        # account, the width is still coming out a bit too small, so we include
488        # a fudge factor of one character here.
489        # Note 2: QFontMetrics.maxWidth is not used here or anywhere else due
490        # to a Qt bug on certain Mac OS systems where it returns 0.
491        width = self._get_font_width() * self.console_width + margin
492        width += style.pixelMetric(QtWidgets.QStyle.PM_ScrollBarExtent)
493
494        if self.paging == 'hsplit':
495            width = width * 2 + splitwidth
496
497        height = font_metrics.height() * self.console_height + margin
498        if self.paging == 'vsplit':
499            height = height * 2 + splitwidth
500
501        return QtCore.QSize(int(width), int(height))
502
503    #---------------------------------------------------------------------------
504    # 'ConsoleWidget' public interface
505    #---------------------------------------------------------------------------
506
507    include_other_output = Bool(False, config=True,
508        help="""Whether to include output from clients
509        other than this one sharing the same kernel.
510
511        Outputs are not displayed until enter is pressed.
512        """
513    )
514
515    other_output_prefix = Unicode('[remote] ', config=True,
516        help="""Prefix to add to outputs coming from clients other than this one.
517
518        Only relevant if include_other_output is True.
519        """
520    )
521
522    def can_copy(self):
523        """ Returns whether text can be copied to the clipboard.
524        """
525        return self._control.textCursor().hasSelection()
526
527    def can_cut(self):
528        """ Returns whether text can be cut to the clipboard.
529        """
530        cursor = self._control.textCursor()
531        return (cursor.hasSelection() and
532                self._in_buffer(cursor.anchor()) and
533                self._in_buffer(cursor.position()))
534
535    def can_paste(self):
536        """ Returns whether text can be pasted from the clipboard.
537        """
538        if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
539            return bool(QtWidgets.QApplication.clipboard().text())
540        return False
541
542    def clear(self, keep_input=True):
543        """ Clear the console.
544
545        Parameters
546        ----------
547        keep_input : bool, optional (default True)
548            If set, restores the old input buffer if a new prompt is written.
549        """
550        if self._executing:
551            self._control.clear()
552        else:
553            if keep_input:
554                input_buffer = self.input_buffer
555            self._control.clear()
556            self._show_prompt()
557            if keep_input:
558                self.input_buffer = input_buffer
559
560    def copy(self):
561        """ Copy the currently selected text to the clipboard.
562        """
563        self.layout().currentWidget().copy()
564
565    def copy_anchor(self, anchor):
566        """ Copy anchor text to the clipboard
567        """
568        QtWidgets.QApplication.clipboard().setText(anchor)
569
570    def cut(self):
571        """ Copy the currently selected text to the clipboard and delete it
572            if it's inside the input buffer.
573        """
574        self.copy()
575        if self.can_cut():
576            self._control.textCursor().removeSelectedText()
577
578    def _handle_is_complete_reply(self, msg):
579        if msg['parent_header'].get('msg_id', 0) != self._is_complete_msg_id:
580            return
581        status = msg['content'].get('status', 'complete')
582        indent = msg['content'].get('indent', '')
583        self._trigger_is_complete_callback(status != 'incomplete', indent)
584
585    def _trigger_is_complete_callback(self, complete=False, indent=''):
586        if self._is_complete_msg_id is not None:
587            self._is_complete_msg_id = None
588            self._is_complete_callback(complete, indent)
589
590    def _register_is_complete_callback(self, source, callback):
591        if self._is_complete_msg_id is not None:
592            if self._is_complete_max_time < time.time():
593                # Second return while waiting for is_complete
594                return
595            else:
596                # request timed out
597                self._trigger_is_complete_callback()
598        self._is_complete_max_time = time.time() + self._is_complete_timeout
599        self._is_complete_callback = callback
600        self._is_complete_msg_id = self.kernel_client.is_complete(source)
601
602    def execute(self, source=None, hidden=False, interactive=False):
603        """ Executes source or the input buffer, possibly prompting for more
604        input.
605
606        Parameters
607        ----------
608        source : str, optional
609
610            The source to execute. If not specified, the input buffer will be
611            used. If specified and 'hidden' is False, the input buffer will be
612            replaced with the source before execution.
613
614        hidden : bool, optional (default False)
615
616            If set, no output will be shown and the prompt will not be modified.
617            In other words, it will be completely invisible to the user that
618            an execution has occurred.
619
620        interactive : bool, optional (default False)
621
622            Whether the console is to treat the source as having been manually
623            entered by the user. The effect of this parameter depends on the
624            subclass implementation.
625
626        Raises
627        ------
628        RuntimeError
629            If incomplete input is given and 'hidden' is True. In this case,
630            it is not possible to prompt for more input.
631
632        Returns
633        -------
634        A boolean indicating whether the source was executed.
635        """
636        # WARNING: The order in which things happen here is very particular, in
637        # large part because our syntax highlighting is fragile. If you change
638        # something, test carefully!
639
640        # Decide what to execute.
641        if source is None:
642            source = self.input_buffer
643        elif not hidden:
644            self.input_buffer = source
645
646        if hidden:
647            self._execute(source, hidden)
648        # Execute the source or show a continuation prompt if it is incomplete.
649        elif interactive and self.execute_on_complete_input:
650            self._register_is_complete_callback(
651                source, partial(self.do_execute, source))
652        else:
653            self.do_execute(source, True, '')
654
655    def do_execute(self, source, complete, indent):
656        if complete:
657            self._append_plain_text('\n')
658            self._input_buffer_executing = self.input_buffer
659            self._executing = True
660            self._finalize_input_request()
661
662            # Perform actual execution.
663            self._execute(source, False)
664
665        else:
666            # Do this inside an edit block so continuation prompts are
667            # removed seamlessly via undo/redo.
668            cursor = self._get_end_cursor()
669            cursor.beginEditBlock()
670            try:
671                cursor.insertText('\n')
672                self._insert_continuation_prompt(cursor, indent)
673            finally:
674                cursor.endEditBlock()
675
676            # Do not do this inside the edit block. It works as expected
677            # when using a QPlainTextEdit control, but does not have an
678            # effect when using a QTextEdit. I believe this is a Qt bug.
679            self._control.moveCursor(QtGui.QTextCursor.End)
680
681    def export_html(self):
682        """ Shows a dialog to export HTML/XML in various formats.
683        """
684        self._html_exporter.export()
685
686    def _finalize_input_request(self):
687        """
688        Set the widget to a non-reading state.
689        """
690        # Must set _reading to False before calling _prompt_finished
691        self._reading = False
692        self._prompt_finished()
693
694        # There is no prompt now, so before_prompt_position is eof
695        self._append_before_prompt_cursor.setPosition(
696            self._get_end_cursor().position())
697
698        # The maximum block count is only in effect during execution.
699        # This ensures that _prompt_pos does not become invalid due to
700        # text truncation.
701        self._control.document().setMaximumBlockCount(self.buffer_size)
702
703        # Setting a positive maximum block count will automatically
704        # disable the undo/redo history, but just to be safe:
705        self._control.setUndoRedoEnabled(False)
706
707    def _get_input_buffer(self, force=False):
708        """ The text that the user has entered entered at the current prompt.
709
710        If the console is currently executing, the text that is executing will
711        always be returned.
712        """
713        # If we're executing, the input buffer may not even exist anymore due to
714        # the limit imposed by 'buffer_size'. Therefore, we store it.
715        if self._executing and not force:
716            return self._input_buffer_executing
717
718        cursor = self._get_end_cursor()
719        cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
720        input_buffer = cursor.selection().toPlainText()
721
722        # Strip out continuation prompts.
723        return input_buffer.replace('\n' + self._continuation_prompt, '\n')
724
725    def _set_input_buffer(self, string):
726        """ Sets the text in the input buffer.
727
728        If the console is currently executing, this call has no *immediate*
729        effect. When the execution is finished, the input buffer will be updated
730        appropriately.
731        """
732        # If we're executing, store the text for later.
733        if self._executing:
734            self._input_buffer_pending = string
735            return
736
737        # Remove old text.
738        cursor = self._get_end_cursor()
739        cursor.beginEditBlock()
740        cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
741        cursor.removeSelectedText()
742
743        # Insert new text with continuation prompts.
744        self._insert_plain_text_into_buffer(self._get_prompt_cursor(), string)
745        cursor.endEditBlock()
746        self._control.moveCursor(QtGui.QTextCursor.End)
747
748    input_buffer = property(_get_input_buffer, _set_input_buffer)
749
750    def _get_font(self):
751        """ The base font being used by the ConsoleWidget.
752        """
753        return self._control.document().defaultFont()
754
755    def _get_font_width(self, font=None):
756        if font is None:
757            font = self.font
758        font_metrics = QtGui.QFontMetrics(font)
759        if hasattr(font_metrics, 'horizontalAdvance'):
760            return font_metrics.horizontalAdvance(' ')
761        else:
762            return font_metrics.width(' ')
763
764    def _set_font(self, font):
765        """ Sets the base font for the ConsoleWidget to the specified QFont.
766        """
767        self._control.setTabStopWidth(
768            self.tab_width * self._get_font_width(font)
769        )
770
771        self._completion_widget.setFont(font)
772        self._control.document().setDefaultFont(font)
773        if self._page_control:
774            self._page_control.document().setDefaultFont(font)
775
776        self.font_changed.emit(font)
777
778    font = property(_get_font, _set_font)
779
780    def _set_completion_widget(self, gui_completion):
781        """ Set gui completion widget.
782        """
783        if gui_completion == 'ncurses':
784            self._completion_widget = CompletionHtml(self)
785        elif gui_completion == 'droplist':
786            self._completion_widget = CompletionWidget(self)
787        elif gui_completion == 'plain':
788            self._completion_widget = CompletionPlain(self)
789
790        self.gui_completion = gui_completion
791
792    def open_anchor(self, anchor):
793        """ Open selected anchor in the default webbrowser
794        """
795        webbrowser.open( anchor )
796
797    def paste(self, mode=QtGui.QClipboard.Clipboard):
798        """ Paste the contents of the clipboard into the input region.
799
800        Parameters
801        ----------
802        mode : QClipboard::Mode, optional [default QClipboard::Clipboard]
803
804            Controls which part of the system clipboard is used. This can be
805            used to access the selection clipboard in X11 and the Find buffer
806            in Mac OS. By default, the regular clipboard is used.
807        """
808        if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
809            # Make sure the paste is safe.
810            self._keep_cursor_in_buffer()
811            cursor = self._control.textCursor()
812
813            # Remove any trailing newline, which confuses the GUI and forces the
814            # user to backspace.
815            text = QtWidgets.QApplication.clipboard().text(mode).rstrip()
816
817            # dedent removes "common leading whitespace" but to preserve relative
818            # indent of multiline code, we have to compensate for any
819            # leading space on the first line, if we're pasting into
820            # an indented position.
821            cursor_offset = cursor.position() - self._get_line_start_pos()
822            if text.startswith(' ' * cursor_offset):
823                text = text[cursor_offset:]
824
825            self._insert_plain_text_into_buffer(cursor, dedent(text))
826
827    def print_(self, printer = None):
828        """ Print the contents of the ConsoleWidget to the specified QPrinter.
829        """
830        if (not printer):
831            printer = QtPrintSupport.QPrinter()
832            if(QtPrintSupport.QPrintDialog(printer).exec_() != QtPrintSupport.QPrintDialog.Accepted):
833                return
834        self._control.print_(printer)
835
836    def prompt_to_top(self):
837        """ Moves the prompt to the top of the viewport.
838        """
839        if not self._executing:
840            prompt_cursor = self._get_prompt_cursor()
841            if self._get_cursor().blockNumber() < prompt_cursor.blockNumber():
842                self._set_cursor(prompt_cursor)
843            self._set_top_cursor(prompt_cursor)
844
845    def redo(self):
846        """ Redo the last operation. If there is no operation to redo, nothing
847            happens.
848        """
849        self._control.redo()
850
851    def reset_font(self):
852        """ Sets the font to the default fixed-width font for this platform.
853        """
854        if sys.platform == 'win32':
855            # Consolas ships with Vista/Win7, fallback to Courier if needed
856            fallback = 'Courier'
857        elif sys.platform == 'darwin':
858            # OSX always has Monaco
859            fallback = 'Monaco'
860        else:
861            # Monospace should always exist
862            fallback = 'Monospace'
863        font = get_font(self.font_family, fallback)
864        if self.font_size:
865            font.setPointSize(self.font_size)
866        else:
867            font.setPointSize(QtWidgets.QApplication.instance().font().pointSize())
868        font.setStyleHint(QtGui.QFont.TypeWriter)
869        self._set_font(font)
870
871    def change_font_size(self, delta):
872        """Change the font size by the specified amount (in points).
873        """
874        font = self.font
875        size = max(font.pointSize() + delta, 1) # minimum 1 point
876        font.setPointSize(size)
877        self._set_font(font)
878
879    def _increase_font_size(self):
880        self.change_font_size(1)
881
882    def _decrease_font_size(self):
883        self.change_font_size(-1)
884
885    def select_all_smart(self):
886        """ Select current cell, or, if already selected, the whole document.
887        """
888        c = self._get_cursor()
889        sel_range = c.selectionStart(), c.selectionEnd()
890
891        c.clearSelection()
892        c.setPosition(self._get_prompt_cursor().position())
893        c.setPosition(self._get_end_pos(),
894                      mode=QtGui.QTextCursor.KeepAnchor)
895        new_sel_range = c.selectionStart(), c.selectionEnd()
896        if sel_range == new_sel_range:
897            # cell already selected, expand selection to whole document
898            self.select_document()
899        else:
900            # set cell selection as active selection
901            self._control.setTextCursor(c)
902
903    def select_document(self):
904        """ Selects all the text in the buffer.
905        """
906        self._control.selectAll()
907
908    def _get_tab_width(self):
909        """ The width (in terms of space characters) for tab characters.
910        """
911        return self._tab_width
912
913    def _set_tab_width(self, tab_width):
914        """ Sets the width (in terms of space characters) for tab characters.
915        """
916        self._control.setTabStopWidth(tab_width * self._get_font_width())
917
918        self._tab_width = tab_width
919
920    tab_width = property(_get_tab_width, _set_tab_width)
921
922    def undo(self):
923        """ Undo the last operation. If there is no operation to undo, nothing
924            happens.
925        """
926        self._control.undo()
927
928    #---------------------------------------------------------------------------
929    # 'ConsoleWidget' abstract interface
930    #---------------------------------------------------------------------------
931
932    def _is_complete(self, source, interactive):
933        """ Returns whether 'source' can be executed. When triggered by an
934            Enter/Return key press, 'interactive' is True; otherwise, it is
935            False.
936        """
937        raise NotImplementedError
938
939    def _execute(self, source, hidden):
940        """ Execute 'source'. If 'hidden', do not show any output.
941        """
942        raise NotImplementedError
943
944    def _prompt_started_hook(self):
945        """ Called immediately after a new prompt is displayed.
946        """
947        pass
948
949    def _prompt_finished_hook(self):
950        """ Called immediately after a prompt is finished, i.e. when some input
951            will be processed and a new prompt displayed.
952        """
953        pass
954
955    def _up_pressed(self, shift_modifier):
956        """ Called when the up key is pressed. Returns whether to continue
957            processing the event.
958        """
959        return True
960
961    def _down_pressed(self, shift_modifier):
962        """ Called when the down key is pressed. Returns whether to continue
963            processing the event.
964        """
965        return True
966
967    def _tab_pressed(self):
968        """ Called when the tab key is pressed. Returns whether to continue
969            processing the event.
970        """
971        return True
972
973    #--------------------------------------------------------------------------
974    # 'ConsoleWidget' protected interface
975    #--------------------------------------------------------------------------
976
977    def _append_custom(self, insert, input, before_prompt=False, *args, **kwargs):
978        """ A low-level method for appending content to the end of the buffer.
979
980        If 'before_prompt' is enabled, the content will be inserted before the
981        current prompt, if there is one.
982        """
983        # Determine where to insert the content.
984        cursor = self._control.textCursor()
985        if before_prompt and (self._reading or not self._executing):
986            self._flush_pending_stream()
987            cursor.setPosition(self._append_before_prompt_pos)
988        else:
989            if insert != self._insert_plain_text:
990                self._flush_pending_stream()
991            cursor.movePosition(QtGui.QTextCursor.End)
992
993        # Perform the insertion.
994        result = insert(cursor, input, *args, **kwargs)
995        return result
996
997    def _append_block(self, block_format=None, before_prompt=False):
998        """ Appends an new QTextBlock to the end of the console buffer.
999        """
1000        self._append_custom(self._insert_block, block_format, before_prompt)
1001
1002    def _append_html(self, html, before_prompt=False):
1003        """ Appends HTML at the end of the console buffer.
1004        """
1005        self._append_custom(self._insert_html, html, before_prompt)
1006
1007    def _append_html_fetching_plain_text(self, html, before_prompt=False):
1008        """ Appends HTML, then returns the plain text version of it.
1009        """
1010        return self._append_custom(self._insert_html_fetching_plain_text,
1011                                   html, before_prompt)
1012
1013    def _append_plain_text(self, text, before_prompt=False):
1014        """ Appends plain text, processing ANSI codes if enabled.
1015        """
1016        self._append_custom(self._insert_plain_text, text, before_prompt)
1017
1018    def _cancel_completion(self):
1019        """ If text completion is progress, cancel it.
1020        """
1021        self._completion_widget.cancel_completion()
1022
1023    def _clear_temporary_buffer(self):
1024        """ Clears the "temporary text" buffer, i.e. all the text following
1025            the prompt region.
1026        """
1027        # Select and remove all text below the input buffer.
1028        cursor = self._get_prompt_cursor()
1029        prompt = self._continuation_prompt.lstrip()
1030        if(self._temp_buffer_filled):
1031            self._temp_buffer_filled = False
1032            while cursor.movePosition(QtGui.QTextCursor.NextBlock):
1033                temp_cursor = QtGui.QTextCursor(cursor)
1034                temp_cursor.select(QtGui.QTextCursor.BlockUnderCursor)
1035                text = temp_cursor.selection().toPlainText().lstrip()
1036                if not text.startswith(prompt):
1037                    break
1038        else:
1039            # We've reached the end of the input buffer and no text follows.
1040            return
1041        cursor.movePosition(QtGui.QTextCursor.Left) # Grab the newline.
1042        cursor.movePosition(QtGui.QTextCursor.End,
1043                            QtGui.QTextCursor.KeepAnchor)
1044        cursor.removeSelectedText()
1045
1046        # After doing this, we have no choice but to clear the undo/redo
1047        # history. Otherwise, the text is not "temporary" at all, because it
1048        # can be recalled with undo/redo. Unfortunately, Qt does not expose
1049        # fine-grained control to the undo/redo system.
1050        if self._control.isUndoRedoEnabled():
1051            self._control.setUndoRedoEnabled(False)
1052            self._control.setUndoRedoEnabled(True)
1053
1054    def _complete_with_items(self, cursor, items):
1055        """ Performs completion with 'items' at the specified cursor location.
1056        """
1057        self._cancel_completion()
1058
1059        if len(items) == 1:
1060            cursor.setPosition(self._control.textCursor().position(),
1061                               QtGui.QTextCursor.KeepAnchor)
1062            cursor.insertText(items[0])
1063
1064        elif len(items) > 1:
1065            current_pos = self._control.textCursor().position()
1066            prefix = os.path.commonprefix(items)
1067            if prefix:
1068                cursor.setPosition(current_pos, QtGui.QTextCursor.KeepAnchor)
1069                cursor.insertText(prefix)
1070                current_pos = cursor.position()
1071
1072            self._completion_widget.show_items(cursor, items,
1073                                               prefix_length=len(prefix))
1074
1075    def _fill_temporary_buffer(self, cursor, text, html=False):
1076        """fill the area below the active editting zone with text"""
1077
1078        current_pos = self._control.textCursor().position()
1079
1080        cursor.beginEditBlock()
1081        self._append_plain_text('\n')
1082        self._page(text, html=html)
1083        cursor.endEditBlock()
1084
1085        cursor.setPosition(current_pos)
1086        self._control.moveCursor(QtGui.QTextCursor.End)
1087        self._control.setTextCursor(cursor)
1088
1089        self._temp_buffer_filled = True
1090
1091
1092    def _context_menu_make(self, pos):
1093        """ Creates a context menu for the given QPoint (in widget coordinates).
1094        """
1095        menu = QtWidgets.QMenu(self)
1096
1097        self.cut_action = menu.addAction('Cut', self.cut)
1098        self.cut_action.setEnabled(self.can_cut())
1099        self.cut_action.setShortcut(QtGui.QKeySequence.Cut)
1100
1101        self.copy_action = menu.addAction('Copy', self.copy)
1102        self.copy_action.setEnabled(self.can_copy())
1103        self.copy_action.setShortcut(QtGui.QKeySequence.Copy)
1104
1105        self.paste_action = menu.addAction('Paste', self.paste)
1106        self.paste_action.setEnabled(self.can_paste())
1107        self.paste_action.setShortcut(QtGui.QKeySequence.Paste)
1108
1109        anchor = self._control.anchorAt(pos)
1110        if anchor:
1111            menu.addSeparator()
1112            self.copy_link_action = menu.addAction(
1113                'Copy Link Address', lambda: self.copy_anchor(anchor=anchor))
1114            self.open_link_action = menu.addAction(
1115                'Open Link', lambda: self.open_anchor(anchor=anchor))
1116
1117        menu.addSeparator()
1118        menu.addAction(self.select_all_action)
1119
1120        menu.addSeparator()
1121        menu.addAction(self.export_action)
1122        menu.addAction(self.print_action)
1123
1124        return menu
1125
1126    def _control_key_down(self, modifiers, include_command=False):
1127        """ Given a KeyboardModifiers flags object, return whether the Control
1128        key is down.
1129
1130        Parameters
1131        ----------
1132        include_command : bool, optional (default True)
1133            Whether to treat the Command key as a (mutually exclusive) synonym
1134            for Control when in Mac OS.
1135        """
1136        # Note that on Mac OS, ControlModifier corresponds to the Command key
1137        # while MetaModifier corresponds to the Control key.
1138        if sys.platform == 'darwin':
1139            down = include_command and (modifiers & QtCore.Qt.ControlModifier)
1140            return bool(down) ^ bool(modifiers & QtCore.Qt.MetaModifier)
1141        else:
1142            return bool(modifiers & QtCore.Qt.ControlModifier)
1143
1144    def _create_control(self):
1145        """ Creates and connects the underlying text widget.
1146        """
1147        # Create the underlying control.
1148        if self.custom_control:
1149            control = self.custom_control()
1150        elif self.kind == 'plain':
1151            control = QtWidgets.QPlainTextEdit()
1152        elif self.kind == 'rich':
1153            control = QtWidgets.QTextEdit()
1154            control.setAcceptRichText(False)
1155            control.setMouseTracking(True)
1156
1157        # Prevent the widget from handling drops, as we already provide
1158        # the logic in this class.
1159        control.setAcceptDrops(False)
1160
1161        # Install event filters. The filter on the viewport is needed for
1162        # mouse events.
1163        control.installEventFilter(self)
1164        control.viewport().installEventFilter(self)
1165
1166        # Connect signals.
1167        control.customContextMenuRequested.connect(
1168            self._custom_context_menu_requested)
1169        control.copyAvailable.connect(self.copy_available)
1170        control.redoAvailable.connect(self.redo_available)
1171        control.undoAvailable.connect(self.undo_available)
1172
1173        # Hijack the document size change signal to prevent Qt from adjusting
1174        # the viewport's scrollbar. We are relying on an implementation detail
1175        # of Q(Plain)TextEdit here, which is potentially dangerous, but without
1176        # this functionality we cannot create a nice terminal interface.
1177        layout = control.document().documentLayout()
1178        layout.documentSizeChanged.disconnect()
1179        layout.documentSizeChanged.connect(self._adjust_scrollbars)
1180
1181        # Configure the scrollbar policy
1182        if self.scrollbar_visibility:
1183            scrollbar_policy = QtCore.Qt.ScrollBarAlwaysOn
1184        else :
1185            scrollbar_policy = QtCore.Qt.ScrollBarAlwaysOff
1186
1187        # Configure the control.
1188        control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
1189        control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
1190        control.setReadOnly(True)
1191        control.setUndoRedoEnabled(False)
1192        control.setVerticalScrollBarPolicy(scrollbar_policy)
1193        return control
1194
1195    def _create_page_control(self):
1196        """ Creates and connects the underlying paging widget.
1197        """
1198        if self.custom_page_control:
1199            control = self.custom_page_control()
1200        elif self.kind == 'plain':
1201            control = QtWidgets.QPlainTextEdit()
1202        elif self.kind == 'rich':
1203            control = QtWidgets.QTextEdit()
1204        control.installEventFilter(self)
1205        viewport = control.viewport()
1206        viewport.installEventFilter(self)
1207        control.setReadOnly(True)
1208        control.setUndoRedoEnabled(False)
1209
1210        # Configure the scrollbar policy
1211        if self.scrollbar_visibility:
1212            scrollbar_policy = QtCore.Qt.ScrollBarAlwaysOn
1213        else :
1214            scrollbar_policy = QtCore.Qt.ScrollBarAlwaysOff
1215
1216        control.setVerticalScrollBarPolicy(scrollbar_policy)
1217        return control
1218
1219    def _event_filter_console_keypress(self, event):
1220        """ Filter key events for the underlying text widget to create a
1221            console-like interface.
1222        """
1223        intercepted = False
1224        cursor = self._control.textCursor()
1225        position = cursor.position()
1226        key = event.key()
1227        ctrl_down = self._control_key_down(event.modifiers())
1228        alt_down = event.modifiers() & QtCore.Qt.AltModifier
1229        shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
1230
1231        cmd_down = (
1232            sys.platform == "darwin" and
1233            self._control_key_down(event.modifiers(), include_command=True)
1234        )
1235        if cmd_down:
1236            if key == QtCore.Qt.Key_Left:
1237                key = QtCore.Qt.Key_Home
1238            elif key == QtCore.Qt.Key_Right:
1239                key = QtCore.Qt.Key_End
1240            elif key == QtCore.Qt.Key_Up:
1241                ctrl_down = True
1242                key = QtCore.Qt.Key_Home
1243            elif key == QtCore.Qt.Key_Down:
1244                ctrl_down = True
1245                key = QtCore.Qt.Key_End
1246        #------ Special modifier logic -----------------------------------------
1247
1248        if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
1249            intercepted = True
1250
1251            # Special handling when tab completing in text mode.
1252            self._cancel_completion()
1253
1254            if self._in_buffer(position):
1255                # Special handling when a reading a line of raw input.
1256                if self._reading:
1257                    self._append_plain_text('\n')
1258                    self._reading = False
1259                    if self._reading_callback:
1260                        self._reading_callback()
1261
1262                # If the input buffer is a single line or there is only
1263                # whitespace after the cursor, execute. Otherwise, split the
1264                # line with a continuation prompt.
1265                elif not self._executing:
1266                    cursor.movePosition(QtGui.QTextCursor.End,
1267                                        QtGui.QTextCursor.KeepAnchor)
1268                    at_end = len(cursor.selectedText().strip()) == 0
1269                    single_line = (self._get_end_cursor().blockNumber() ==
1270                                   self._get_prompt_cursor().blockNumber())
1271                    if (at_end or shift_down or single_line) and not ctrl_down:
1272                        self.execute(interactive = not shift_down)
1273                    else:
1274                        # Do this inside an edit block for clean undo/redo.
1275                        pos = self._get_input_buffer_cursor_pos()
1276                        def callback(complete, indent):
1277                            try:
1278                                cursor.beginEditBlock()
1279                                cursor.setPosition(position)
1280                                cursor.insertText('\n')
1281                                self._insert_continuation_prompt(cursor)
1282                                if indent:
1283                                    cursor.insertText(indent)
1284                            finally:
1285                                cursor.endEditBlock()
1286
1287                            # Ensure that the whole input buffer is visible.
1288                            # FIXME: This will not be usable if the input buffer is
1289                            # taller than the console widget.
1290                            self._control.moveCursor(QtGui.QTextCursor.End)
1291                            self._control.setTextCursor(cursor)
1292                        self._register_is_complete_callback(
1293                            self._get_input_buffer()[:pos], callback)
1294
1295        #------ Control/Cmd modifier -------------------------------------------
1296
1297        elif ctrl_down:
1298            if key == QtCore.Qt.Key_G:
1299                self._keyboard_quit()
1300                intercepted = True
1301
1302            elif key == QtCore.Qt.Key_K:
1303                if self._in_buffer(position):
1304                    cursor.clearSelection()
1305                    cursor.movePosition(QtGui.QTextCursor.EndOfLine,
1306                                        QtGui.QTextCursor.KeepAnchor)
1307                    if not cursor.hasSelection():
1308                        # Line deletion (remove continuation prompt)
1309                        cursor.movePosition(QtGui.QTextCursor.NextBlock,
1310                                            QtGui.QTextCursor.KeepAnchor)
1311                        cursor.movePosition(QtGui.QTextCursor.Right,
1312                                            QtGui.QTextCursor.KeepAnchor,
1313                                            len(self._continuation_prompt))
1314                    self._kill_ring.kill_cursor(cursor)
1315                    self._set_cursor(cursor)
1316                intercepted = True
1317
1318            elif key == QtCore.Qt.Key_L:
1319                self.prompt_to_top()
1320                intercepted = True
1321
1322            elif key == QtCore.Qt.Key_O:
1323                if self._page_control and self._page_control.isVisible():
1324                    self._page_control.setFocus()
1325                intercepted = True
1326
1327            elif key == QtCore.Qt.Key_U:
1328                if self._in_buffer(position):
1329                    cursor.clearSelection()
1330                    start_line = cursor.blockNumber()
1331                    if start_line == self._get_prompt_cursor().blockNumber():
1332                        offset = len(self._prompt)
1333                    else:
1334                        offset = len(self._continuation_prompt)
1335                    cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1336                                        QtGui.QTextCursor.KeepAnchor)
1337                    cursor.movePosition(QtGui.QTextCursor.Right,
1338                                        QtGui.QTextCursor.KeepAnchor, offset)
1339                    self._kill_ring.kill_cursor(cursor)
1340                    self._set_cursor(cursor)
1341                intercepted = True
1342
1343            elif key == QtCore.Qt.Key_Y:
1344                self._keep_cursor_in_buffer()
1345                self._kill_ring.yank()
1346                intercepted = True
1347
1348            elif key in (QtCore.Qt.Key_Backspace, QtCore.Qt.Key_Delete):
1349                if key == QtCore.Qt.Key_Backspace:
1350                    cursor = self._get_word_start_cursor(position)
1351                else: # key == QtCore.Qt.Key_Delete
1352                    cursor = self._get_word_end_cursor(position)
1353                cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1354                self._kill_ring.kill_cursor(cursor)
1355                intercepted = True
1356
1357            elif key == QtCore.Qt.Key_D:
1358                if len(self.input_buffer) == 0 and not self._executing:
1359                    self.exit_requested.emit(self)
1360                # if executing and input buffer empty
1361                elif len(self._get_input_buffer(force=True)) == 0:
1362                    # input a EOT ansi control character
1363                    self._control.textCursor().insertText(chr(4))
1364                    new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1365                                                QtCore.Qt.Key_Return,
1366                                                QtCore.Qt.NoModifier)
1367                    QtWidgets.QApplication.instance().sendEvent(self._control, new_event)
1368                    intercepted = True
1369                else:
1370                    new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1371                                                QtCore.Qt.Key_Delete,
1372                                                QtCore.Qt.NoModifier)
1373                    QtWidgets.QApplication.instance().sendEvent(self._control, new_event)
1374                    intercepted = True
1375
1376            elif key == QtCore.Qt.Key_Down:
1377                self._scroll_to_end()
1378
1379            elif key == QtCore.Qt.Key_Up:
1380                self._control.verticalScrollBar().setValue(0)
1381        #------ Alt modifier ---------------------------------------------------
1382
1383        elif alt_down:
1384            if key == QtCore.Qt.Key_B:
1385                self._set_cursor(self._get_word_start_cursor(position))
1386                intercepted = True
1387
1388            elif key == QtCore.Qt.Key_F:
1389                self._set_cursor(self._get_word_end_cursor(position))
1390                intercepted = True
1391
1392            elif key == QtCore.Qt.Key_Y:
1393                self._kill_ring.rotate()
1394                intercepted = True
1395
1396            elif key == QtCore.Qt.Key_Backspace:
1397                cursor = self._get_word_start_cursor(position)
1398                cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1399                self._kill_ring.kill_cursor(cursor)
1400                intercepted = True
1401
1402            elif key == QtCore.Qt.Key_D:
1403                cursor = self._get_word_end_cursor(position)
1404                cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1405                self._kill_ring.kill_cursor(cursor)
1406                intercepted = True
1407
1408            elif key == QtCore.Qt.Key_Delete:
1409                intercepted = True
1410
1411            elif key == QtCore.Qt.Key_Greater:
1412                self._control.moveCursor(QtGui.QTextCursor.End)
1413                intercepted = True
1414
1415            elif key == QtCore.Qt.Key_Less:
1416                self._control.setTextCursor(self._get_prompt_cursor())
1417                intercepted = True
1418
1419        #------ No modifiers ---------------------------------------------------
1420
1421        else:
1422            self._trigger_is_complete_callback()
1423            if shift_down:
1424                anchormode = QtGui.QTextCursor.KeepAnchor
1425            else:
1426                anchormode = QtGui.QTextCursor.MoveAnchor
1427
1428            if key == QtCore.Qt.Key_Escape:
1429                self._keyboard_quit()
1430                intercepted = True
1431
1432            elif key == QtCore.Qt.Key_Up and not shift_down:
1433                if self._reading or not self._up_pressed(shift_down):
1434                    intercepted = True
1435                else:
1436                    prompt_line = self._get_prompt_cursor().blockNumber()
1437                    intercepted = cursor.blockNumber() <= prompt_line
1438
1439            elif key == QtCore.Qt.Key_Down and not shift_down:
1440                if self._reading or not self._down_pressed(shift_down):
1441                    intercepted = True
1442                else:
1443                    end_line = self._get_end_cursor().blockNumber()
1444                    intercepted = cursor.blockNumber() == end_line
1445
1446            elif key == QtCore.Qt.Key_Tab:
1447                if not self._reading:
1448                    if self._tab_pressed():
1449                        self._indent(dedent=False)
1450                    intercepted = True
1451
1452            elif key == QtCore.Qt.Key_Backtab:
1453                self._indent(dedent=True)
1454                intercepted = True
1455
1456            elif key == QtCore.Qt.Key_Left and not shift_down:
1457
1458                # Move to the previous line
1459                line, col = cursor.blockNumber(), cursor.columnNumber()
1460                if line > self._get_prompt_cursor().blockNumber() and \
1461                        col == len(self._continuation_prompt):
1462                    self._control.moveCursor(QtGui.QTextCursor.PreviousBlock,
1463                                             mode=anchormode)
1464                    self._control.moveCursor(QtGui.QTextCursor.EndOfBlock,
1465                                             mode=anchormode)
1466                    intercepted = True
1467
1468                # Regular left movement
1469                else:
1470                    intercepted = not self._in_buffer(position - 1)
1471
1472            elif key == QtCore.Qt.Key_Right and not shift_down:
1473                #original_block_number = cursor.blockNumber()
1474                if position == self._get_line_end_pos():
1475                    cursor.movePosition(QtGui.QTextCursor.NextBlock, mode=anchormode)
1476                    cursor.movePosition(QtGui.QTextCursor.Right,
1477                                        mode=anchormode,
1478                                        n=len(self._continuation_prompt))
1479                    self._control.setTextCursor(cursor)
1480                else:
1481                    self._control.moveCursor(QtGui.QTextCursor.Right,
1482                                             mode=anchormode)
1483                intercepted = True
1484
1485            elif key == QtCore.Qt.Key_Home:
1486                start_pos = self._get_line_start_pos()
1487
1488                c = self._get_cursor()
1489                spaces = self._get_leading_spaces()
1490                if (c.position() > start_pos + spaces or
1491                        c.columnNumber() == len(self._continuation_prompt)):
1492                    start_pos += spaces     # Beginning of text
1493
1494                if shift_down and self._in_buffer(position):
1495                    if c.selectedText():
1496                        sel_max = max(c.selectionStart(), c.selectionEnd())
1497                        cursor.setPosition(sel_max,
1498                                           QtGui.QTextCursor.MoveAnchor)
1499                    cursor.setPosition(start_pos, QtGui.QTextCursor.KeepAnchor)
1500                else:
1501                    cursor.setPosition(start_pos)
1502                self._set_cursor(cursor)
1503                intercepted = True
1504
1505            elif key == QtCore.Qt.Key_Backspace:
1506
1507                # Line deletion (remove continuation prompt)
1508                line, col = cursor.blockNumber(), cursor.columnNumber()
1509                if not self._reading and \
1510                        col == len(self._continuation_prompt) and \
1511                        line > self._get_prompt_cursor().blockNumber():
1512                    cursor.beginEditBlock()
1513                    cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1514                                        QtGui.QTextCursor.KeepAnchor)
1515                    cursor.removeSelectedText()
1516                    cursor.deletePreviousChar()
1517                    cursor.endEditBlock()
1518                    intercepted = True
1519
1520                # Regular backwards deletion
1521                else:
1522                    anchor = cursor.anchor()
1523                    if anchor == position:
1524                        intercepted = not self._in_buffer(position - 1)
1525                    else:
1526                        intercepted = not self._in_buffer(min(anchor, position))
1527
1528            elif key == QtCore.Qt.Key_Delete:
1529
1530                # Line deletion (remove continuation prompt)
1531                if not self._reading and self._in_buffer(position) and \
1532                        cursor.atBlockEnd() and not cursor.hasSelection():
1533                    cursor.movePosition(QtGui.QTextCursor.NextBlock,
1534                                        QtGui.QTextCursor.KeepAnchor)
1535                    cursor.movePosition(QtGui.QTextCursor.Right,
1536                                        QtGui.QTextCursor.KeepAnchor,
1537                                        len(self._continuation_prompt))
1538                    cursor.removeSelectedText()
1539                    intercepted = True
1540
1541                # Regular forwards deletion:
1542                else:
1543                    anchor = cursor.anchor()
1544                    intercepted = (not self._in_buffer(anchor) or
1545                                   not self._in_buffer(position))
1546
1547        #------ Special sequences ----------------------------------------------
1548
1549        if not intercepted:
1550            if event.matches(QtGui.QKeySequence.Copy):
1551                self.copy()
1552                intercepted = True
1553
1554            elif event.matches(QtGui.QKeySequence.Cut):
1555                self.cut()
1556                intercepted = True
1557
1558            elif event.matches(QtGui.QKeySequence.Paste):
1559                self.paste()
1560                intercepted = True
1561
1562        # Don't move the cursor if Control/Cmd is pressed to allow copy-paste
1563        # using the keyboard in any part of the buffer. Also, permit scrolling
1564        # with Page Up/Down keys. Finally, if we're executing, don't move the
1565        # cursor (if even this made sense, we can't guarantee that the prompt
1566        # position is still valid due to text truncation).
1567        if not (self._control_key_down(event.modifiers(), include_command=True)
1568                or key in (QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown)
1569                or (self._executing and not self._reading)
1570                or (event.text() == "" and not
1571                    (not shift_down and key in (QtCore.Qt.Key_Up, QtCore.Qt.Key_Down)))):
1572            self._keep_cursor_in_buffer()
1573
1574        return intercepted
1575
1576    def _event_filter_page_keypress(self, event):
1577        """ Filter key events for the paging widget to create console-like
1578            interface.
1579        """
1580        key = event.key()
1581        ctrl_down = self._control_key_down(event.modifiers())
1582        alt_down = event.modifiers() & QtCore.Qt.AltModifier
1583
1584        if ctrl_down:
1585            if key == QtCore.Qt.Key_O:
1586                self._control.setFocus()
1587                return True
1588
1589        elif alt_down:
1590            if key == QtCore.Qt.Key_Greater:
1591                self._page_control.moveCursor(QtGui.QTextCursor.End)
1592                return True
1593
1594            elif key == QtCore.Qt.Key_Less:
1595                self._page_control.moveCursor(QtGui.QTextCursor.Start)
1596                return True
1597
1598        elif key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
1599            if self._splitter:
1600                self._page_control.hide()
1601                self._control.setFocus()
1602            else:
1603                self.layout().setCurrentWidget(self._control)
1604                # re-enable buffer truncation after paging
1605                self._control.document().setMaximumBlockCount(self.buffer_size)
1606            return True
1607
1608        elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return,
1609                     QtCore.Qt.Key_Tab):
1610            new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1611                                        QtCore.Qt.Key_PageDown,
1612                                        QtCore.Qt.NoModifier)
1613            QtWidgets.QApplication.instance().sendEvent(self._page_control, new_event)
1614            return True
1615
1616        elif key == QtCore.Qt.Key_Backspace:
1617            new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1618                                        QtCore.Qt.Key_PageUp,
1619                                        QtCore.Qt.NoModifier)
1620            QtWidgets.QApplication.instance().sendEvent(self._page_control, new_event)
1621            return True
1622
1623        # vi/less -like key bindings
1624        elif key == QtCore.Qt.Key_J:
1625            new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1626                                        QtCore.Qt.Key_Down,
1627                                        QtCore.Qt.NoModifier)
1628            QtWidgets.QApplication.instance().sendEvent(self._page_control, new_event)
1629            return True
1630
1631        # vi/less -like key bindings
1632        elif key == QtCore.Qt.Key_K:
1633            new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1634                                        QtCore.Qt.Key_Up,
1635                                        QtCore.Qt.NoModifier)
1636            QtWidgets.QApplication.instance().sendEvent(self._page_control, new_event)
1637            return True
1638
1639        return False
1640
1641    def _on_flush_pending_stream_timer(self):
1642        """ Flush the pending stream output and change the
1643        prompt position appropriately.
1644        """
1645        cursor = self._control.textCursor()
1646        cursor.movePosition(QtGui.QTextCursor.End)
1647        self._flush_pending_stream()
1648        cursor.movePosition(QtGui.QTextCursor.End)
1649
1650    def _flush_pending_stream(self):
1651        """ Flush out pending text into the widget. """
1652        text = self._pending_insert_text
1653        self._pending_insert_text = []
1654        buffer_size = self._control.document().maximumBlockCount()
1655        if buffer_size > 0:
1656            text = self._get_last_lines_from_list(text, buffer_size)
1657        text = ''.join(text)
1658        t = time.time()
1659        self._insert_plain_text(self._get_end_cursor(), text, flush=True)
1660        # Set the flush interval to equal the maximum time to update text.
1661        self._pending_text_flush_interval.setInterval(max(100,
1662                                                 (time.time()-t)*1000))
1663
1664    def _format_as_columns(self, items, separator='  '):
1665        """ Transform a list of strings into a single string with columns.
1666
1667        Parameters
1668        ----------
1669        items : sequence of strings
1670            The strings to process.
1671
1672        separator : str, optional [default is two spaces]
1673            The string that separates columns.
1674
1675        Returns
1676        -------
1677        The formatted string.
1678        """
1679        # Calculate the number of characters available.
1680        width = self._control.document().textWidth()
1681        char_width = self._get_font_width()
1682        displaywidth = max(10, (width / char_width) - 1)
1683
1684        return columnize(items, separator, displaywidth)
1685
1686    def _get_block_plain_text(self, block):
1687        """ Given a QTextBlock, return its unformatted text.
1688        """
1689        cursor = QtGui.QTextCursor(block)
1690        cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1691        cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
1692                            QtGui.QTextCursor.KeepAnchor)
1693        return cursor.selection().toPlainText()
1694
1695    def _get_cursor(self):
1696        """ Get a cursor at the current insert position.
1697        """
1698        return self._control.textCursor()
1699
1700    def _get_end_cursor(self):
1701        """ Get a cursor at the last character of the current cell.
1702        """
1703        cursor = self._control.textCursor()
1704        cursor.movePosition(QtGui.QTextCursor.End)
1705        return cursor
1706
1707    def _get_end_pos(self):
1708        """ Get the position of the last character of the current cell.
1709        """
1710        return self._get_end_cursor().position()
1711
1712    def _get_line_start_cursor(self):
1713        """ Get a cursor at the first character of the current line.
1714        """
1715        cursor = self._control.textCursor()
1716        start_line = cursor.blockNumber()
1717        if start_line == self._get_prompt_cursor().blockNumber():
1718            cursor.setPosition(self._prompt_pos)
1719        else:
1720            cursor.movePosition(QtGui.QTextCursor.StartOfLine)
1721            cursor.setPosition(cursor.position() +
1722                               len(self._continuation_prompt))
1723        return cursor
1724
1725    def _get_line_start_pos(self):
1726        """ Get the position of the first character of the current line.
1727        """
1728        return self._get_line_start_cursor().position()
1729
1730    def _get_line_end_cursor(self):
1731        """ Get a cursor at the last character of the current line.
1732        """
1733        cursor = self._control.textCursor()
1734        cursor.movePosition(QtGui.QTextCursor.EndOfLine)
1735        return cursor
1736
1737    def _get_line_end_pos(self):
1738        """ Get the position of the last character of the current line.
1739        """
1740        return self._get_line_end_cursor().position()
1741
1742    def _get_input_buffer_cursor_column(self):
1743        """ Get the column of the cursor in the input buffer, excluding the
1744            contribution by the prompt, or -1 if there is no such column.
1745        """
1746        prompt = self._get_input_buffer_cursor_prompt()
1747        if prompt is None:
1748            return -1
1749        else:
1750            cursor = self._control.textCursor()
1751            return cursor.columnNumber() - len(prompt)
1752
1753    def _get_input_buffer_cursor_line(self):
1754        """ Get the text of the line of the input buffer that contains the
1755            cursor, or None if there is no such line.
1756        """
1757        prompt = self._get_input_buffer_cursor_prompt()
1758        if prompt is None:
1759            return None
1760        else:
1761            cursor = self._control.textCursor()
1762            text = self._get_block_plain_text(cursor.block())
1763            return text[len(prompt):]
1764
1765    def _get_input_buffer_cursor_pos(self):
1766        """Get the cursor position within the input buffer."""
1767        cursor = self._control.textCursor()
1768        cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
1769        input_buffer = cursor.selection().toPlainText()
1770
1771        # Don't count continuation prompts
1772        return len(input_buffer.replace('\n' + self._continuation_prompt, '\n'))
1773
1774    def _get_input_buffer_cursor_prompt(self):
1775        """ Returns the (plain text) prompt for line of the input buffer that
1776            contains the cursor, or None if there is no such line.
1777        """
1778        if self._executing:
1779            return None
1780        cursor = self._control.textCursor()
1781        if cursor.position() >= self._prompt_pos:
1782            if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
1783                return self._prompt
1784            else:
1785                return self._continuation_prompt
1786        else:
1787            return None
1788
1789    def _get_last_lines(self, text, num_lines, return_count=False):
1790        """ Get the last specified number of lines of text (like `tail -n`).
1791        If return_count is True, returns a tuple of clipped text and the
1792        number of lines in the clipped text.
1793        """
1794        pos = len(text)
1795        if pos < num_lines:
1796            if return_count:
1797                return text, text.count('\n') if return_count else text
1798            else:
1799                return text
1800        i = 0
1801        while i < num_lines:
1802            pos = text.rfind('\n', None, pos)
1803            if pos == -1:
1804                pos = None
1805                break
1806            i += 1
1807        if return_count:
1808            return text[pos:], i
1809        else:
1810            return text[pos:]
1811
1812    def _get_last_lines_from_list(self, text_list, num_lines):
1813        """ Get the list of text clipped to last specified lines.
1814        """
1815        ret = []
1816        lines_pending = num_lines
1817        for text in reversed(text_list):
1818            text, lines_added = self._get_last_lines(text, lines_pending,
1819                                                     return_count=True)
1820            ret.append(text)
1821            lines_pending -= lines_added
1822            if lines_pending <= 0:
1823                break
1824        return ret[::-1]
1825
1826    def _get_leading_spaces(self):
1827        """ Get the number of leading spaces of the current line.
1828        """
1829
1830        cursor = self._get_cursor()
1831        start_line = cursor.blockNumber()
1832        if start_line == self._get_prompt_cursor().blockNumber():
1833            # first line
1834            offset = len(self._prompt)
1835        else:
1836            # continuation
1837            offset = len(self._continuation_prompt)
1838        cursor.select(QtGui.QTextCursor.LineUnderCursor)
1839        text = cursor.selectedText()[offset:]
1840        return len(text) - len(text.lstrip())
1841
1842    @property
1843    def _prompt_pos(self):
1844        """ Find the position in the text right after the prompt.
1845        """
1846        return min(self._prompt_cursor.position() + 1, self._get_end_pos())
1847
1848    @property
1849    def _append_before_prompt_pos(self):
1850        """ Find the position in the text right before the prompt.
1851        """
1852        return min(self._append_before_prompt_cursor.position(),
1853                   self._get_end_pos())
1854
1855    def _get_prompt_cursor(self):
1856        """ Get a cursor at the prompt position of the current cell.
1857        """
1858        cursor = self._control.textCursor()
1859        cursor.setPosition(self._prompt_pos)
1860        return cursor
1861
1862    def _get_selection_cursor(self, start, end):
1863        """ Get a cursor with text selected between the positions 'start' and
1864            'end'.
1865        """
1866        cursor = self._control.textCursor()
1867        cursor.setPosition(start)
1868        cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
1869        return cursor
1870
1871    def _get_word_start_cursor(self, position):
1872        """ Find the start of the word to the left the given position. If a
1873            sequence of non-word characters precedes the first word, skip over
1874            them. (This emulates the behavior of bash, emacs, etc.)
1875        """
1876        document = self._control.document()
1877        cursor = self._control.textCursor()
1878        line_start_pos = self._get_line_start_pos()
1879
1880        if position == self._prompt_pos:
1881            return cursor
1882        elif position == line_start_pos:
1883            # Cursor is at the beginning of a line, move to the last
1884            # non-whitespace character of the previous line
1885            cursor = self._control.textCursor()
1886            cursor.setPosition(position)
1887            cursor.movePosition(QtGui.QTextCursor.PreviousBlock)
1888            cursor.movePosition(QtGui.QTextCursor.EndOfBlock)
1889            position = cursor.position()
1890            while (
1891                position >= self._prompt_pos and
1892                is_whitespace(document.characterAt(position))
1893            ):
1894                position -= 1
1895            cursor.setPosition(position + 1)
1896        else:
1897            position -= 1
1898
1899            # Find the last alphanumeric char, but don't move across lines
1900            while (
1901                position >= self._prompt_pos and
1902                position >= line_start_pos and
1903                not is_letter_or_number(document.characterAt(position))
1904            ):
1905                position -= 1
1906
1907            # Find the first alphanumeric char, but don't move across lines
1908            while (
1909                position >= self._prompt_pos and
1910                position >= line_start_pos and
1911                is_letter_or_number(document.characterAt(position))
1912            ):
1913                position -= 1
1914
1915            cursor.setPosition(position + 1)
1916
1917        return cursor
1918
1919    def _get_word_end_cursor(self, position):
1920        """ Find the end of the word to the right the given position. If a
1921            sequence of non-word characters precedes the first word, skip over
1922            them. (This emulates the behavior of bash, emacs, etc.)
1923        """
1924        document = self._control.document()
1925        cursor = self._control.textCursor()
1926        end_pos = self._get_end_pos()
1927        line_end_pos = self._get_line_end_pos()
1928
1929        if position == end_pos:
1930            # Cursor is at the very end of the buffer
1931            return cursor
1932        elif position == line_end_pos:
1933            # Cursor is at the end of a line, move to the first
1934            # non-whitespace character of the next line
1935            cursor = self._control.textCursor()
1936            cursor.setPosition(position)
1937            cursor.movePosition(QtGui.QTextCursor.NextBlock)
1938            position = cursor.position() + len(self._continuation_prompt)
1939            while (
1940                position < end_pos and
1941                is_whitespace(document.characterAt(position))
1942            ):
1943                position += 1
1944            cursor.setPosition(position)
1945        else:
1946            if is_whitespace(document.characterAt(position)):
1947                # The next character is whitespace. If this is part of
1948                # indentation whitespace, skip to the first non-whitespace
1949                # character.
1950                is_indentation_whitespace = True
1951                back_pos = position - 1
1952                line_start_pos = self._get_line_start_pos()
1953                while back_pos >= line_start_pos:
1954                    if not is_whitespace(document.characterAt(back_pos)):
1955                        is_indentation_whitespace = False
1956                        break
1957                    back_pos -= 1
1958                if is_indentation_whitespace:
1959                    # Skip to the first non-whitespace character
1960                    while (
1961                        position < end_pos and
1962                        position < line_end_pos and
1963                        is_whitespace(document.characterAt(position))
1964                    ):
1965                        position += 1
1966                    cursor.setPosition(position)
1967                    return cursor
1968
1969            while (
1970                position < end_pos and
1971                position < line_end_pos and
1972                not is_letter_or_number(document.characterAt(position))
1973            ):
1974                position += 1
1975
1976            while (
1977                position < end_pos and
1978                position < line_end_pos and
1979                is_letter_or_number(document.characterAt(position))
1980            ):
1981                position += 1
1982
1983            cursor.setPosition(position)
1984        return cursor
1985
1986    def _indent(self, dedent=True):
1987        """ Indent/Dedent current line or current text selection.
1988        """
1989        num_newlines = self._get_cursor().selectedText().count("\u2029")
1990        save_cur = self._get_cursor()
1991        cur = self._get_cursor()
1992
1993        # move to first line of selection, if present
1994        cur.setPosition(cur.selectionStart())
1995        self._control.setTextCursor(cur)
1996        spaces = self._get_leading_spaces()
1997        # calculate number of spaces neded to align/indent to 4-space multiple
1998        step = self._tab_width - (spaces % self._tab_width)
1999
2000        # insertText shouldn't replace if selection is active
2001        cur.clearSelection()
2002
2003        # indent all lines in selection (ir just current) by `step`
2004        for _ in range(num_newlines+1):
2005            # update underlying cursor for _get_line_start_pos
2006            self._control.setTextCursor(cur)
2007            # move to first non-ws char on line
2008            cur.setPosition(self._get_line_start_pos())
2009            if dedent:
2010                spaces = min(step, self._get_leading_spaces())
2011                safe_step = spaces % self._tab_width
2012                cur.movePosition(QtGui.QTextCursor.Right,
2013                                 QtGui.QTextCursor.KeepAnchor,
2014                                 min(spaces, safe_step if safe_step != 0
2015                                    else self._tab_width))
2016                cur.removeSelectedText()
2017            else:
2018                cur.insertText(' '*step)
2019            cur.movePosition(QtGui.QTextCursor.Down)
2020
2021        # restore cursor
2022        self._control.setTextCursor(save_cur)
2023
2024    def _insert_continuation_prompt(self, cursor, indent=''):
2025        """ Inserts new continuation prompt using the specified cursor.
2026        """
2027        if self._continuation_prompt_html is None:
2028            self._insert_plain_text(cursor, self._continuation_prompt)
2029        else:
2030            self._continuation_prompt = self._insert_html_fetching_plain_text(
2031                cursor, self._continuation_prompt_html)
2032        if indent:
2033            cursor.insertText(indent)
2034
2035    def _insert_block(self, cursor, block_format=None):
2036        """ Inserts an empty QTextBlock using the specified cursor.
2037        """
2038        if block_format is None:
2039            block_format = QtGui.QTextBlockFormat()
2040        cursor.insertBlock(block_format)
2041
2042    def _insert_html(self, cursor, html):
2043        """ Inserts HTML using the specified cursor in such a way that future
2044            formatting is unaffected.
2045        """
2046        cursor.beginEditBlock()
2047        cursor.insertHtml(html)
2048
2049        # After inserting HTML, the text document "remembers" it's in "html
2050        # mode", which means that subsequent calls adding plain text will result
2051        # in unwanted formatting, lost tab characters, etc. The following code
2052        # hacks around this behavior, which I consider to be a bug in Qt, by
2053        # (crudely) resetting the document's style state.
2054        cursor.movePosition(QtGui.QTextCursor.Left,
2055                            QtGui.QTextCursor.KeepAnchor)
2056        if cursor.selection().toPlainText() == ' ':
2057            cursor.removeSelectedText()
2058        else:
2059            cursor.movePosition(QtGui.QTextCursor.Right)
2060        cursor.insertText(' ', QtGui.QTextCharFormat())
2061        cursor.endEditBlock()
2062
2063    def _insert_html_fetching_plain_text(self, cursor, html):
2064        """ Inserts HTML using the specified cursor, then returns its plain text
2065            version.
2066        """
2067        cursor.beginEditBlock()
2068        cursor.removeSelectedText()
2069
2070        start = cursor.position()
2071        self._insert_html(cursor, html)
2072        end = cursor.position()
2073        cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
2074        text = cursor.selection().toPlainText()
2075
2076        cursor.setPosition(end)
2077        cursor.endEditBlock()
2078        return text
2079
2080    def _viewport_at_end(self):
2081        """Check if the viewport is at the end of the document."""
2082        viewport = self._control.viewport()
2083        end_scroll_pos = self._control.cursorForPosition(
2084            QtCore.QPoint(viewport.width() - 1, viewport.height() - 1)
2085            ).position()
2086        end_doc_pos = self._get_end_pos()
2087        return end_doc_pos - end_scroll_pos <= 1
2088
2089    def _scroll_to_end(self):
2090        """Scroll to the end of the document."""
2091        end_scroll = (self._control.verticalScrollBar().maximum()
2092                      - self._control.verticalScrollBar().pageStep())
2093        # Only scroll down
2094        if end_scroll > self._control.verticalScrollBar().value():
2095            self._control.verticalScrollBar().setValue(end_scroll)
2096
2097    def _insert_plain_text(self, cursor, text, flush=False):
2098        """ Inserts plain text using the specified cursor, processing ANSI codes
2099            if enabled.
2100        """
2101        should_autoscroll = self._viewport_at_end()
2102        # maximumBlockCount() can be different from self.buffer_size in
2103        # case input prompt is active.
2104        buffer_size = self._control.document().maximumBlockCount()
2105
2106        if (self._executing and not flush and
2107                self._pending_text_flush_interval.isActive() and
2108                cursor.position() == self._get_end_pos()):
2109            # Queue the text to insert in case it is being inserted at end
2110            self._pending_insert_text.append(text)
2111            if buffer_size > 0:
2112                self._pending_insert_text = self._get_last_lines_from_list(
2113                                        self._pending_insert_text, buffer_size)
2114            return
2115
2116        if self._executing and not self._pending_text_flush_interval.isActive():
2117            self._pending_text_flush_interval.start()
2118
2119        # Clip the text to last `buffer_size` lines.
2120        if buffer_size > 0:
2121            text = self._get_last_lines(text, buffer_size)
2122
2123        cursor.beginEditBlock()
2124        if self.ansi_codes:
2125            for substring in self._ansi_processor.split_string(text):
2126                for act in self._ansi_processor.actions:
2127
2128                    # Unlike real terminal emulators, we don't distinguish
2129                    # between the screen and the scrollback buffer. A screen
2130                    # erase request clears everything.
2131                    if act.action == 'erase' and act.area == 'screen':
2132                        cursor.select(QtGui.QTextCursor.Document)
2133                        cursor.removeSelectedText()
2134
2135                    # Simulate a form feed by scrolling just past the last line.
2136                    elif act.action == 'scroll' and act.unit == 'page':
2137                        cursor.insertText('\n')
2138                        cursor.endEditBlock()
2139                        self._set_top_cursor(cursor)
2140                        cursor.joinPreviousEditBlock()
2141                        cursor.deletePreviousChar()
2142
2143                        if os.name == 'nt':
2144                            cursor.select(QtGui.QTextCursor.Document)
2145                            cursor.removeSelectedText()
2146
2147                    elif act.action == 'carriage-return':
2148                        cursor.movePosition(
2149                            cursor.StartOfLine, cursor.KeepAnchor)
2150
2151                    elif act.action == 'beep':
2152                        QtWidgets.QApplication.instance().beep()
2153
2154                    elif act.action == 'backspace':
2155                        if not cursor.atBlockStart():
2156                            cursor.movePosition(
2157                                cursor.PreviousCharacter, cursor.KeepAnchor)
2158
2159                    elif act.action == 'newline':
2160                        cursor.movePosition(cursor.EndOfLine)
2161
2162                format = self._ansi_processor.get_format()
2163
2164                selection = cursor.selectedText()
2165                if len(selection) == 0:
2166                    cursor.insertText(substring, format)
2167                elif substring is not None:
2168                    # BS and CR are treated as a change in print
2169                    # position, rather than a backwards character
2170                    # deletion for output equivalence with (I)Python
2171                    # terminal.
2172                    if len(substring) >= len(selection):
2173                        cursor.insertText(substring, format)
2174                    else:
2175                        old_text = selection[len(substring):]
2176                        cursor.insertText(substring + old_text, format)
2177                        cursor.movePosition(cursor.PreviousCharacter,
2178                               cursor.KeepAnchor, len(old_text))
2179        else:
2180            cursor.insertText(text)
2181        cursor.endEditBlock()
2182
2183        if should_autoscroll:
2184            self._scroll_to_end()
2185
2186    def _insert_plain_text_into_buffer(self, cursor, text):
2187        """ Inserts text into the input buffer using the specified cursor (which
2188            must be in the input buffer), ensuring that continuation prompts are
2189            inserted as necessary.
2190        """
2191        lines = text.splitlines(True)
2192        if lines:
2193            if lines[-1].endswith('\n'):
2194                # If the text ends with a newline, add a blank line so a new
2195                # continuation prompt is produced.
2196                lines.append('')
2197            cursor.beginEditBlock()
2198            cursor.insertText(lines[0])
2199            for line in lines[1:]:
2200                if self._continuation_prompt_html is None:
2201                    cursor.insertText(self._continuation_prompt)
2202                else:
2203                    self._continuation_prompt = \
2204                        self._insert_html_fetching_plain_text(
2205                            cursor, self._continuation_prompt_html)
2206                cursor.insertText(line)
2207            cursor.endEditBlock()
2208
2209    def _in_buffer(self, position):
2210        """
2211        Returns whether the specified position is inside the editing region.
2212        """
2213        return position == self._move_position_in_buffer(position)
2214
2215    def _move_position_in_buffer(self, position):
2216        """
2217        Return the next position in buffer.
2218        """
2219        cursor = self._control.textCursor()
2220        cursor.setPosition(position)
2221        line = cursor.blockNumber()
2222        prompt_line = self._get_prompt_cursor().blockNumber()
2223        if line == prompt_line:
2224            if position >= self._prompt_pos:
2225                return position
2226            return self._prompt_pos
2227        if line > prompt_line:
2228            cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
2229            prompt_pos = cursor.position() + len(self._continuation_prompt)
2230            if position >= prompt_pos:
2231                return position
2232            return prompt_pos
2233        return self._prompt_pos
2234
2235    def _keep_cursor_in_buffer(self):
2236        """ Ensures that the cursor is inside the editing region. Returns
2237            whether the cursor was moved.
2238        """
2239        cursor = self._control.textCursor()
2240        endpos = cursor.selectionEnd()
2241
2242        if endpos < self._prompt_pos:
2243            cursor.setPosition(endpos)
2244            line = cursor.blockNumber()
2245            prompt_line = self._get_prompt_cursor().blockNumber()
2246            if line == prompt_line:
2247                # Cursor is on prompt line, move to start of buffer
2248                cursor.setPosition(self._prompt_pos)
2249            else:
2250                # Cursor is not in buffer, move to the end
2251                cursor.movePosition(QtGui.QTextCursor.End)
2252            self._control.setTextCursor(cursor)
2253            return True
2254
2255        startpos = cursor.selectionStart()
2256
2257        new_endpos = self._move_position_in_buffer(endpos)
2258        new_startpos = self._move_position_in_buffer(startpos)
2259        if new_endpos == endpos and new_startpos == startpos:
2260            return False
2261
2262        cursor.setPosition(new_startpos)
2263        cursor.setPosition(new_endpos, QtGui.QTextCursor.KeepAnchor)
2264        self._control.setTextCursor(cursor)
2265        return True
2266
2267    def _keyboard_quit(self):
2268        """ Cancels the current editing task ala Ctrl-G in Emacs.
2269        """
2270        if self._temp_buffer_filled :
2271            self._cancel_completion()
2272            self._clear_temporary_buffer()
2273        else:
2274            self.input_buffer = ''
2275
2276    def _page(self, text, html=False):
2277        """ Displays text using the pager if it exceeds the height of the
2278        viewport.
2279
2280        Parameters
2281        ----------
2282        html : bool, optional (default False)
2283            If set, the text will be interpreted as HTML instead of plain text.
2284        """
2285        line_height = QtGui.QFontMetrics(self.font).height()
2286        minlines = self._control.viewport().height() / line_height
2287        if self.paging != 'none' and \
2288                re.match("(?:[^\n]*\n){%i}" % minlines, text):
2289            if self.paging == 'custom':
2290                self.custom_page_requested.emit(text)
2291            else:
2292                # disable buffer truncation during paging
2293                self._control.document().setMaximumBlockCount(0)
2294                self._page_control.clear()
2295                cursor = self._page_control.textCursor()
2296                if html:
2297                    self._insert_html(cursor, text)
2298                else:
2299                    self._insert_plain_text(cursor, text)
2300                self._page_control.moveCursor(QtGui.QTextCursor.Start)
2301
2302                self._page_control.viewport().resize(self._control.size())
2303                if self._splitter:
2304                    self._page_control.show()
2305                    self._page_control.setFocus()
2306                else:
2307                    self.layout().setCurrentWidget(self._page_control)
2308        elif html:
2309            self._append_html(text)
2310        else:
2311            self._append_plain_text(text)
2312
2313    def _set_paging(self, paging):
2314        """
2315        Change the pager to `paging` style.
2316
2317        Parameters
2318        ----------
2319        paging : string
2320            Either "hsplit", "vsplit", or "inside"
2321        """
2322        if self._splitter is None:
2323            raise NotImplementedError("""can only switch if --paging=hsplit or
2324                    --paging=vsplit is used.""")
2325        if paging == 'hsplit':
2326            self._splitter.setOrientation(QtCore.Qt.Horizontal)
2327        elif paging == 'vsplit':
2328            self._splitter.setOrientation(QtCore.Qt.Vertical)
2329        elif paging == 'inside':
2330            raise NotImplementedError("""switching to 'inside' paging not
2331                    supported yet.""")
2332        else:
2333            raise ValueError("unknown paging method '%s'" % paging)
2334        self.paging = paging
2335
2336    def _prompt_finished(self):
2337        """ Called immediately after a prompt is finished, i.e. when some input
2338            will be processed and a new prompt displayed.
2339        """
2340        self._control.setReadOnly(True)
2341        self._prompt_finished_hook()
2342
2343    def _prompt_started(self):
2344        """ Called immediately after a new prompt is displayed.
2345        """
2346        # Temporarily disable the maximum block count to permit undo/redo and
2347        # to ensure that the prompt position does not change due to truncation.
2348        self._control.document().setMaximumBlockCount(0)
2349        self._control.setUndoRedoEnabled(True)
2350
2351        # Work around bug in QPlainTextEdit: input method is not re-enabled
2352        # when read-only is disabled.
2353        self._control.setReadOnly(False)
2354        self._control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
2355
2356        if not self._reading:
2357            self._executing = False
2358        self._prompt_started_hook()
2359
2360        # If the input buffer has changed while executing, load it.
2361        if self._input_buffer_pending:
2362            self.input_buffer = self._input_buffer_pending
2363            self._input_buffer_pending = ''
2364
2365        self._control.moveCursor(QtGui.QTextCursor.End)
2366
2367    def _readline(self, prompt='', callback=None, password=False):
2368        """ Reads one line of input from the user.
2369
2370        Parameters
2371        ----------
2372        prompt : str, optional
2373            The prompt to print before reading the line.
2374
2375        callback : callable, optional
2376            A callback to execute with the read line. If not specified, input is
2377            read *synchronously* and this method does not return until it has
2378            been read.
2379
2380        Returns
2381        -------
2382        If a callback is specified, returns nothing. Otherwise, returns the
2383        input string with the trailing newline stripped.
2384        """
2385        if self._reading:
2386            raise RuntimeError('Cannot read a line. Widget is already reading.')
2387
2388        if not callback and not self.isVisible():
2389            # If the user cannot see the widget, this function cannot return.
2390            raise RuntimeError('Cannot synchronously read a line if the widget '
2391                               'is not visible!')
2392
2393        self._reading = True
2394        if password:
2395            self._show_prompt('Warning: QtConsole does not support password mode, '\
2396                              'the text you type will be visible.', newline=True)
2397        self._show_prompt(prompt, newline=False)
2398
2399        if callback is None:
2400            self._reading_callback = None
2401            while self._reading:
2402                QtCore.QCoreApplication.processEvents()
2403            return self._get_input_buffer(force=True).rstrip('\n')
2404
2405        else:
2406            self._reading_callback = lambda: \
2407                callback(self._get_input_buffer(force=True).rstrip('\n'))
2408
2409    def _set_continuation_prompt(self, prompt, html=False):
2410        """ Sets the continuation prompt.
2411
2412        Parameters
2413        ----------
2414        prompt : str
2415            The prompt to show when more input is needed.
2416
2417        html : bool, optional (default False)
2418            If set, the prompt will be inserted as formatted HTML. Otherwise,
2419            the prompt will be treated as plain text, though ANSI color codes
2420            will be handled.
2421        """
2422        if html:
2423            self._continuation_prompt_html = prompt
2424        else:
2425            self._continuation_prompt = prompt
2426            self._continuation_prompt_html = None
2427
2428    def _set_cursor(self, cursor):
2429        """ Convenience method to set the current cursor.
2430        """
2431        self._control.setTextCursor(cursor)
2432
2433    def _set_top_cursor(self, cursor):
2434        """ Scrolls the viewport so that the specified cursor is at the top.
2435        """
2436        scrollbar = self._control.verticalScrollBar()
2437        scrollbar.setValue(scrollbar.maximum())
2438        original_cursor = self._control.textCursor()
2439        self._control.setTextCursor(cursor)
2440        self._control.ensureCursorVisible()
2441        self._control.setTextCursor(original_cursor)
2442
2443    def _show_prompt(self, prompt=None, html=False, newline=True):
2444        """ Writes a new prompt at the end of the buffer.
2445
2446        Parameters
2447        ----------
2448        prompt : str, optional
2449            The prompt to show. If not specified, the previous prompt is used.
2450
2451        html : bool, optional (default False)
2452            Only relevant when a prompt is specified. If set, the prompt will
2453            be inserted as formatted HTML. Otherwise, the prompt will be treated
2454            as plain text, though ANSI color codes will be handled.
2455
2456        newline : bool, optional (default True)
2457            If set, a new line will be written before showing the prompt if
2458            there is not already a newline at the end of the buffer.
2459        """
2460        self._flush_pending_stream()
2461        cursor = self._get_end_cursor()
2462
2463        # Save the current position to support _append*(before_prompt=True).
2464        # We can't leave the cursor at the end of the document though, because
2465        # that would cause any further additions to move the cursor. Therefore,
2466        # we move it back one place and move it forward again at the end of
2467        # this method. However, we only do this if the cursor isn't already
2468        # at the start of the text.
2469        if cursor.position() == 0:
2470            move_forward = False
2471        else:
2472            move_forward = True
2473            self._append_before_prompt_cursor.setPosition(cursor.position() - 1)
2474
2475        # Insert a preliminary newline, if necessary.
2476        if newline and cursor.position() > 0:
2477            cursor.movePosition(QtGui.QTextCursor.Left,
2478                                QtGui.QTextCursor.KeepAnchor)
2479            if cursor.selection().toPlainText() != '\n':
2480                self._append_block()
2481
2482        # Write the prompt.
2483        self._append_plain_text(self._prompt_sep)
2484        if prompt is None:
2485            if self._prompt_html is None:
2486                self._append_plain_text(self._prompt)
2487            else:
2488                self._append_html(self._prompt_html)
2489        else:
2490            if html:
2491                self._prompt = self._append_html_fetching_plain_text(prompt)
2492                self._prompt_html = prompt
2493            else:
2494                self._append_plain_text(prompt)
2495                self._prompt = prompt
2496                self._prompt_html = None
2497
2498        self._flush_pending_stream()
2499        self._prompt_cursor.setPosition(self._get_end_pos() - 1)
2500
2501        if move_forward:
2502            self._append_before_prompt_cursor.setPosition(
2503                self._append_before_prompt_cursor.position() + 1)
2504        self._prompt_started()
2505
2506    #------ Signal handlers ----------------------------------------------------
2507
2508    def _adjust_scrollbars(self):
2509        """ Expands the vertical scrollbar beyond the range set by Qt.
2510        """
2511        # This code is adapted from _q_adjustScrollbars in qplaintextedit.cpp
2512        # and qtextedit.cpp.
2513        document = self._control.document()
2514        scrollbar = self._control.verticalScrollBar()
2515        viewport_height = self._control.viewport().height()
2516        if isinstance(self._control, QtWidgets.QPlainTextEdit):
2517            maximum = max(0, document.lineCount() - 1)
2518            step = viewport_height / self._control.fontMetrics().lineSpacing()
2519        else:
2520            # QTextEdit does not do line-based layout and blocks will not in
2521            # general have the same height. Therefore it does not make sense to
2522            # attempt to scroll in line height increments.
2523            maximum = document.size().height()
2524            step = viewport_height
2525        diff = maximum - scrollbar.maximum()
2526        scrollbar.setRange(0, round(maximum))
2527        scrollbar.setPageStep(round(step))
2528
2529        # Compensate for undesirable scrolling that occurs automatically due to
2530        # maximumBlockCount() text truncation.
2531        if diff < 0 and document.blockCount() == document.maximumBlockCount():
2532            scrollbar.setValue(round(scrollbar.value() + diff))
2533
2534    def _custom_context_menu_requested(self, pos):
2535        """ Shows a context menu at the given QPoint (in widget coordinates).
2536        """
2537        menu = self._context_menu_make(pos)
2538        menu.exec_(self._control.mapToGlobal(pos))
2539