• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

jupyter / qtconsole / 16950375420

13 Aug 2025 10:03PM UTC coverage: 61.682% (-0.06%) from 61.739%
16950375420

Pull #640

github

web-flow
Merge b53d885fa into 091a70541
Pull Request #640: [WIP] PR: Support IPython 9 theme/colors handling

0 of 6 new or added lines in 1 file covered. (0.0%)

2 existing lines in 2 files now uncovered.

2933 of 4755 relevant lines covered (61.68%)

1.23 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

66.02
/qtconsole/console_widget.py
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

6
from functools import partial
2✔
7
import os
2✔
8
import os.path
2✔
9
import re
2✔
10
import sys
2✔
11
from textwrap import dedent
2✔
12
import time
2✔
13
from unicodedata import category
2✔
14
import webbrowser
2✔
15

16
from qtpy import QT6
2✔
17
from qtpy import QtCore, QtGui, QtPrintSupport, QtWidgets
2✔
18

19
from qtconsole.rich_text import HtmlExporter
2✔
20
from qtconsole.util import MetaQObjectHasTraits, get_font, superQ
2✔
21

22
from traitlets.config.configurable import LoggingConfigurable
2✔
23
from traitlets import Bool, Enum, Integer, Unicode
2✔
24

25
from .ansi_code_processor import QtAnsiCodeProcessor
2✔
26
from .completion_widget import CompletionWidget
2✔
27
from .completion_html import CompletionHtml
2✔
28
from .completion_plain import CompletionPlain
2✔
29
from .kill_ring import QtKillRing
2✔
30
from .util import columnize
2✔
31

32

33
def is_letter_or_number(char):
2✔
34
    """ Returns whether the specified unicode character is a letter or a number.
35
    """
36
    cat = category(char)
2✔
37
    return cat.startswith('L') or cat.startswith('N')
2✔
38

39
def is_whitespace(char):
2✔
40
    """Check whether a given char counts as white space."""
41
    return category(char).startswith('Z')
2✔
42

43
#-----------------------------------------------------------------------------
44
# Classes
45
#-----------------------------------------------------------------------------
46

47
class ConsoleWidget(MetaQObjectHasTraits('NewBase', (LoggingConfigurable, superQ(QtWidgets.QWidget)), {})):
2✔
48
    """ An abstract base class for console-type widgets. This class has
49
        functionality for:
50

51
            * Maintaining a prompt and editing region
52
            * Providing the traditional Unix-style console keyboard shortcuts
53
            * Performing tab completion
54
            * Paging text
55
            * Handling ANSI escape codes
56

57
        ConsoleWidget also provides a number of utility methods that will be
58
        convenient to implementors of a console-style widget.
59
    """
60

61
    #------ Configuration ------------------------------------------------------
62

63
    ansi_codes = Bool(True, config=True,
2✔
64
        help="Whether to process ANSI escape codes."
65
    )
66
    buffer_size = Integer(500, config=True,
2✔
67
        help="""
68
        The maximum number of lines of text before truncation. Specifying a
69
        non-positive number disables text truncation (not recommended).
70
        """
71
    )
72
    execute_on_complete_input = Bool(True, config=True,
2✔
73
        help="""Whether to automatically execute on syntactically complete input.
74

75
        If False, Shift-Enter is required to submit each execution.
76
        Disabling this is mainly useful for non-Python kernels,
77
        where the completion check would be wrong.
78
        """
79
    )
80
    gui_completion = Enum(['plain', 'droplist', 'ncurses'], config=True,
2✔
81
                    default_value = 'ncurses',
82
                    help="""
83
                    The type of completer to use. Valid values are:
84

85
                    'plain'   : Show the available completion as a text list
86
                                Below the editing area.
87
                    'droplist': Show the completion in a drop down list navigable
88
                                by the arrow keys, and from which you can select
89
                                completion by pressing Return.
90
                    'ncurses' : Show the completion as a text list which is navigable by
91
                                `tab` and arrow keys.
92
                    """
93
    )
94
    gui_completion_height = Integer(0, config=True,
2✔
95
        help="""
96
        Set Height for completion.
97

98
        'droplist'
99
            Height in pixels.
100
        'ncurses'
101
            Maximum number of rows.
102
        """
103
    )
104
    # NOTE: this value can only be specified during initialization.
105
    kind = Enum(['plain', 'rich'], default_value='plain', config=True,
2✔
106
        help="""
107
        The type of underlying text widget to use. Valid values are 'plain',
108
        which specifies a QPlainTextEdit, and 'rich', which specifies a
109
        QTextEdit.
110
        """
111
    )
112
    # NOTE: this value can only be specified during initialization.
113
    paging = Enum(['inside', 'hsplit', 'vsplit', 'custom', 'none'],
2✔
114
                  default_value='inside', config=True,
115
        help="""
116
        The type of paging to use. Valid values are:
117

118
        'inside'
119
           The widget pages like a traditional terminal.
120
        'hsplit'
121
           When paging is requested, the widget is split horizontally. The top
122
           pane contains the console, and the bottom pane contains the paged text.
123
        'vsplit'
124
           Similar to 'hsplit', except that a vertical splitter is used.
125
        'custom'
126
           No action is taken by the widget beyond emitting a
127
           'custom_page_requested(str)' signal.
128
        'none'
129
           The text is written directly to the console.
130
        """)
131

132
    scrollbar_visibility = Bool(True, config=True,
2✔
133
        help="""The visibility of the scrollar. If False then the scrollbar will be
134
        invisible."""
135
    )
136

137
    font_family = Unicode(config=True,
2✔
138
        help="""The font family to use for the console.
139
        On OSX this defaults to Monaco, on Windows the default is
140
        Consolas with fallback of Courier, and on other platforms
141
        the default is Monospace.
142
        """)
143
    def _font_family_default(self):
2✔
144
        if sys.platform == 'win32':
2✔
145
            # Consolas ships with Vista/Win7, fallback to Courier if needed
146
            return 'Consolas'
×
147
        elif sys.platform == 'darwin':
2✔
148
            # OSX always has Monaco, no need for a fallback
149
            return 'Monaco'
×
150
        else:
151
            # Monospace should always exist, no need for a fallback
152
            return 'Monospace'
2✔
153

154
    font_size = Integer(config=True,
2✔
155
        help="""The font size. If unconfigured, Qt will be entrusted
156
        with the size of the font.
157
        """)
158

159
    console_width = Integer(81, config=True,
2✔
160
        help="""The width of the console at start time in number
161
        of characters (will double with `hsplit` paging)
162
        """)
163

164
    console_height = Integer(25, config=True,
2✔
165
        help="""The height of the console at start time in number
166
        of characters (will double with `vsplit` paging)
167
        """)
168

169
    # Whether to override ShortcutEvents for the keybindings defined by this
170
    # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
171
    # priority (when it has focus) over, e.g., window-level menu shortcuts.
172
    override_shortcuts = Bool(False)
2✔
173

174
    # ------ Custom Qt Widgets -------------------------------------------------
175

176
    # For other projects to easily override the Qt widgets used by the console
177
    # (e.g. Spyder)
178
    custom_control = None
2✔
179
    custom_page_control = None
2✔
180

181
    #------ Signals ------------------------------------------------------------
182

183
    # Signals that indicate ConsoleWidget state.
184
    copy_available = QtCore.Signal(bool)
2✔
185
    redo_available = QtCore.Signal(bool)
2✔
186
    undo_available = QtCore.Signal(bool)
2✔
187

188
    # Signal emitted when paging is needed and the paging style has been
189
    # specified as 'custom'.
190
    custom_page_requested = QtCore.Signal(object)
2✔
191

192
    # Signal emitted when the font is changed.
193
    font_changed = QtCore.Signal(QtGui.QFont)
2✔
194

195
    #------ Protected class variables ------------------------------------------
196

197
    # control handles
198
    _control = None
2✔
199
    _page_control = None
2✔
200
    _splitter = None
2✔
201

202
    # When the control key is down, these keys are mapped.
203
    _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
2✔
204
                         QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
205
                         QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
206
                         QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
207
                         QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
208
                         QtCore.Qt.Key_H : QtCore.Qt.Key_Backspace, }
209
    if not sys.platform == 'darwin':
2✔
210
        # On OS X, Ctrl-E already does the right thing, whereas End moves the
211
        # cursor to the bottom of the buffer.
212
        _ctrl_down_remap[QtCore.Qt.Key_E] = QtCore.Qt.Key_End
2✔
213

214
    # The shortcuts defined by this widget. We need to keep track of these to
215
    # support 'override_shortcuts' above.
216
    _shortcuts = set(_ctrl_down_remap.keys()) | \
2✔
217
                     { QtCore.Qt.Key_C, QtCore.Qt.Key_G, QtCore.Qt.Key_O,
218
                       QtCore.Qt.Key_V }
219

220
    _temp_buffer_filled = False
2✔
221

222
    #---------------------------------------------------------------------------
223
    # 'QObject' interface
224
    #---------------------------------------------------------------------------
225

226
    def __init__(self, parent=None, **kw):
2✔
227
        """ Create a ConsoleWidget.
228

229
        Parameters
230
        ----------
231
        parent : QWidget, optional [default None]
232
            The parent for this widget.
233
        """
234
        super().__init__(**kw)
2✔
235
        if parent:
2✔
236
            self.setParent(parent)
×
237

238
        self._is_complete_msg_id = None
2✔
239
        self._is_complete_timeout = 0.1
2✔
240
        self._is_complete_max_time = None
2✔
241

242
        # While scrolling the pager on Mac OS X, it tears badly.  The
243
        # NativeGesture is platform and perhaps build-specific hence
244
        # we take adequate precautions here.
245
        self._pager_scroll_events = [QtCore.QEvent.Wheel]
2✔
246
        if hasattr(QtCore.QEvent, 'NativeGesture'):
2✔
247
            self._pager_scroll_events.append(QtCore.QEvent.NativeGesture)
2✔
248

249
        # Create the layout and underlying text widget.
250
        layout = QtWidgets.QStackedLayout(self)
2✔
251
        layout.setContentsMargins(0, 0, 0, 0)
2✔
252
        self._control = self._create_control()
2✔
253
        if self.paging in ('hsplit', 'vsplit'):
2✔
254
            self._splitter = QtWidgets.QSplitter()
×
255
            if self.paging == 'hsplit':
×
256
                self._splitter.setOrientation(QtCore.Qt.Horizontal)
×
257
            else:
258
                self._splitter.setOrientation(QtCore.Qt.Vertical)
×
259
            self._splitter.addWidget(self._control)
×
260
            layout.addWidget(self._splitter)
×
261
        else:
262
            layout.addWidget(self._control)
2✔
263

264
        # Create the paging widget, if necessary.
265
        if self.paging in ('inside', 'hsplit', 'vsplit'):
2✔
266
            self._page_control = self._create_page_control()
2✔
267
            if self._splitter:
2✔
268
                self._page_control.hide()
×
269
                self._splitter.addWidget(self._page_control)
×
270
            else:
271
                layout.addWidget(self._page_control)
2✔
272

273
        # Initialize protected variables. Some variables contain useful state
274
        # information for subclasses; they should be considered read-only.
275
        self._append_before_prompt_cursor = self._control.textCursor()
2✔
276
        self._ansi_processor = QtAnsiCodeProcessor()
2✔
277
        if self.gui_completion == 'ncurses':
2✔
278
            self._completion_widget = CompletionHtml(self, self.gui_completion_height)
2✔
279
        elif self.gui_completion == 'droplist':
×
280
            self._completion_widget = CompletionWidget(self, self.gui_completion_height)
×
281
        elif self.gui_completion == 'plain':
×
282
            self._completion_widget = CompletionPlain(self)
×
283

284
        self._continuation_prompt = '> '
2✔
285
        self._continuation_prompt_html = None
2✔
286
        self._executing = False
2✔
287
        self._filter_resize = False
2✔
288
        self._html_exporter = HtmlExporter(self._control)
2✔
289
        self._input_buffer_executing = ''
2✔
290
        self._input_buffer_pending = ''
2✔
291
        self._kill_ring = QtKillRing(self._control)
2✔
292
        self._prompt = ''
2✔
293
        self._prompt_html = None
2✔
294
        self._prompt_cursor = self._control.textCursor()
2✔
295
        self._prompt_sep = ''
2✔
296
        self._reading = False
2✔
297
        self._reading_callback = None
2✔
298
        self._tab_width = 4
2✔
299

300
        # Cursor position of where to insert text.
301
        # Control characters allow this to move around on the current line.
302
        self._insert_text_cursor = self._control.textCursor()
2✔
303

304
        # List of strings pending to be appended as plain text in the widget.
305
        # The text is not immediately inserted when available to not
306
        # choke the Qt event loop with paint events for the widget in
307
        # case of lots of output from kernel.
308
        self._pending_insert_text = []
2✔
309

310
        # Timer to flush the pending stream messages. The interval is adjusted
311
        # later based on actual time taken for flushing a screen (buffer_size)
312
        # of output text.
313
        self._pending_text_flush_interval = QtCore.QTimer(self._control)
2✔
314
        self._pending_text_flush_interval.setInterval(100)
2✔
315
        self._pending_text_flush_interval.setSingleShot(True)
2✔
316
        self._pending_text_flush_interval.timeout.connect(
2✔
317
                                            self._on_flush_pending_stream_timer)
318

319
        # Set a monospaced font.
320
        self.reset_font()
2✔
321

322
        # Configure actions.
323
        action = QtWidgets.QAction('Print', None)
2✔
324
        action.setEnabled(True)
2✔
325
        printkey = QtGui.QKeySequence(QtGui.QKeySequence.Print)
2✔
326
        if printkey.matches("Ctrl+P") and sys.platform != 'darwin':
2✔
327
            # Only override the default if there is a collision.
328
            # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
329
            printkey = "Ctrl+Shift+P"
2✔
330
        action.setShortcut(printkey)
2✔
331
        action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
2✔
332
        action.triggered.connect(self.print_)
2✔
333
        self.addAction(action)
2✔
334
        self.print_action = action
2✔
335

336
        action = QtWidgets.QAction('Save as HTML/XML', None)
2✔
337
        action.setShortcut(QtGui.QKeySequence.Save)
2✔
338
        action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
2✔
339
        action.triggered.connect(self.export_html)
2✔
340
        self.addAction(action)
2✔
341
        self.export_action = action
2✔
342

343
        action = QtWidgets.QAction('Select All', None)
2✔
344
        action.setEnabled(True)
2✔
345
        selectall = QtGui.QKeySequence(QtGui.QKeySequence.SelectAll)
2✔
346
        if selectall.matches("Ctrl+A") and sys.platform != 'darwin':
2✔
347
            # Only override the default if there is a collision.
348
            # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
349
            selectall = "Ctrl+Shift+A"
2✔
350
        action.setShortcut(selectall)
2✔
351
        action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
2✔
352
        action.triggered.connect(self.select_all_smart)
2✔
353
        self.addAction(action)
2✔
354
        self.select_all_action = action
2✔
355

356
        self.increase_font_size = QtWidgets.QAction("Bigger Font",
2✔
357
                self,
358
                shortcut=QtGui.QKeySequence.ZoomIn,
359
                shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut,
360
                statusTip="Increase the font size by one point",
361
                triggered=self._increase_font_size)
362
        self.addAction(self.increase_font_size)
2✔
363

364
        self.decrease_font_size = QtWidgets.QAction("Smaller Font",
2✔
365
                self,
366
                shortcut=QtGui.QKeySequence.ZoomOut,
367
                shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut,
368
                statusTip="Decrease the font size by one point",
369
                triggered=self._decrease_font_size)
370
        self.addAction(self.decrease_font_size)
2✔
371

372
        self.reset_font_size = QtWidgets.QAction("Normal Font",
2✔
373
                self,
374
                shortcut="Ctrl+0",
375
                shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut,
376
                statusTip="Restore the Normal font size",
377
                triggered=self.reset_font)
378
        self.addAction(self.reset_font_size)
2✔
379

380
        # Accept drag and drop events here. Drops were already turned off
381
        # in self._control when that widget was created.
382
        self.setAcceptDrops(True)
2✔
383

384
    #---------------------------------------------------------------------------
385
    # Drag and drop support
386
    #---------------------------------------------------------------------------
387

388
    def dragEnterEvent(self, e):
2✔
389
        if e.mimeData().hasUrls():
×
390
            # The link action should indicate to that the drop will insert
391
            # the file anme.
392
            e.setDropAction(QtCore.Qt.LinkAction)
×
393
            e.accept()
×
394
        elif e.mimeData().hasText():
×
395
            # By changing the action to copy we don't need to worry about
396
            # the user accidentally moving text around in the widget.
397
            e.setDropAction(QtCore.Qt.CopyAction)
×
398
            e.accept()
×
399

400
    def dragMoveEvent(self, e):
2✔
401
        if e.mimeData().hasUrls():
×
402
            pass
×
403
        elif e.mimeData().hasText():
×
404
            cursor = self._control.cursorForPosition(e.pos())
×
405
            if self._in_buffer(cursor.position()):
×
406
                e.setDropAction(QtCore.Qt.CopyAction)
×
407
                self._control.setTextCursor(cursor)
×
408
            else:
409
                e.setDropAction(QtCore.Qt.IgnoreAction)
×
410
            e.accept()
×
411

412
    def dropEvent(self, e):
2✔
413
        if e.mimeData().hasUrls():
×
414
            self._keep_cursor_in_buffer()
×
415
            cursor = self._control.textCursor()
×
416
            filenames = [url.toLocalFile() for url in e.mimeData().urls()]
×
417
            text = ', '.join("'" + f.replace("'", "'\"'\"'") + "'"
×
418
                             for f in filenames)
419
            self._insert_plain_text_into_buffer(cursor, text)
×
420
        elif e.mimeData().hasText():
×
421
            cursor = self._control.cursorForPosition(e.pos())
×
422
            if self._in_buffer(cursor.position()):
×
423
                text = e.mimeData().text()
×
424
                self._insert_plain_text_into_buffer(cursor, text)
×
425

426
    def eventFilter(self, obj, event):
2✔
427
        """ Reimplemented to ensure a console-like behavior in the underlying
428
            text widgets.
429
        """
430
        etype = event.type()
2✔
431
        if etype == QtCore.QEvent.KeyPress:
2✔
432

433
            # Re-map keys for all filtered widgets.
434
            key = event.key()
2✔
435
            if self._control_key_down(event.modifiers()) and \
2✔
436
                    key in self._ctrl_down_remap:
437
                new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
×
438
                                            self._ctrl_down_remap[key],
439
                                            QtCore.Qt.NoModifier)
440
                QtWidgets.QApplication.instance().sendEvent(obj, new_event)
×
441
                return True
×
442

443
            elif obj == self._control:
2✔
444
                return self._event_filter_console_keypress(event)
2✔
445

446
            elif obj == self._page_control:
×
447
                return self._event_filter_page_keypress(event)
×
448

449
        # Make middle-click paste safe.
450
        elif getattr(event, 'button', False) and \
2✔
451
                etype == QtCore.QEvent.MouseButtonRelease and \
452
                event.button() == QtCore.Qt.MiddleButton and \
453
                obj == self._control.viewport():
454
            cursor = self._control.cursorForPosition(event.pos())
×
455
            self._control.setTextCursor(cursor)
×
456
            self.paste(QtGui.QClipboard.Selection)
×
457
            return True
×
458

459
        # Manually adjust the scrollbars *after* a resize event is dispatched.
460
        elif etype == QtCore.QEvent.Resize and not self._filter_resize:
2✔
461
            self._filter_resize = True
2✔
462
            QtWidgets.QApplication.instance().sendEvent(obj, event)
2✔
463
            self._adjust_scrollbars()
2✔
464
            self._filter_resize = False
2✔
465
            return True
2✔
466

467
        # Override shortcuts for all filtered widgets.
468
        elif etype == QtCore.QEvent.ShortcutOverride and \
2✔
469
                self.override_shortcuts and \
470
                self._control_key_down(event.modifiers()) and \
471
                event.key() in self._shortcuts:
472
            event.accept()
×
473

474
        # Handle scrolling of the vsplit pager. This hack attempts to solve
475
        # problems with tearing of the help text inside the pager window.  This
476
        # happens only on Mac OS X with both PySide and PyQt. This fix isn't
477
        # perfect but makes the pager more usable.
478
        elif etype in self._pager_scroll_events and \
2✔
479
                obj == self._page_control:
480
            self._page_control.repaint()
×
481
            return True
×
482

483
        elif etype == QtCore.QEvent.MouseMove:
2✔
484
            anchor = self._control.anchorAt(event.pos())
2✔
485
            if QT6:
2✔
486
                pos = event.globalPosition().toPoint()
1✔
487
            else:
UNCOV
488
                pos = event.globalPos()
1✔
489
            QtWidgets.QToolTip.showText(pos, anchor)
2✔
490

491
        elif (
2✔
492
            etype == QtCore.QEvent.Wheel
493
            and self._control_key_down(event.modifiers())
494
        ):
495
            if sys.platform != 'darwin':
×
496
                if hasattr(event, 'angleDelta'):
×
497
                    if event.angleDelta().y() < 0:
×
498
                        self._decrease_font_size()
×
499
                    elif event.angleDelta().y() > 0:
×
500
                        self._increase_font_size()
×
501
                elif hasattr(event, 'delta'):
×
502
                    if event.delta() < 0:
×
503
                        self._decrease_font_size()
×
504
                    elif event.delta() > 0:
×
505
                        self._increase_font_size()
×
506

507
            # This is necessary to prevent that the mouse wheel event also
508
            # scrolls up and down in this case.
509
            return True
×
510

511
        return super().eventFilter(obj, event)
2✔
512

513
    #---------------------------------------------------------------------------
514
    # 'QWidget' interface
515
    #---------------------------------------------------------------------------
516

517
    def sizeHint(self):
2✔
518
        """ Reimplemented to suggest a size that is 80 characters wide and
519
            25 lines high.
520
        """
521
        font_metrics = QtGui.QFontMetrics(self.font)
2✔
522
        margin = (self._control.frameWidth() +
2✔
523
                  self._control.document().documentMargin()) * 2
524
        style = self.style()
2✔
525
        splitwidth = style.pixelMetric(QtWidgets.QStyle.PM_SplitterWidth)
2✔
526

527
        # Note 1: Despite my best efforts to take the various margins into
528
        # account, the width is still coming out a bit too small, so we include
529
        # a fudge factor of one character here.
530
        # Note 2: QFontMetrics.maxWidth is not used here or anywhere else due
531
        # to a Qt bug on certain Mac OS systems where it returns 0.
532
        width = self._get_font_width() * self.console_width + margin
2✔
533
        width += style.pixelMetric(QtWidgets.QStyle.PM_ScrollBarExtent)
2✔
534

535
        if self.paging == 'hsplit':
2✔
536
            width = width * 2 + splitwidth
×
537

538
        height = font_metrics.height() * self.console_height + margin
2✔
539
        if self.paging == 'vsplit':
2✔
540
            height = height * 2 + splitwidth
×
541

542
        return QtCore.QSize(int(width), int(height))
2✔
543

544
    #---------------------------------------------------------------------------
545
    # 'ConsoleWidget' public interface
546
    #---------------------------------------------------------------------------
547

548
    include_other_output = Bool(False, config=True,
2✔
549
        help="""Whether to include output from clients
550
        other than this one sharing the same kernel.
551

552
        Outputs are not displayed until enter is pressed.
553
        """
554
    )
555

556
    other_output_prefix = Unicode('[remote] ', config=True,
2✔
557
        help="""Prefix to add to outputs coming from clients other than this one.
558

559
        Only relevant if include_other_output is True.
560
        """
561
    )
562

563
    def can_copy(self):
2✔
564
        """ Returns whether text can be copied to the clipboard.
565
        """
566
        return self._control.textCursor().hasSelection()
×
567

568
    def can_cut(self):
2✔
569
        """ Returns whether text can be cut to the clipboard.
570
        """
571
        cursor = self._control.textCursor()
×
572
        return (cursor.hasSelection() and
×
573
                self._in_buffer(cursor.anchor()) and
574
                self._in_buffer(cursor.position()))
575

576
    def can_paste(self):
2✔
577
        """ Returns whether text can be pasted from the clipboard.
578
        """
579
        if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
×
580
            return bool(QtWidgets.QApplication.clipboard().text())
×
581
        return False
×
582

583
    def clear(self, keep_input=True):
2✔
584
        """ Clear the console.
585

586
        Parameters
587
        ----------
588
        keep_input : bool, optional (default True)
589
            If set, restores the old input buffer if a new prompt is written.
590
        """
591
        if self._executing:
×
592
            self._control.clear()
×
593
        else:
594
            if keep_input:
×
595
                input_buffer = self.input_buffer
×
596
            self._control.clear()
×
597
            self._show_prompt()
×
598
            if keep_input:
×
599
                self.input_buffer = input_buffer
×
600

601
    def copy(self):
2✔
602
        """ Copy the currently selected text to the clipboard.
603
        """
604
        self.layout().currentWidget().copy()
2✔
605

606
    def copy_anchor(self, anchor):
2✔
607
        """ Copy anchor text to the clipboard
608
        """
609
        QtWidgets.QApplication.clipboard().setText(anchor)
×
610

611
    def cut(self):
2✔
612
        """ Copy the currently selected text to the clipboard and delete it
613
            if it's inside the input buffer.
614
        """
615
        self.copy()
×
616
        if self.can_cut():
×
617
            self._control.textCursor().removeSelectedText()
×
618

619
    def _handle_is_complete_reply(self, msg):
2✔
620
        if msg['parent_header'].get('msg_id', 0) != self._is_complete_msg_id:
2✔
621
            return
2✔
622
        status = msg['content'].get('status', 'complete')
2✔
623
        indent = msg['content'].get('indent', '')
2✔
624
        self._trigger_is_complete_callback(status != 'incomplete', indent)
2✔
625

626
    def _trigger_is_complete_callback(self, complete=False, indent=''):
2✔
627
        if self._is_complete_msg_id is not None:
2✔
628
            self._is_complete_msg_id = None
2✔
629
            self._is_complete_callback(complete, indent)
2✔
630

631
    def _register_is_complete_callback(self, source, callback):
2✔
632
        if self._is_complete_msg_id is not None:
2✔
633
            if self._is_complete_max_time < time.time():
×
634
                # Second return while waiting for is_complete
635
                return
×
636
            else:
637
                # request timed out
638
                self._trigger_is_complete_callback()
×
639
        self._is_complete_max_time = time.time() + self._is_complete_timeout
2✔
640
        self._is_complete_callback = callback
2✔
641
        self._is_complete_msg_id = self.kernel_client.is_complete(source)
2✔
642

643
    def execute(self, source=None, hidden=False, interactive=False):
2✔
644
        """ Executes source or the input buffer, possibly prompting for more
645
        input.
646

647
        Parameters
648
        ----------
649
        source : str, optional
650

651
            The source to execute. If not specified, the input buffer will be
652
            used. If specified and 'hidden' is False, the input buffer will be
653
            replaced with the source before execution.
654

655
        hidden : bool, optional (default False)
656

657
            If set, no output will be shown and the prompt will not be modified.
658
            In other words, it will be completely invisible to the user that
659
            an execution has occurred.
660

661
        interactive : bool, optional (default False)
662

663
            Whether the console is to treat the source as having been manually
664
            entered by the user. The effect of this parameter depends on the
665
            subclass implementation.
666

667
        Raises
668
        ------
669
        RuntimeError
670
            If incomplete input is given and 'hidden' is True. In this case,
671
            it is not possible to prompt for more input.
672

673
        Returns
674
        -------
675
        A boolean indicating whether the source was executed.
676
        """
677
        # WARNING: The order in which things happen here is very particular, in
678
        # large part because our syntax highlighting is fragile. If you change
679
        # something, test carefully!
680

681
        # Decide what to execute.
682
        if source is None:
2✔
683
            source = self.input_buffer
2✔
684
        elif not hidden:
2✔
685
            self.input_buffer = source
2✔
686

687
        if hidden:
2✔
688
            self._execute(source, hidden)
×
689
        # Execute the source or show a continuation prompt if it is incomplete.
690
        elif interactive and self.execute_on_complete_input:
2✔
691
            self._register_is_complete_callback(
2✔
692
                source, partial(self.do_execute, source))
693
        else:
694
            self.do_execute(source, True, '')
2✔
695

696
    def do_execute(self, source, complete, indent):
2✔
697
        if complete:
2✔
698
            self._append_plain_text('\n')
2✔
699
            self._input_buffer_executing = self.input_buffer
2✔
700
            self._executing = True
2✔
701
            self._finalize_input_request()
2✔
702

703
            # Perform actual execution.
704
            self._execute(source, False)
2✔
705

706
        else:
707
            # Do this inside an edit block so continuation prompts are
708
            # removed seamlessly via undo/redo.
709
            cursor = self._get_end_cursor()
2✔
710
            cursor.beginEditBlock()
2✔
711
            try:
2✔
712
                cursor.insertText('\n')
2✔
713
                self._insert_continuation_prompt(cursor, indent)
2✔
714
            finally:
715
                cursor.endEditBlock()
2✔
716

717
            # Do not do this inside the edit block. It works as expected
718
            # when using a QPlainTextEdit control, but does not have an
719
            # effect when using a QTextEdit. I believe this is a Qt bug.
720
            self._control.moveCursor(QtGui.QTextCursor.End)
2✔
721

722
            # Advance where text is inserted
723
            self._insert_text_cursor.movePosition(QtGui.QTextCursor.End)
2✔
724

725
    def export_html(self):
2✔
726
        """ Shows a dialog to export HTML/XML in various formats.
727
        """
728
        self._html_exporter.export()
×
729

730
    def _finalize_input_request(self):
2✔
731
        """
732
        Set the widget to a non-reading state.
733
        """
734
        # Must set _reading to False before calling _prompt_finished
735
        self._reading = False
2✔
736
        self._prompt_finished()
2✔
737

738
        # There is no prompt now, so before_prompt_position is eof
739
        self._append_before_prompt_cursor.setPosition(
2✔
740
            self._get_end_cursor().position())
741

742
        self._insert_text_cursor.setPosition(
2✔
743
            self._get_end_cursor().position())
744

745
        # The maximum block count is only in effect during execution.
746
        # This ensures that _prompt_pos does not become invalid due to
747
        # text truncation.
748
        self._control.document().setMaximumBlockCount(self.buffer_size)
2✔
749

750
        # Setting a positive maximum block count will automatically
751
        # disable the undo/redo history, but just to be safe:
752
        self._control.setUndoRedoEnabled(False)
2✔
753

754
    def _get_input_buffer(self, force=False):
2✔
755
        """ The text that the user has entered entered at the current prompt.
756

757
        If the console is currently executing, the text that is executing will
758
        always be returned.
759
        """
760
        # If we're executing, the input buffer may not even exist anymore due to
761
        # the limit imposed by 'buffer_size'. Therefore, we store it.
762
        if self._executing and not force:
2✔
763
            return self._input_buffer_executing
2✔
764

765
        cursor = self._get_end_cursor()
2✔
766
        cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
2✔
767
        input_buffer = cursor.selection().toPlainText()
2✔
768

769
        # Strip out continuation prompts.
770
        return input_buffer.replace('\n' + self._continuation_prompt, '\n')
2✔
771

772
    def _set_input_buffer(self, string):
2✔
773
        """ Sets the text in the input buffer.
774

775
        If the console is currently executing, this call has no *immediate*
776
        effect. When the execution is finished, the input buffer will be updated
777
        appropriately.
778
        """
779
        # If we're executing, store the text for later.
780
        if self._executing:
2✔
781
            self._input_buffer_pending = string
2✔
782
            return
2✔
783

784
        # Remove old text.
785
        cursor = self._get_end_cursor()
2✔
786
        cursor.beginEditBlock()
2✔
787
        cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
2✔
788
        cursor.removeSelectedText()
2✔
789

790
        # Insert new text with continuation prompts.
791
        self._insert_plain_text_into_buffer(self._get_prompt_cursor(), string)
2✔
792
        cursor.endEditBlock()
2✔
793
        self._control.moveCursor(QtGui.QTextCursor.End)
2✔
794

795
    input_buffer = property(_get_input_buffer, _set_input_buffer)
2✔
796

797
    def _get_font(self):
2✔
798
        """ The base font being used by the ConsoleWidget.
799
        """
800
        return self._control.document().defaultFont()
2✔
801

802
    def _get_font_width(self, font=None):
2✔
803
        if font is None:
2✔
804
            font = self.font
2✔
805
        font_metrics = QtGui.QFontMetrics(font)
2✔
806
        if hasattr(font_metrics, 'horizontalAdvance'):
2✔
807
            return font_metrics.horizontalAdvance(' ')
2✔
808
        else:
809
            return font_metrics.width(' ')
×
810

811
    def _set_font(self, font):
2✔
812
        """ Sets the base font for the ConsoleWidget to the specified QFont.
813
        """
814
        self._control.setTabStopWidth(
2✔
815
            self.tab_width * self._get_font_width(font)
816
        )
817

818
        self._completion_widget.setFont(font)
2✔
819
        self._control.document().setDefaultFont(font)
2✔
820
        if self._page_control:
2✔
821
            self._page_control.document().setDefaultFont(font)
2✔
822

823
        self.font_changed.emit(font)
2✔
824

825
    font = property(_get_font, _set_font)
2✔
826

827
    def _set_completion_widget(self, gui_completion):
2✔
828
        """ Set gui completion widget.
829
        """
830
        if gui_completion == 'ncurses':
×
831
            self._completion_widget = CompletionHtml(self)
×
832
        elif gui_completion == 'droplist':
×
833
            self._completion_widget = CompletionWidget(self)
×
834
        elif gui_completion == 'plain':
×
835
            self._completion_widget = CompletionPlain(self)
×
836

837
        self.gui_completion = gui_completion
×
838

839
    def open_anchor(self, anchor):
2✔
840
        """ Open selected anchor in the default webbrowser
841
        """
842
        webbrowser.open( anchor )
×
843

844
    def paste(self, mode=QtGui.QClipboard.Clipboard):
2✔
845
        """ Paste the contents of the clipboard into the input region.
846

847
        Parameters
848
        ----------
849
        mode : QClipboard::Mode, optional [default QClipboard::Clipboard]
850

851
            Controls which part of the system clipboard is used. This can be
852
            used to access the selection clipboard in X11 and the Find buffer
853
            in Mac OS. By default, the regular clipboard is used.
854
        """
855
        if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
2✔
856
            # Make sure the paste is safe.
857
            self._keep_cursor_in_buffer()
2✔
858
            cursor = self._control.textCursor()
2✔
859

860
            # Remove any trailing newline, which confuses the GUI and forces the
861
            # user to backspace.
862
            text = QtWidgets.QApplication.clipboard().text(mode).rstrip()
2✔
863

864
            # dedent removes "common leading whitespace" but to preserve relative
865
            # indent of multiline code, we have to compensate for any
866
            # leading space on the first line, if we're pasting into
867
            # an indented position.
868
            cursor_offset = cursor.position() - self._get_line_start_pos()
2✔
869
            if text.startswith(' ' * cursor_offset):
2✔
870
                text = text[cursor_offset:]
2✔
871

872
            self._insert_plain_text_into_buffer(cursor, dedent(text))
2✔
873

874
    def print_(self, printer=None):
2✔
875
        """ Print the contents of the ConsoleWidget to the specified QPrinter.
876
        """
877
        if not printer:
×
878
            printer = QtPrintSupport.QPrinter()
×
879
            if QtPrintSupport.QPrintDialog(printer).exec_() != QtPrintSupport.QPrintDialog.Accepted:
×
880
                return
×
881
        self._control.print_(printer)
×
882

883
    def prompt_to_top(self):
2✔
884
        """ Moves the prompt to the top of the viewport.
885
        """
886
        if not self._executing:
×
887
            prompt_cursor = self._get_prompt_cursor()
×
888
            if self._get_cursor().blockNumber() < prompt_cursor.blockNumber():
×
889
                self._set_cursor(prompt_cursor)
×
890
            self._set_top_cursor(prompt_cursor)
×
891

892
    def redo(self):
2✔
893
        """ Redo the last operation. If there is no operation to redo, nothing
894
            happens.
895
        """
896
        self._control.redo()
×
897

898
    def reset_font(self):
2✔
899
        """ Sets the font to the default fixed-width font for this platform.
900
        """
901
        if sys.platform == 'win32':
2✔
902
            # Consolas ships with Vista/Win7, fallback to Courier if needed
903
            fallback = 'Courier'
×
904
        elif sys.platform == 'darwin':
2✔
905
            # OSX always has Monaco
906
            fallback = 'Monaco'
×
907
        else:
908
            # Monospace should always exist
909
            fallback = 'Monospace'
2✔
910
        font = get_font(self.font_family, fallback)
2✔
911
        if self.font_size:
2✔
912
            font.setPointSize(self.font_size)
×
913
        else:
914
            font.setPointSize(QtWidgets.QApplication.instance().font().pointSize())
2✔
915
        font.setStyleHint(QtGui.QFont.TypeWriter)
2✔
916
        self._set_font(font)
2✔
917

918
    def change_font_size(self, delta):
2✔
919
        """Change the font size by the specified amount (in points).
920
        """
921
        font = self.font
×
922
        size = max(font.pointSize() + delta, 1) # minimum 1 point
×
923
        font.setPointSize(size)
×
924
        self._set_font(font)
×
925

926
    def _increase_font_size(self):
2✔
927
        self.change_font_size(1)
×
928

929
    def _decrease_font_size(self):
2✔
930
        self.change_font_size(-1)
×
931

932
    def select_all_smart(self):
2✔
933
        """ Select current cell, or, if already selected, the whole document.
934
        """
935
        c = self._get_cursor()
2✔
936
        sel_range = c.selectionStart(), c.selectionEnd()
2✔
937

938
        c.clearSelection()
2✔
939
        c.setPosition(self._get_prompt_cursor().position())
2✔
940
        c.setPosition(self._get_end_pos(),
2✔
941
                      mode=QtGui.QTextCursor.KeepAnchor)
942
        new_sel_range = c.selectionStart(), c.selectionEnd()
2✔
943
        if sel_range == new_sel_range:
2✔
944
            # cell already selected, expand selection to whole document
945
            self.select_document()
2✔
946
        else:
947
            # set cell selection as active selection
948
            self._control.setTextCursor(c)
2✔
949

950
    def select_document(self):
2✔
951
        """ Selects all the text in the buffer.
952
        """
953
        self._control.selectAll()
2✔
954

955
    def _get_tab_width(self):
2✔
956
        """ The width (in terms of space characters) for tab characters.
957
        """
958
        return self._tab_width
2✔
959

960
    def _set_tab_width(self, tab_width):
2✔
961
        """ Sets the width (in terms of space characters) for tab characters.
962
        """
963
        self._control.setTabStopWidth(tab_width * self._get_font_width())
2✔
964

965
        self._tab_width = tab_width
2✔
966

967
    tab_width = property(_get_tab_width, _set_tab_width)
2✔
968

969
    def undo(self):
2✔
970
        """ Undo the last operation. If there is no operation to undo, nothing
971
            happens.
972
        """
973
        self._control.undo()
×
974

975
    #---------------------------------------------------------------------------
976
    # 'ConsoleWidget' abstract interface
977
    #---------------------------------------------------------------------------
978

979
    def _is_complete(self, source, interactive):
2✔
980
        """ Returns whether 'source' can be executed. When triggered by an
981
            Enter/Return key press, 'interactive' is True; otherwise, it is
982
            False.
983
        """
984
        raise NotImplementedError
×
985

986
    def _execute(self, source, hidden):
2✔
987
        """ Execute 'source'. If 'hidden', do not show any output.
988
        """
989
        raise NotImplementedError
×
990

991
    def _prompt_started_hook(self):
2✔
992
        """ Called immediately after a new prompt is displayed.
993
        """
994
        pass
2✔
995

996
    def _prompt_finished_hook(self):
2✔
997
        """ Called immediately after a prompt is finished, i.e. when some input
998
            will be processed and a new prompt displayed.
999
        """
1000
        pass
2✔
1001

1002
    def _up_pressed(self, shift_modifier):
2✔
1003
        """ Called when the up key is pressed. Returns whether to continue
1004
            processing the event.
1005
        """
1006
        return True
×
1007

1008
    def _down_pressed(self, shift_modifier):
2✔
1009
        """ Called when the down key is pressed. Returns whether to continue
1010
            processing the event.
1011
        """
1012
        return True
×
1013

1014
    def _tab_pressed(self):
2✔
1015
        """ Called when the tab key is pressed. Returns whether to continue
1016
            processing the event.
1017
        """
1018
        return True
2✔
1019

1020
    #--------------------------------------------------------------------------
1021
    # 'ConsoleWidget' protected interface
1022
    #--------------------------------------------------------------------------
1023

1024
    def _append_custom(self, insert, input, before_prompt=False, *args, **kwargs):
2✔
1025
        """ A low-level method for appending content to the end of the buffer.
1026

1027
        If 'before_prompt' is enabled, the content will be inserted before the
1028
        current prompt, if there is one.
1029
        """
1030
        # Determine where to insert the content.
1031
        cursor = self._insert_text_cursor
2✔
1032
        if before_prompt and (self._reading or not self._executing):
2✔
1033
            self._flush_pending_stream()
2✔
1034

1035
            # Jump to before prompt, if there is one
1036
            if cursor.position() >= self._append_before_prompt_pos \
2✔
1037
                    and self._append_before_prompt_pos != self._get_end_pos():
1038
                cursor.setPosition(self._append_before_prompt_pos)
2✔
1039

1040
                # If we're appending on the same line as the prompt, use insert mode.
1041
                # If so, the character at self._append_before_prompt_pos will not be a newline
1042
                cursor.movePosition(QtGui.QTextCursor.Right,
2✔
1043
                                    QtGui.QTextCursor.KeepAnchor)
1044
                if cursor.selection().toPlainText() != '\n':
2✔
1045
                    cursor._insert_mode = True
2✔
1046
                cursor.movePosition(QtGui.QTextCursor.Left)
2✔
1047
        else:
1048
            # Insert at current printing point.
1049
            # If cursor is before prompt jump to end, but only if there
1050
            # is a prompt (before_prompt_pos != end)
1051
            if cursor.position() <= self._append_before_prompt_pos \
2✔
1052
                    and self._append_before_prompt_pos != self._get_end_pos():
1053
                cursor.movePosition(QtGui.QTextCursor.End)
2✔
1054

1055
            if insert != self._insert_plain_text:
2✔
1056
                self._flush_pending_stream()
2✔
1057

1058
        # Perform the insertion.
1059
        result = insert(cursor, input, *args, **kwargs)
2✔
1060

1061
        # Remove insert mode tag
1062
        if hasattr(cursor, '_insert_mode'):
2✔
1063
            del cursor._insert_mode
2✔
1064

1065
        return result
2✔
1066

1067
    def _append_block(self, block_format=None, before_prompt=False):
2✔
1068
        """ Appends an new QTextBlock to the end of the console buffer.
1069
        """
1070
        self._append_custom(self._insert_block, block_format, before_prompt)
2✔
1071

1072
    def _append_html(self, html, before_prompt=False):
2✔
1073
        """ Appends HTML at the end of the console buffer.
1074
        """
1075
        self._append_custom(self._insert_html, html, before_prompt)
2✔
1076

1077
    def _append_html_fetching_plain_text(self, html, before_prompt=False):
2✔
1078
        """ Appends HTML, then returns the plain text version of it.
1079
        """
1080
        return self._append_custom(self._insert_html_fetching_plain_text,
2✔
1081
                                   html, before_prompt)
1082

1083
    def _append_plain_text(self, text, before_prompt=False):
2✔
1084
        """ Appends plain text, processing ANSI codes if enabled.
1085
        """
1086
        self._append_custom(self._insert_plain_text, text, before_prompt)
2✔
1087

1088
    def _cancel_completion(self):
2✔
1089
        """ If text completion is progress, cancel it.
1090
        """
1091
        self._completion_widget.cancel_completion()
2✔
1092

1093
    def _clear_temporary_buffer(self):
2✔
1094
        """ Clears the "temporary text" buffer, i.e. all the text following
1095
            the prompt region.
1096
        """
1097
        # Select and remove all text below the input buffer.
1098
        cursor = self._get_prompt_cursor()
2✔
1099
        prompt = self._continuation_prompt.lstrip()
2✔
1100
        if self._temp_buffer_filled:
2✔
1101
            self._temp_buffer_filled = False
×
1102
            while cursor.movePosition(QtGui.QTextCursor.NextBlock):
×
1103
                temp_cursor = QtGui.QTextCursor(cursor)
×
1104
                temp_cursor.select(QtGui.QTextCursor.BlockUnderCursor)
×
1105
                text = temp_cursor.selection().toPlainText().lstrip()
×
1106
                if not text.startswith(prompt):
×
1107
                    break
×
1108
        else:
1109
            # We've reached the end of the input buffer and no text follows.
1110
            return
2✔
1111
        cursor.movePosition(QtGui.QTextCursor.Left) # Grab the newline.
×
1112
        cursor.movePosition(QtGui.QTextCursor.End,
×
1113
                            QtGui.QTextCursor.KeepAnchor)
1114
        cursor.removeSelectedText()
×
1115

1116
        # After doing this, we have no choice but to clear the undo/redo
1117
        # history. Otherwise, the text is not "temporary" at all, because it
1118
        # can be recalled with undo/redo. Unfortunately, Qt does not expose
1119
        # fine-grained control to the undo/redo system.
1120
        if self._control.isUndoRedoEnabled():
×
1121
            self._control.setUndoRedoEnabled(False)
×
1122
            self._control.setUndoRedoEnabled(True)
×
1123

1124
    def _complete_with_items(self, cursor, items):
2✔
1125
        """ Performs completion with 'items' at the specified cursor location.
1126
        """
1127
        self._cancel_completion()
×
1128

1129
        if len(items) == 1:
×
1130
            cursor.setPosition(self._control.textCursor().position(),
×
1131
                               QtGui.QTextCursor.KeepAnchor)
1132
            cursor.insertText(items[0])
×
1133

1134
        elif len(items) > 1:
×
1135
            current_pos = self._control.textCursor().position()
×
1136
            prefix = os.path.commonprefix(items)
×
1137
            if prefix:
×
1138
                cursor.setPosition(current_pos, QtGui.QTextCursor.KeepAnchor)
×
1139
                cursor.insertText(prefix)
×
1140
                current_pos = cursor.position()
×
1141

1142
            self._completion_widget.show_items(cursor, items,
×
1143
                                               prefix_length=len(prefix))
1144

1145
    def _fill_temporary_buffer(self, cursor, text, html=False):
2✔
1146
        """fill the area below the active editting zone with text"""
1147

1148
        current_pos = self._control.textCursor().position()
×
1149

1150
        cursor.beginEditBlock()
×
1151
        self._append_plain_text('\n')
×
1152
        self._page(text, html=html)
×
1153
        cursor.endEditBlock()
×
1154

1155
        cursor.setPosition(current_pos)
×
1156
        self._control.moveCursor(QtGui.QTextCursor.End)
×
1157
        self._control.setTextCursor(cursor)
×
1158

1159
        self._temp_buffer_filled = True
×
1160

1161

1162
    def _context_menu_make(self, pos):
2✔
1163
        """ Creates a context menu for the given QPoint (in widget coordinates).
1164
        """
1165
        menu = QtWidgets.QMenu(self)
×
1166

1167
        self.cut_action = menu.addAction('Cut', self.cut)
×
1168
        self.cut_action.setEnabled(self.can_cut())
×
1169
        self.cut_action.setShortcut(QtGui.QKeySequence.Cut)
×
1170

1171
        self.copy_action = menu.addAction('Copy', self.copy)
×
1172
        self.copy_action.setEnabled(self.can_copy())
×
1173
        self.copy_action.setShortcut(QtGui.QKeySequence.Copy)
×
1174

1175
        self.paste_action = menu.addAction('Paste', self.paste)
×
1176
        self.paste_action.setEnabled(self.can_paste())
×
1177
        self.paste_action.setShortcut(QtGui.QKeySequence.Paste)
×
1178

1179
        anchor = self._control.anchorAt(pos)
×
1180
        if anchor:
×
1181
            menu.addSeparator()
×
1182
            self.copy_link_action = menu.addAction(
×
1183
                'Copy Link Address', lambda: self.copy_anchor(anchor=anchor))
1184
            self.open_link_action = menu.addAction(
×
1185
                'Open Link', lambda: self.open_anchor(anchor=anchor))
1186

1187
        menu.addSeparator()
×
1188
        menu.addAction(self.select_all_action)
×
1189

1190
        menu.addSeparator()
×
1191
        menu.addAction(self.export_action)
×
1192
        menu.addAction(self.print_action)
×
1193

1194
        return menu
×
1195

1196
    def _control_key_down(self, modifiers, include_command=False):
2✔
1197
        """ Given a KeyboardModifiers flags object, return whether the Control
1198
        key is down.
1199

1200
        Parameters
1201
        ----------
1202
        include_command : bool, optional (default True)
1203
            Whether to treat the Command key as a (mutually exclusive) synonym
1204
            for Control when in Mac OS.
1205
        """
1206
        # Note that on Mac OS, ControlModifier corresponds to the Command key
1207
        # while MetaModifier corresponds to the Control key.
1208
        if sys.platform == 'darwin':
2✔
1209
            down = include_command and (modifiers & QtCore.Qt.ControlModifier)
×
1210
            return bool(down) ^ bool(modifiers & QtCore.Qt.MetaModifier)
×
1211
        else:
1212
            return bool(modifiers & QtCore.Qt.ControlModifier)
2✔
1213

1214
    def _create_control(self):
2✔
1215
        """ Creates and connects the underlying text widget.
1216
        """
1217
        # Create the underlying control.
1218
        if self.custom_control:
2✔
1219
            control = self.custom_control()
×
1220
        elif self.kind == 'plain':
2✔
1221
            control = QtWidgets.QPlainTextEdit()
2✔
1222
        elif self.kind == 'rich':
2✔
1223
            control = QtWidgets.QTextEdit()
2✔
1224
            control.setAcceptRichText(False)
2✔
1225
            control.setMouseTracking(True)
2✔
1226

1227
        # Prevent the widget from handling drops, as we already provide
1228
        # the logic in this class.
1229
        control.setAcceptDrops(False)
2✔
1230

1231
        # Install event filters. The filter on the viewport is needed for
1232
        # mouse events.
1233
        control.installEventFilter(self)
2✔
1234
        control.viewport().installEventFilter(self)
2✔
1235

1236
        # Connect signals.
1237
        control.customContextMenuRequested.connect(
2✔
1238
            self._custom_context_menu_requested)
1239
        control.copyAvailable.connect(self.copy_available)
2✔
1240
        control.redoAvailable.connect(self.redo_available)
2✔
1241
        control.undoAvailable.connect(self.undo_available)
2✔
1242

1243
        # Hijack the document size change signal to prevent Qt from adjusting
1244
        # the viewport's scrollbar. We are relying on an implementation detail
1245
        # of Q(Plain)TextEdit here, which is potentially dangerous, but without
1246
        # this functionality we cannot create a nice terminal interface.
1247
        layout = control.document().documentLayout()
2✔
1248
        layout.documentSizeChanged.disconnect()
2✔
1249
        layout.documentSizeChanged.connect(self._adjust_scrollbars)
2✔
1250

1251
        # Configure the scrollbar policy
1252
        if self.scrollbar_visibility:
2✔
1253
            scrollbar_policy = QtCore.Qt.ScrollBarAlwaysOn
2✔
1254
        else :
1255
            scrollbar_policy = QtCore.Qt.ScrollBarAlwaysOff
×
1256

1257
        # Configure the control.
1258
        control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
2✔
1259
        control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
2✔
1260
        control.setReadOnly(True)
2✔
1261
        control.setUndoRedoEnabled(False)
2✔
1262
        control.setVerticalScrollBarPolicy(scrollbar_policy)
2✔
1263
        return control
2✔
1264

1265
    def _create_page_control(self):
2✔
1266
        """ Creates and connects the underlying paging widget.
1267
        """
1268
        if self.custom_page_control:
2✔
1269
            control = self.custom_page_control()
×
1270
        elif self.kind == 'plain':
2✔
1271
            control = QtWidgets.QPlainTextEdit()
2✔
1272
        elif self.kind == 'rich':
2✔
1273
            control = QtWidgets.QTextEdit()
2✔
1274
        control.installEventFilter(self)
2✔
1275
        viewport = control.viewport()
2✔
1276
        viewport.installEventFilter(self)
2✔
1277
        control.setReadOnly(True)
2✔
1278
        control.setUndoRedoEnabled(False)
2✔
1279

1280
        # Configure the scrollbar policy
1281
        if self.scrollbar_visibility:
2✔
1282
            scrollbar_policy = QtCore.Qt.ScrollBarAlwaysOn
2✔
1283
        else :
1284
            scrollbar_policy = QtCore.Qt.ScrollBarAlwaysOff
×
1285

1286
        control.setVerticalScrollBarPolicy(scrollbar_policy)
2✔
1287
        return control
2✔
1288

1289
    def _event_filter_console_keypress(self, event):
2✔
1290
        """ Filter key events for the underlying text widget to create a
1291
            console-like interface.
1292
        """
1293
        intercepted = False
2✔
1294
        cursor = self._control.textCursor()
2✔
1295
        position = cursor.position()
2✔
1296
        key = event.key()
2✔
1297
        ctrl_down = self._control_key_down(event.modifiers())
2✔
1298
        alt_down = event.modifiers() & QtCore.Qt.AltModifier
2✔
1299
        shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
2✔
1300

1301
        cmd_down = (
2✔
1302
            sys.platform == "darwin" and
1303
            self._control_key_down(event.modifiers(), include_command=True)
1304
        )
1305
        if cmd_down:
2✔
1306
            if key == QtCore.Qt.Key_Left:
×
1307
                key = QtCore.Qt.Key_Home
×
1308
            elif key == QtCore.Qt.Key_Right:
×
1309
                key = QtCore.Qt.Key_End
×
1310
            elif key == QtCore.Qt.Key_Up:
×
1311
                ctrl_down = True
×
1312
                key = QtCore.Qt.Key_Home
×
1313
            elif key == QtCore.Qt.Key_Down:
×
1314
                ctrl_down = True
×
1315
                key = QtCore.Qt.Key_End
×
1316
        #------ Special modifier logic -----------------------------------------
1317

1318
        if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
2✔
1319
            intercepted = True
2✔
1320

1321
            # Special handling when tab completing in text mode.
1322
            self._cancel_completion()
2✔
1323

1324
            if self._in_buffer(position):
2✔
1325
                # Special handling when a reading a line of raw input.
1326
                if self._reading:
2✔
1327
                    self._append_plain_text('\n')
2✔
1328
                    self._reading = False
2✔
1329
                    if self._reading_callback:
2✔
1330
                        self._reading_callback()
2✔
1331

1332
                # If the input buffer is a single line or there is only
1333
                # whitespace after the cursor, execute. Otherwise, split the
1334
                # line with a continuation prompt.
1335
                elif not self._executing:
2✔
1336
                    cursor.movePosition(QtGui.QTextCursor.End,
2✔
1337
                                        QtGui.QTextCursor.KeepAnchor)
1338
                    at_end = len(cursor.selectedText().strip()) == 0
2✔
1339
                    single_line = (self._get_end_cursor().blockNumber() ==
2✔
1340
                                   self._get_prompt_cursor().blockNumber())
1341
                    if (at_end or shift_down or single_line) and not ctrl_down:
2✔
1342
                        self.execute(interactive = not shift_down)
2✔
1343
                    else:
1344
                        # Do this inside an edit block for clean undo/redo.
1345
                        pos = self._get_input_buffer_cursor_pos()
×
1346
                        def callback(complete, indent):
×
1347
                            try:
×
1348
                                cursor.beginEditBlock()
×
1349
                                cursor.setPosition(position)
×
1350
                                cursor.insertText('\n')
×
1351
                                self._insert_continuation_prompt(cursor)
×
1352
                                if indent:
×
1353
                                    cursor.insertText(indent)
×
1354
                            finally:
1355
                                cursor.endEditBlock()
×
1356

1357
                            # Ensure that the whole input buffer is visible.
1358
                            # FIXME: This will not be usable if the input buffer is
1359
                            # taller than the console widget.
1360
                            self._control.moveCursor(QtGui.QTextCursor.End)
×
1361
                            self._control.setTextCursor(cursor)
×
1362
                        self._register_is_complete_callback(
×
1363
                            self._get_input_buffer()[:pos], callback)
1364

1365
        #------ Control/Cmd modifier -------------------------------------------
1366

1367
        elif ctrl_down:
2✔
1368
            if key == QtCore.Qt.Key_G:
2✔
1369
                self._keyboard_quit()
×
1370
                intercepted = True
×
1371

1372
            elif key == QtCore.Qt.Key_K:
2✔
1373
                if self._in_buffer(position):
2✔
1374
                    cursor.clearSelection()
2✔
1375
                    cursor.movePosition(QtGui.QTextCursor.EndOfLine,
2✔
1376
                                        QtGui.QTextCursor.KeepAnchor)
1377
                    if not cursor.hasSelection():
2✔
1378
                        # Line deletion (remove continuation prompt)
1379
                        cursor.movePosition(QtGui.QTextCursor.NextBlock,
×
1380
                                            QtGui.QTextCursor.KeepAnchor)
1381
                        cursor.movePosition(QtGui.QTextCursor.Right,
×
1382
                                            QtGui.QTextCursor.KeepAnchor,
1383
                                            len(self._continuation_prompt))
1384
                    self._kill_ring.kill_cursor(cursor)
2✔
1385
                    self._set_cursor(cursor)
2✔
1386
                intercepted = True
2✔
1387

1388
            elif key == QtCore.Qt.Key_L:
2✔
1389
                self.prompt_to_top()
×
1390
                intercepted = True
×
1391

1392
            elif key == QtCore.Qt.Key_O:
2✔
1393
                if self._page_control and self._page_control.isVisible():
×
1394
                    self._page_control.setFocus()
×
1395
                intercepted = True
×
1396

1397
            elif key == QtCore.Qt.Key_U:
2✔
1398
                if self._in_buffer(position):
×
1399
                    cursor.clearSelection()
×
1400
                    start_line = cursor.blockNumber()
×
1401
                    if start_line == self._get_prompt_cursor().blockNumber():
×
1402
                        offset = len(self._prompt)
×
1403
                    else:
1404
                        offset = len(self._continuation_prompt)
×
1405
                    cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
×
1406
                                        QtGui.QTextCursor.KeepAnchor)
1407
                    cursor.movePosition(QtGui.QTextCursor.Right,
×
1408
                                        QtGui.QTextCursor.KeepAnchor, offset)
1409
                    self._kill_ring.kill_cursor(cursor)
×
1410
                    self._set_cursor(cursor)
×
1411
                intercepted = True
×
1412

1413
            elif key == QtCore.Qt.Key_Y:
2✔
1414
                self._keep_cursor_in_buffer()
×
1415
                self._kill_ring.yank()
×
1416
                intercepted = True
×
1417

1418
            elif key in (QtCore.Qt.Key_Backspace, QtCore.Qt.Key_Delete):
2✔
1419
                if key == QtCore.Qt.Key_Backspace:
2✔
1420
                    cursor = self._get_word_start_cursor(position)
2✔
1421
                else: # key == QtCore.Qt.Key_Delete
1422
                    cursor = self._get_word_end_cursor(position)
2✔
1423
                cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
2✔
1424
                self._kill_ring.kill_cursor(cursor)
2✔
1425
                intercepted = True
2✔
1426

1427
            elif key == QtCore.Qt.Key_D:
2✔
1428
                if len(self.input_buffer) == 0 and not self._executing:
×
1429
                    self.exit_requested.emit(self)
×
1430
                # if executing and input buffer empty
1431
                elif len(self._get_input_buffer(force=True)) == 0:
×
1432
                    # input a EOT ansi control character
1433
                    self._control.textCursor().insertText(chr(4))
×
1434
                    new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
×
1435
                                                QtCore.Qt.Key_Return,
1436
                                                QtCore.Qt.NoModifier)
1437
                    QtWidgets.QApplication.instance().sendEvent(self._control, new_event)
×
1438
                    intercepted = True
×
1439
                else:
1440
                    new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
×
1441
                                                QtCore.Qt.Key_Delete,
1442
                                                QtCore.Qt.NoModifier)
1443
                    QtWidgets.QApplication.instance().sendEvent(self._control, new_event)
×
1444
                    intercepted = True
×
1445

1446
            elif key == QtCore.Qt.Key_Down:
2✔
1447
                self._scroll_to_end()
×
1448

1449
            elif key == QtCore.Qt.Key_Up:
2✔
1450
                self._control.verticalScrollBar().setValue(0)
×
1451
        #------ Alt modifier ---------------------------------------------------
1452

1453
        elif alt_down:
2✔
1454
            if key == QtCore.Qt.Key_B:
×
1455
                self._set_cursor(self._get_word_start_cursor(position))
×
1456
                intercepted = True
×
1457

1458
            elif key == QtCore.Qt.Key_F:
×
1459
                self._set_cursor(self._get_word_end_cursor(position))
×
1460
                intercepted = True
×
1461

1462
            elif key == QtCore.Qt.Key_Y:
×
1463
                self._kill_ring.rotate()
×
1464
                intercepted = True
×
1465

1466
            elif key == QtCore.Qt.Key_Backspace:
×
1467
                cursor = self._get_word_start_cursor(position)
×
1468
                cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
×
1469
                self._kill_ring.kill_cursor(cursor)
×
1470
                intercepted = True
×
1471

1472
            elif key == QtCore.Qt.Key_D:
×
1473
                cursor = self._get_word_end_cursor(position)
×
1474
                cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
×
1475
                self._kill_ring.kill_cursor(cursor)
×
1476
                intercepted = True
×
1477

1478
            elif key == QtCore.Qt.Key_Delete:
×
1479
                intercepted = True
×
1480

1481
            elif key == QtCore.Qt.Key_Greater:
×
1482
                self._control.moveCursor(QtGui.QTextCursor.End)
×
1483
                intercepted = True
×
1484

1485
            elif key == QtCore.Qt.Key_Less:
×
1486
                self._control.setTextCursor(self._get_prompt_cursor())
×
1487
                intercepted = True
×
1488

1489
        #------ No modifiers ---------------------------------------------------
1490

1491
        else:
1492
            self._trigger_is_complete_callback()
2✔
1493
            if shift_down:
2✔
1494
                anchormode = QtGui.QTextCursor.KeepAnchor
2✔
1495
            else:
1496
                anchormode = QtGui.QTextCursor.MoveAnchor
2✔
1497

1498
            if key == QtCore.Qt.Key_Escape:
2✔
1499
                self._keyboard_quit()
×
1500
                intercepted = True
×
1501

1502
            elif key == QtCore.Qt.Key_Up and not shift_down:
2✔
1503
                if self._reading or not self._up_pressed(shift_down):
×
1504
                    intercepted = True
×
1505
                else:
1506
                    prompt_line = self._get_prompt_cursor().blockNumber()
×
1507
                    intercepted = cursor.blockNumber() <= prompt_line
×
1508

1509
            elif key == QtCore.Qt.Key_Down and not shift_down:
2✔
1510
                if self._reading or not self._down_pressed(shift_down):
×
1511
                    intercepted = True
×
1512
                else:
1513
                    end_line = self._get_end_cursor().blockNumber()
×
1514
                    intercepted = cursor.blockNumber() == end_line
×
1515

1516
            elif key == QtCore.Qt.Key_Tab:
2✔
1517
                if not self._reading:
2✔
1518
                    if self._tab_pressed():
2✔
1519
                        self._indent(dedent=False)
2✔
1520
                    intercepted = True
2✔
1521

1522
            elif key == QtCore.Qt.Key_Backtab:
2✔
1523
                self._indent(dedent=True)
2✔
1524
                intercepted = True
2✔
1525

1526
            elif key == QtCore.Qt.Key_Left and not shift_down:
2✔
1527

1528
                # Move to the previous line
1529
                line, col = cursor.blockNumber(), cursor.columnNumber()
2✔
1530
                if line > self._get_prompt_cursor().blockNumber() and \
2✔
1531
                        col == len(self._continuation_prompt):
1532
                    self._control.moveCursor(QtGui.QTextCursor.PreviousBlock,
2✔
1533
                                             mode=anchormode)
1534
                    self._control.moveCursor(QtGui.QTextCursor.EndOfBlock,
2✔
1535
                                             mode=anchormode)
1536
                    intercepted = True
2✔
1537

1538
                # Regular left movement
1539
                else:
1540
                    intercepted = not self._in_buffer(position - 1)
2✔
1541

1542
            elif key == QtCore.Qt.Key_Right and not shift_down:
2✔
1543
                #original_block_number = cursor.blockNumber()
1544
                if position == self._get_line_end_pos():
2✔
1545
                    cursor.movePosition(QtGui.QTextCursor.NextBlock, mode=anchormode)
2✔
1546
                    cursor.movePosition(QtGui.QTextCursor.Right,
2✔
1547
                                        mode=anchormode,
1548
                                        n=len(self._continuation_prompt))
1549
                    self._control.setTextCursor(cursor)
2✔
1550
                else:
1551
                    self._control.moveCursor(QtGui.QTextCursor.Right,
×
1552
                                             mode=anchormode)
1553
                intercepted = True
2✔
1554

1555
            elif key == QtCore.Qt.Key_Home:
2✔
1556
                start_pos = self._get_line_start_pos()
×
1557

1558
                c = self._get_cursor()
×
1559
                spaces = self._get_leading_spaces()
×
1560
                if (c.position() > start_pos + spaces or
×
1561
                        c.columnNumber() == len(self._continuation_prompt)):
1562
                    start_pos += spaces     # Beginning of text
×
1563

1564
                if shift_down and self._in_buffer(position):
×
1565
                    if c.selectedText():
×
1566
                        sel_max = max(c.selectionStart(), c.selectionEnd())
×
1567
                        cursor.setPosition(sel_max,
×
1568
                                           QtGui.QTextCursor.MoveAnchor)
1569
                    cursor.setPosition(start_pos, QtGui.QTextCursor.KeepAnchor)
×
1570
                else:
1571
                    cursor.setPosition(start_pos)
×
1572
                self._set_cursor(cursor)
×
1573
                intercepted = True
×
1574

1575
            elif key == QtCore.Qt.Key_Backspace:
2✔
1576

1577
                # Line deletion (remove continuation prompt)
1578
                line, col = cursor.blockNumber(), cursor.columnNumber()
2✔
1579
                if not self._reading and \
2✔
1580
                        col == len(self._continuation_prompt) and \
1581
                        line > self._get_prompt_cursor().blockNumber():
1582
                    cursor.beginEditBlock()
×
1583
                    cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
×
1584
                                        QtGui.QTextCursor.KeepAnchor)
1585
                    cursor.removeSelectedText()
×
1586
                    cursor.deletePreviousChar()
×
1587
                    cursor.endEditBlock()
×
1588
                    intercepted = True
×
1589

1590
                # Regular backwards deletion
1591
                else:
1592
                    anchor = cursor.anchor()
2✔
1593
                    if anchor == position:
2✔
1594
                        intercepted = not self._in_buffer(position - 1)
2✔
1595
                    else:
1596
                        intercepted = not self._in_buffer(min(anchor, position))
×
1597

1598
            elif key == QtCore.Qt.Key_Delete:
2✔
1599

1600
                # Line deletion (remove continuation prompt)
1601
                if not self._reading and self._in_buffer(position) and \
×
1602
                        cursor.atBlockEnd() and not cursor.hasSelection():
1603
                    cursor.movePosition(QtGui.QTextCursor.NextBlock,
×
1604
                                        QtGui.QTextCursor.KeepAnchor)
1605
                    cursor.movePosition(QtGui.QTextCursor.Right,
×
1606
                                        QtGui.QTextCursor.KeepAnchor,
1607
                                        len(self._continuation_prompt))
1608
                    cursor.removeSelectedText()
×
1609
                    intercepted = True
×
1610

1611
                # Regular forwards deletion:
1612
                else:
1613
                    anchor = cursor.anchor()
×
1614
                    intercepted = (not self._in_buffer(anchor) or
×
1615
                                   not self._in_buffer(position))
1616

1617
        #------ Special sequences ----------------------------------------------
1618

1619
        if not intercepted:
2✔
1620
            if event.matches(QtGui.QKeySequence.Copy):
2✔
1621
                self.copy()
2✔
1622
                intercepted = True
2✔
1623

1624
            elif event.matches(QtGui.QKeySequence.Cut):
2✔
1625
                self.cut()
×
1626
                intercepted = True
×
1627

1628
            elif event.matches(QtGui.QKeySequence.Paste):
2✔
1629
                self.paste()
2✔
1630
                intercepted = True
2✔
1631

1632
        # Don't move the cursor if Control/Cmd is pressed to allow copy-paste
1633
        # using the keyboard in any part of the buffer. Also, permit scrolling
1634
        # with Page Up/Down keys. Finally, if we're executing, don't move the
1635
        # cursor (if even this made sense, we can't guarantee that the prompt
1636
        # position is still valid due to text truncation).
1637
        if not (self._control_key_down(event.modifiers(), include_command=True)
2✔
1638
                or key in (QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown)
1639
                or (self._executing and not self._reading)
1640
                or (event.text() == "" and not
1641
                    (not shift_down and key in (QtCore.Qt.Key_Up, QtCore.Qt.Key_Down)))):
1642
            self._keep_cursor_in_buffer()
2✔
1643

1644
        return intercepted
2✔
1645

1646
    def _event_filter_page_keypress(self, event):
2✔
1647
        """ Filter key events for the paging widget to create console-like
1648
            interface.
1649
        """
1650
        key = event.key()
×
1651
        ctrl_down = self._control_key_down(event.modifiers())
×
1652
        alt_down = event.modifiers() & QtCore.Qt.AltModifier
×
1653

1654
        if ctrl_down:
×
1655
            if key == QtCore.Qt.Key_O:
×
1656
                self._control.setFocus()
×
1657
                return True
×
1658

1659
        elif alt_down:
×
1660
            if key == QtCore.Qt.Key_Greater:
×
1661
                self._page_control.moveCursor(QtGui.QTextCursor.End)
×
1662
                return True
×
1663

1664
            elif key == QtCore.Qt.Key_Less:
×
1665
                self._page_control.moveCursor(QtGui.QTextCursor.Start)
×
1666
                return True
×
1667

1668
        elif key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
×
1669
            if self._splitter:
×
1670
                self._page_control.hide()
×
1671
                self._control.setFocus()
×
1672
            else:
1673
                self.layout().setCurrentWidget(self._control)
×
1674
                # re-enable buffer truncation after paging
1675
                self._control.document().setMaximumBlockCount(self.buffer_size)
×
1676
            return True
×
1677

1678
        elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return,
×
1679
                     QtCore.Qt.Key_Tab):
1680
            new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
×
1681
                                        QtCore.Qt.Key_PageDown,
1682
                                        QtCore.Qt.NoModifier)
1683
            QtWidgets.QApplication.instance().sendEvent(self._page_control, new_event)
×
1684
            return True
×
1685

1686
        elif key == QtCore.Qt.Key_Backspace:
×
1687
            new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
×
1688
                                        QtCore.Qt.Key_PageUp,
1689
                                        QtCore.Qt.NoModifier)
1690
            QtWidgets.QApplication.instance().sendEvent(self._page_control, new_event)
×
1691
            return True
×
1692

1693
        # vi/less -like key bindings
1694
        elif key == QtCore.Qt.Key_J:
×
1695
            new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
×
1696
                                        QtCore.Qt.Key_Down,
1697
                                        QtCore.Qt.NoModifier)
1698
            QtWidgets.QApplication.instance().sendEvent(self._page_control, new_event)
×
1699
            return True
×
1700

1701
        # vi/less -like key bindings
1702
        elif key == QtCore.Qt.Key_K:
×
1703
            new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
×
1704
                                        QtCore.Qt.Key_Up,
1705
                                        QtCore.Qt.NoModifier)
1706
            QtWidgets.QApplication.instance().sendEvent(self._page_control, new_event)
×
1707
            return True
×
1708

1709
        return False
×
1710

1711
    def _on_flush_pending_stream_timer(self):
2✔
1712
        """ Flush pending text into the widget on console timer trigger.
1713
        """
1714
        self._flush_pending_stream()
2✔
1715

1716
    def _flush_pending_stream(self):
2✔
1717
        """
1718
        Flush pending text into the widget.
1719

1720
        It only applies to text that is pending when the console is in the
1721
        running state. Text printed when console is not running is shown
1722
        immediately, and does not wait to be flushed.
1723
        """
1724
        text = self._pending_insert_text
2✔
1725
        self._pending_insert_text = []
2✔
1726
        buffer_size = self._control.document().maximumBlockCount()
2✔
1727
        if buffer_size > 0:
2✔
1728
            text = self._get_last_lines_from_list(text, buffer_size)
2✔
1729
        text = ''.join(text)
2✔
1730
        t = time.time()
2✔
1731
        self._insert_plain_text(self._insert_text_cursor, text, flush=True)
2✔
1732
        # Set the flush interval to equal the maximum time to update text.
1733
        self._pending_text_flush_interval.setInterval(
2✔
1734
            int(max(100, (time.time() - t) * 1000))
1735
        )
1736

1737
    def _get_cursor(self):
2✔
1738
        """ Get a cursor at the current insert position.
1739
        """
1740
        return self._control.textCursor()
2✔
1741

1742
    def _get_end_cursor(self):
2✔
1743
        """ Get a cursor at the last character of the current cell.
1744
        """
1745
        cursor = self._control.textCursor()
2✔
1746
        cursor.movePosition(QtGui.QTextCursor.End)
2✔
1747
        return cursor
2✔
1748

1749
    def _get_end_pos(self):
2✔
1750
        """ Get the position of the last character of the current cell.
1751
        """
1752
        return self._get_end_cursor().position()
2✔
1753

1754
    def _get_line_start_cursor(self):
2✔
1755
        """ Get a cursor at the first character of the current line.
1756
        """
1757
        cursor = self._control.textCursor()
2✔
1758
        start_line = cursor.blockNumber()
2✔
1759
        if start_line == self._get_prompt_cursor().blockNumber():
2✔
1760
            cursor.setPosition(self._prompt_pos)
2✔
1761
        else:
1762
            cursor.movePosition(QtGui.QTextCursor.StartOfLine)
2✔
1763
            cursor.setPosition(cursor.position() +
2✔
1764
                               len(self._continuation_prompt))
1765
        return cursor
2✔
1766

1767
    def _get_line_start_pos(self):
2✔
1768
        """ Get the position of the first character of the current line.
1769
        """
1770
        return self._get_line_start_cursor().position()
2✔
1771

1772
    def _get_line_end_cursor(self):
2✔
1773
        """ Get a cursor at the last character of the current line.
1774
        """
1775
        cursor = self._control.textCursor()
2✔
1776
        cursor.movePosition(QtGui.QTextCursor.EndOfLine)
2✔
1777
        return cursor
2✔
1778

1779
    def _get_line_end_pos(self):
2✔
1780
        """ Get the position of the last character of the current line.
1781
        """
1782
        return self._get_line_end_cursor().position()
2✔
1783

1784
    def _get_input_buffer_cursor_column(self):
2✔
1785
        """ Get the column of the cursor in the input buffer, excluding the
1786
            contribution by the prompt, or -1 if there is no such column.
1787
        """
1788
        prompt = self._get_input_buffer_cursor_prompt()
2✔
1789
        if prompt is None:
2✔
1790
            return -1
2✔
1791
        else:
1792
            cursor = self._control.textCursor()
×
1793
            return cursor.columnNumber() - len(prompt)
×
1794

1795
    def _get_input_buffer_cursor_line(self):
2✔
1796
        """ Get the text of the line of the input buffer that contains the
1797
            cursor, or None if there is no such line.
1798
        """
1799
        prompt = self._get_input_buffer_cursor_prompt()
×
1800
        if prompt is None:
×
1801
            return None
×
1802
        else:
1803
            cursor = self._control.textCursor()
×
1804
            text = cursor.block().text()
×
1805
            return text[len(prompt):]
×
1806

1807
    def _get_input_buffer_cursor_pos(self):
2✔
1808
        """Get the cursor position within the input buffer."""
1809
        cursor = self._control.textCursor()
2✔
1810
        cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
2✔
1811
        input_buffer = cursor.selection().toPlainText()
2✔
1812

1813
        # Don't count continuation prompts
1814
        return len(input_buffer.replace('\n' + self._continuation_prompt, '\n'))
2✔
1815

1816
    def _get_input_buffer_cursor_prompt(self):
2✔
1817
        """ Returns the (plain text) prompt for line of the input buffer that
1818
            contains the cursor, or None if there is no such line.
1819
        """
1820
        if self._executing:
2✔
1821
            return None
2✔
1822
        cursor = self._control.textCursor()
×
1823
        if cursor.position() >= self._prompt_pos:
×
1824
            if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
×
1825
                return self._prompt
×
1826
            else:
1827
                return self._continuation_prompt
×
1828
        else:
1829
            return None
×
1830

1831
    def _get_last_lines(self, text, num_lines, return_count=False):
2✔
1832
        """ Get the last specified number of lines of text (like `tail -n`).
1833
        If return_count is True, returns a tuple of clipped text and the
1834
        number of lines in the clipped text.
1835
        """
1836
        pos = len(text)
2✔
1837
        if pos < num_lines:
2✔
1838
            if return_count:
2✔
1839
                return text, text.count('\n') if return_count else text
2✔
1840
            else:
1841
                return text
2✔
1842
        i = 0
2✔
1843
        while i < num_lines:
2✔
1844
            pos = text.rfind('\n', None, pos)
2✔
1845
            if pos == -1:
2✔
1846
                pos = None
2✔
1847
                break
2✔
1848
            i += 1
2✔
1849
        if return_count:
2✔
1850
            return text[pos:], i
2✔
1851
        else:
1852
            return text[pos:]
2✔
1853

1854
    def _get_last_lines_from_list(self, text_list, num_lines):
2✔
1855
        """ Get the list of text clipped to last specified lines.
1856
        """
1857
        ret = []
2✔
1858
        lines_pending = num_lines
2✔
1859
        for text in reversed(text_list):
2✔
1860
            text, lines_added = self._get_last_lines(text, lines_pending,
2✔
1861
                                                     return_count=True)
1862
            ret.append(text)
2✔
1863
            lines_pending -= lines_added
2✔
1864
            if lines_pending <= 0:
2✔
1865
                break
×
1866
        return ret[::-1]
2✔
1867

1868
    def _get_leading_spaces(self):
2✔
1869
        """ Get the number of leading spaces of the current line.
1870
        """
1871

1872
        cursor = self._get_cursor()
2✔
1873
        start_line = cursor.blockNumber()
2✔
1874
        if start_line == self._get_prompt_cursor().blockNumber():
2✔
1875
            # first line
1876
            offset = len(self._prompt)
2✔
1877
        else:
1878
            # continuation
1879
            offset = len(self._continuation_prompt)
2✔
1880
        cursor.select(QtGui.QTextCursor.LineUnderCursor)
2✔
1881
        text = cursor.selectedText()[offset:]
2✔
1882
        return len(text) - len(text.lstrip())
2✔
1883

1884
    @property
2✔
1885
    def _prompt_pos(self):
2✔
1886
        """ Find the position in the text right after the prompt.
1887
        """
1888
        return min(self._prompt_cursor.position() + 1, self._get_end_pos())
2✔
1889

1890
    @property
2✔
1891
    def _append_before_prompt_pos(self):
2✔
1892
        """ Find the position in the text right before the prompt.
1893
        """
1894
        return min(self._append_before_prompt_cursor.position(),
2✔
1895
                   self._get_end_pos())
1896

1897
    def _get_prompt_cursor(self):
2✔
1898
        """ Get a cursor at the prompt position of the current cell.
1899
        """
1900
        cursor = self._control.textCursor()
2✔
1901
        cursor.setPosition(self._prompt_pos)
2✔
1902
        return cursor
2✔
1903

1904
    def _get_selection_cursor(self, start, end):
2✔
1905
        """ Get a cursor with text selected between the positions 'start' and
1906
            'end'.
1907
        """
1908
        cursor = self._control.textCursor()
×
1909
        cursor.setPosition(start)
×
1910
        cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
×
1911
        return cursor
×
1912

1913
    def _get_word_start_cursor(self, position):
2✔
1914
        """ Find the start of the word to the left the given position. If a
1915
            sequence of non-word characters precedes the first word, skip over
1916
            them. (This emulates the behavior of bash, emacs, etc.)
1917
        """
1918
        document = self._control.document()
2✔
1919
        cursor = self._control.textCursor()
2✔
1920
        line_start_pos = self._get_line_start_pos()
2✔
1921

1922
        if position == self._prompt_pos:
2✔
1923
            return cursor
×
1924
        elif position == line_start_pos:
2✔
1925
            # Cursor is at the beginning of a line, move to the last
1926
            # non-whitespace character of the previous line
1927
            cursor = self._control.textCursor()
2✔
1928
            cursor.setPosition(position)
2✔
1929
            cursor.movePosition(QtGui.QTextCursor.PreviousBlock)
2✔
1930
            cursor.movePosition(QtGui.QTextCursor.EndOfBlock)
2✔
1931
            position = cursor.position()
2✔
1932
            while (
2✔
1933
                position >= self._prompt_pos and
1934
                is_whitespace(document.characterAt(position))
1935
            ):
1936
                position -= 1
2✔
1937
            cursor.setPosition(position + 1)
2✔
1938
        else:
1939
            position -= 1
2✔
1940

1941
            # Find the last alphanumeric char, but don't move across lines
1942
            while (
2✔
1943
                position >= self._prompt_pos and
1944
                position >= line_start_pos and
1945
                not is_letter_or_number(document.characterAt(position))
1946
            ):
1947
                position -= 1
2✔
1948

1949
            # Find the first alphanumeric char, but don't move across lines
1950
            while (
2✔
1951
                position >= self._prompt_pos and
1952
                position >= line_start_pos and
1953
                is_letter_or_number(document.characterAt(position))
1954
            ):
1955
                position -= 1
2✔
1956

1957
            cursor.setPosition(position + 1)
2✔
1958

1959
        return cursor
2✔
1960

1961
    def _get_word_end_cursor(self, position):
2✔
1962
        """ Find the end of the word to the right the given position. If a
1963
            sequence of non-word characters precedes the first word, skip over
1964
            them. (This emulates the behavior of bash, emacs, etc.)
1965
        """
1966
        document = self._control.document()
2✔
1967
        cursor = self._control.textCursor()
2✔
1968
        end_pos = self._get_end_pos()
2✔
1969
        line_end_pos = self._get_line_end_pos()
2✔
1970

1971
        if position == end_pos:
2✔
1972
            # Cursor is at the very end of the buffer
1973
            return cursor
×
1974
        elif position == line_end_pos:
2✔
1975
            # Cursor is at the end of a line, move to the first
1976
            # non-whitespace character of the next line
1977
            cursor = self._control.textCursor()
2✔
1978
            cursor.setPosition(position)
2✔
1979
            cursor.movePosition(QtGui.QTextCursor.NextBlock)
2✔
1980
            position = cursor.position() + len(self._continuation_prompt)
2✔
1981
            while (
2✔
1982
                position < end_pos and
1983
                is_whitespace(document.characterAt(position))
1984
            ):
1985
                position += 1
2✔
1986
            cursor.setPosition(position)
2✔
1987
        else:
1988
            if is_whitespace(document.characterAt(position)):
2✔
1989
                # The next character is whitespace. If this is part of
1990
                # indentation whitespace, skip to the first non-whitespace
1991
                # character.
1992
                is_indentation_whitespace = True
2✔
1993
                back_pos = position - 1
2✔
1994
                line_start_pos = self._get_line_start_pos()
2✔
1995
                while back_pos >= line_start_pos:
2✔
1996
                    if not is_whitespace(document.characterAt(back_pos)):
×
1997
                        is_indentation_whitespace = False
×
1998
                        break
×
1999
                    back_pos -= 1
×
2000
                if is_indentation_whitespace:
2✔
2001
                    # Skip to the first non-whitespace character
2002
                    while (
2✔
2003
                        position < end_pos and
2004
                        position < line_end_pos and
2005
                        is_whitespace(document.characterAt(position))
2006
                    ):
2007
                        position += 1
2✔
2008
                    cursor.setPosition(position)
2✔
2009
                    return cursor
2✔
2010

2011
            while (
2✔
2012
                position < end_pos and
2013
                position < line_end_pos and
2014
                not is_letter_or_number(document.characterAt(position))
2015
            ):
2016
                position += 1
2✔
2017

2018
            while (
2✔
2019
                position < end_pos and
2020
                position < line_end_pos and
2021
                is_letter_or_number(document.characterAt(position))
2022
            ):
2023
                position += 1
2✔
2024

2025
            cursor.setPosition(position)
2✔
2026
        return cursor
2✔
2027

2028
    def _indent(self, dedent=True):
2✔
2029
        """ Indent/Dedent current line or current text selection.
2030
        """
2031
        num_newlines = self._get_cursor().selectedText().count("\u2029")
2✔
2032
        save_cur = self._get_cursor()
2✔
2033
        cur = self._get_cursor()
2✔
2034

2035
        # move to first line of selection, if present
2036
        cur.setPosition(cur.selectionStart())
2✔
2037
        self._control.setTextCursor(cur)
2✔
2038
        spaces = self._get_leading_spaces()
2✔
2039
        # calculate number of spaces neded to align/indent to 4-space multiple
2040
        step = self._tab_width - (spaces % self._tab_width)
2✔
2041

2042
        # insertText shouldn't replace if selection is active
2043
        cur.clearSelection()
2✔
2044

2045
        # indent all lines in selection (ir just current) by `step`
2046
        for _ in range(num_newlines+1):
2✔
2047
            # update underlying cursor for _get_line_start_pos
2048
            self._control.setTextCursor(cur)
2✔
2049
            # move to first non-ws char on line
2050
            cur.setPosition(self._get_line_start_pos())
2✔
2051
            if dedent:
2✔
2052
                spaces = min(step, self._get_leading_spaces())
2✔
2053
                safe_step = spaces % self._tab_width
2✔
2054
                cur.movePosition(QtGui.QTextCursor.Right,
2✔
2055
                                 QtGui.QTextCursor.KeepAnchor,
2056
                                 min(spaces, safe_step if safe_step != 0
2057
                                    else self._tab_width))
2058
                cur.removeSelectedText()
2✔
2059
            else:
2060
                cur.insertText(' '*step)
2✔
2061
            cur.movePosition(QtGui.QTextCursor.Down)
2✔
2062

2063
        # restore cursor
2064
        self._control.setTextCursor(save_cur)
2✔
2065

2066
    def _insert_continuation_prompt(self, cursor, indent=''):
2✔
2067
        """ Inserts new continuation prompt using the specified cursor.
2068
        """
2069
        if self._continuation_prompt_html is None:
2✔
2070
            self._insert_plain_text(cursor, self._continuation_prompt)
2✔
2071
        else:
2072
            self._continuation_prompt = self._insert_html_fetching_plain_text(
2✔
2073
                cursor, self._continuation_prompt_html)
2074
        if indent:
2✔
2075
            cursor.insertText(indent)
2✔
2076

2077
    def _insert_block(self, cursor, block_format=None):
2✔
2078
        """ Inserts an empty QTextBlock using the specified cursor.
2079
        """
2080
        if block_format is None:
2✔
2081
            block_format = QtGui.QTextBlockFormat()
2✔
2082
        cursor.insertBlock(block_format)
2✔
2083

2084
    def _insert_html(self, cursor, html):
2✔
2085
        """ Inserts HTML using the specified cursor in such a way that future
2086
            formatting is unaffected.
2087
        """
2088
        cursor.beginEditBlock()
2✔
2089
        cursor.insertHtml(html)
2✔
2090

2091
        # After inserting HTML, the text document "remembers" it's in "html
2092
        # mode", which means that subsequent calls adding plain text will result
2093
        # in unwanted formatting, lost tab characters, etc. The following code
2094
        # hacks around this behavior, which I consider to be a bug in Qt, by
2095
        # (crudely) resetting the document's style state.
2096
        cursor.movePosition(QtGui.QTextCursor.Left,
2✔
2097
                            QtGui.QTextCursor.KeepAnchor)
2098
        if cursor.selection().toPlainText() == ' ':
2✔
2099
            cursor.removeSelectedText()
2✔
2100
        else:
2101
            cursor.movePosition(QtGui.QTextCursor.Right)
2✔
2102
        cursor.insertText(' ', QtGui.QTextCharFormat())
2✔
2103
        cursor.endEditBlock()
2✔
2104

2105
    def _insert_html_fetching_plain_text(self, cursor, html):
2✔
2106
        """ Inserts HTML using the specified cursor, then returns its plain text
2107
            version.
2108
        """
2109
        cursor.beginEditBlock()
2✔
2110
        cursor.removeSelectedText()
2✔
2111

2112
        start = cursor.position()
2✔
2113
        self._insert_html(cursor, html)
2✔
2114
        end = cursor.position()
2✔
2115
        cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
2✔
2116
        text = cursor.selection().toPlainText()
2✔
2117

2118
        cursor.setPosition(end)
2✔
2119
        cursor.endEditBlock()
2✔
2120
        return text
2✔
2121

2122
    def _viewport_at_end(self):
2✔
2123
        """Check if the viewport is at the end of the document."""
2124
        viewport = self._control.viewport()
2✔
2125
        end_scroll_pos = self._control.cursorForPosition(
2✔
2126
            QtCore.QPoint(viewport.width() - 1, viewport.height() - 1)
2127
            ).position()
2128
        end_doc_pos = self._get_end_pos()
2✔
2129
        return end_doc_pos - end_scroll_pos <= 1
2✔
2130

2131
    def _scroll_to_end(self):
2✔
2132
        """Scroll to the end of the document."""
2133
        end_scroll = (self._control.verticalScrollBar().maximum()
2✔
2134
                      - self._control.verticalScrollBar().pageStep())
2135
        # Only scroll down
2136
        if end_scroll > self._control.verticalScrollBar().value():
2✔
2137
            self._control.verticalScrollBar().setValue(end_scroll)
2✔
2138

2139
    def _insert_plain_text(self, cursor, text, flush=False):
2✔
2140
        """ Inserts plain text using the specified cursor, processing ANSI codes
2141
            if enabled.
2142
        """
2143
        should_autoscroll = self._viewport_at_end()
2✔
2144
        # maximumBlockCount() can be different from self.buffer_size in
2145
        # case input prompt is active.
2146
        buffer_size = self._control.document().maximumBlockCount()
2✔
2147

2148
        if (self._executing and not flush and
2✔
2149
                self._pending_text_flush_interval.isActive() and
2150
                cursor.position() == self._insert_text_cursor.position()):
2151
            # Queue the text to insert in case it is being inserted at end
2152
            self._pending_insert_text.append(text)
2✔
2153
            if buffer_size > 0:
2✔
2154
                self._pending_insert_text = self._get_last_lines_from_list(
2✔
2155
                    self._pending_insert_text, buffer_size)
2156
            return
2✔
2157

2158
        if self._executing and not self._pending_text_flush_interval.isActive():
2✔
2159
            self._pending_text_flush_interval.start()
2✔
2160

2161
        # Clip the text to last `buffer_size` lines.
2162
        if buffer_size > 0:
2✔
2163
            text = self._get_last_lines(text, buffer_size)
2✔
2164

2165
        cursor.beginEditBlock()
2✔
2166
        if self.ansi_codes:
2✔
2167
            for substring in self._ansi_processor.split_string(text):
2✔
2168
                for act in self._ansi_processor.actions:
2✔
2169

2170
                    # Unlike real terminal emulators, we don't distinguish
2171
                    # between the screen and the scrollback buffer. A screen
2172
                    # erase request clears everything.
2173
                    if act.action == 'erase':
2✔
2174
                        remove = False
2✔
2175
                        fill = False
2✔
2176
                        if act.area == 'screen':
2✔
2177
                            cursor.select(QtGui.QTextCursor.Document)
2✔
2178
                            remove = True
2✔
2179
                        if act.area == 'line':
2✔
2180
                            if act.erase_to == 'all':
2✔
2181
                                cursor.select(QtGui.QTextCursor.LineUnderCursor)
2✔
2182
                                remove = True
2✔
2183
                            elif act.erase_to == 'start':
2✔
2184
                                cursor.movePosition(
2✔
2185
                                    QtGui.QTextCursor.StartOfLine,
2186
                                    QtGui.QTextCursor.KeepAnchor)
2187
                                remove = True
2✔
2188
                                fill = True
2✔
2189
                            elif act.erase_to == 'end':
2✔
2190
                                cursor.movePosition(
2✔
2191
                                    QtGui.QTextCursor.EndOfLine,
2192
                                    QtGui.QTextCursor.KeepAnchor)
2193
                                remove = True
2✔
2194
                        if remove:
2✔
2195
                            nspace=cursor.selectionEnd()-cursor.selectionStart() if fill else 0
2✔
2196
                            cursor.removeSelectedText()
2✔
2197
                            if nspace>0: cursor.insertText(' '*nspace) # replace text by space, to keep cursor position as specified
2✔
2198

2199
                    # Simulate a form feed by scrolling just past the last line.
2200
                    elif act.action == 'scroll' and act.unit == 'page':
2✔
2201
                        cursor.insertText('\n')
×
2202
                        cursor.endEditBlock()
×
2203
                        self._set_top_cursor(cursor)
×
2204
                        cursor.joinPreviousEditBlock()
×
2205
                        cursor.deletePreviousChar()
×
2206

2207
                        if os.name == 'nt':
×
2208
                            cursor.select(QtGui.QTextCursor.Document)
×
2209
                            cursor.removeSelectedText()
×
2210

2211
                    elif act.action == 'move' and act.unit == 'line':
2✔
2212
                        if act.dir == 'up':
×
2213
                            for i in range(act.count):
×
2214
                                cursor.movePosition(
×
2215
                                    QtGui.QTextCursor.Up
2216
                                )
2217
                        elif act.dir == 'down':
×
2218
                            for i in range(act.count):
×
2219
                                cursor.movePosition(
×
2220
                                    QtGui.QTextCursor.Down
2221
                                )
2222
                        elif act.dir == 'leftup':
×
2223
                            for i in range(act.count):
×
2224
                                cursor.movePosition(
×
2225
                                    QtGui.QTextCursor.Up
2226
                                )
2227
                            cursor.movePosition(
×
2228
                                QtGui.QTextCursor.StartOfLine,
2229
                                QtGui.QTextCursor.MoveAnchor
2230
                            )
2231

2232
                    elif act.action == 'carriage-return':
2✔
2233
                        cursor.movePosition(
2✔
2234
                            QtGui.QTextCursor.StartOfLine,
2235
                            QtGui.QTextCursor.MoveAnchor)
2236

2237
                    elif act.action == 'beep':
2✔
2238
                        QtWidgets.QApplication.instance().beep()
×
2239

2240
                    elif act.action == 'backspace':
2✔
2241
                        if not cursor.atBlockStart():
2✔
2242
                            cursor.movePosition(
2✔
2243
                                QtGui.QTextCursor.PreviousCharacter,
2244
                                QtGui.QTextCursor.MoveAnchor)
2245

2246
                    elif act.action == 'newline':
2✔
2247
                        if (
2✔
2248
                            cursor.block() != cursor.document().lastBlock()
2249
                            and not cursor.document()
2250
                            .toPlainText()
2251
                            .endswith(self._prompt)
2252
                        ):
2253
                            cursor.movePosition(QtGui.QTextCursor.NextBlock)
2✔
2254
                        else:
2255
                            cursor.movePosition(
2✔
2256
                                QtGui.QTextCursor.EndOfLine,
2257
                                QtGui.QTextCursor.MoveAnchor,
2258
                            )
2259
                            cursor.insertText("\n")
2✔
2260

2261
                # simulate replacement mode
2262
                if substring is not None:
2✔
2263
                    format = self._ansi_processor.get_format()
2✔
2264

2265
                    # Note that using _insert_mode means the \r ANSI sequence will not swallow characters.
2266
                    if not (hasattr(cursor, '_insert_mode') and cursor._insert_mode):
2✔
2267
                        pos = cursor.position()
2✔
2268
                        cursor2 = QtGui.QTextCursor(cursor)  # self._get_line_end_pos() is the previous line, don't use it
2✔
2269
                        cursor2.movePosition(QtGui.QTextCursor.EndOfLine)
2✔
2270
                        remain = cursor2.position() - pos    # number of characters until end of line
2✔
2271
                        n=len(substring)
2✔
2272
                        swallow = min(n, remain)             # number of character to swallow
2✔
2273
                        cursor.setPosition(pos + swallow, QtGui.QTextCursor.KeepAnchor)
2✔
2274
                    cursor.insertText(substring, format)
2✔
2275
        else:
2276
            cursor.insertText(text)
×
2277
        cursor.endEditBlock()
2✔
2278

2279
        if should_autoscroll:
2✔
2280
            self._scroll_to_end()
2✔
2281

2282
    def _insert_plain_text_into_buffer(self, cursor, text):
2✔
2283
        """ Inserts text into the input buffer using the specified cursor (which
2284
            must be in the input buffer), ensuring that continuation prompts are
2285
            inserted as necessary.
2286
        """
2287
        lines = text.splitlines(True)
2✔
2288
        if lines:
2✔
2289
            if lines[-1].endswith('\n'):
2✔
2290
                # If the text ends with a newline, add a blank line so a new
2291
                # continuation prompt is produced.
2292
                lines.append('')
2✔
2293
            cursor.beginEditBlock()
2✔
2294
            cursor.insertText(lines[0])
2✔
2295
            for line in lines[1:]:
2✔
2296
                if self._continuation_prompt_html is None:
2✔
2297
                    cursor.insertText(self._continuation_prompt)
2✔
2298
                else:
2299
                    self._continuation_prompt = \
2✔
2300
                        self._insert_html_fetching_plain_text(
2301
                            cursor, self._continuation_prompt_html)
2302
                cursor.insertText(line)
2✔
2303
            cursor.endEditBlock()
2✔
2304

2305
    def _in_buffer(self, position):
2✔
2306
        """
2307
        Returns whether the specified position is inside the editing region.
2308
        """
2309
        return position == self._move_position_in_buffer(position)
2✔
2310

2311
    def _move_position_in_buffer(self, position):
2✔
2312
        """
2313
        Return the next position in buffer.
2314
        """
2315
        cursor = self._control.textCursor()
2✔
2316
        cursor.setPosition(position)
2✔
2317
        line = cursor.blockNumber()
2✔
2318
        prompt_line = self._get_prompt_cursor().blockNumber()
2✔
2319
        if line == prompt_line:
2✔
2320
            if position >= self._prompt_pos:
2✔
2321
                return position
2✔
2322
            return self._prompt_pos
2✔
2323
        if line > prompt_line:
2✔
2324
            cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
2✔
2325
            prompt_pos = cursor.position() + len(self._continuation_prompt)
2✔
2326
            if position >= prompt_pos:
2✔
2327
                return position
2✔
2328
            return prompt_pos
2✔
2329
        return self._prompt_pos
×
2330

2331
    def _keep_cursor_in_buffer(self):
2✔
2332
        """ Ensures that the cursor is inside the editing region. Returns
2333
            whether the cursor was moved.
2334
        """
2335
        cursor = self._control.textCursor()
2✔
2336
        endpos = cursor.selectionEnd()
2✔
2337

2338
        if endpos < self._prompt_pos:
2✔
2339
            cursor.setPosition(endpos)
×
2340
            line = cursor.blockNumber()
×
2341
            prompt_line = self._get_prompt_cursor().blockNumber()
×
2342
            if line == prompt_line:
×
2343
                # Cursor is on prompt line, move to start of buffer
2344
                cursor.setPosition(self._prompt_pos)
×
2345
            else:
2346
                # Cursor is not in buffer, move to the end
2347
                cursor.movePosition(QtGui.QTextCursor.End)
×
2348
            self._control.setTextCursor(cursor)
×
2349
            return True
×
2350

2351
        startpos = cursor.selectionStart()
2✔
2352

2353
        new_endpos = self._move_position_in_buffer(endpos)
2✔
2354
        new_startpos = self._move_position_in_buffer(startpos)
2✔
2355
        if new_endpos == endpos and new_startpos == startpos:
2✔
2356
            return False
2✔
2357

2358
        cursor.setPosition(new_startpos)
2✔
2359
        cursor.setPosition(new_endpos, QtGui.QTextCursor.KeepAnchor)
2✔
2360
        self._control.setTextCursor(cursor)
2✔
2361
        return True
2✔
2362

2363
    def _keyboard_quit(self):
2✔
2364
        """ Cancels the current editing task ala Ctrl-G in Emacs.
2365
        """
2366
        if self._temp_buffer_filled :
×
2367
            self._cancel_completion()
×
2368
            self._clear_temporary_buffer()
×
2369
        else:
2370
            self.input_buffer = ''
×
2371

2372
    def _page(self, text, html=False):
2✔
2373
        """ Displays text using the pager if it exceeds the height of the
2374
        viewport.
2375

2376
        Parameters
2377
        ----------
2378
        html : bool, optional (default False)
2379
            If set, the text will be interpreted as HTML instead of plain text.
2380
        """
2381
        line_height = QtGui.QFontMetrics(self.font).height()
×
2382
        minlines = self._control.viewport().height() / line_height
×
2383
        if self.paging != 'none' and \
×
2384
                re.match("(?:[^\n]*\n){%i}" % minlines, text):
2385
            if self.paging == 'custom':
×
2386
                self.custom_page_requested.emit(text)
×
2387
            else:
2388
                # disable buffer truncation during paging
2389
                self._control.document().setMaximumBlockCount(0)
×
2390
                self._page_control.clear()
×
2391
                cursor = self._page_control.textCursor()
×
2392
                if html:
×
2393
                    self._insert_html(cursor, text)
×
2394
                else:
2395
                    self._insert_plain_text(cursor, text)
×
2396
                self._page_control.moveCursor(QtGui.QTextCursor.Start)
×
2397

2398
                self._page_control.viewport().resize(self._control.size())
×
2399
                if self._splitter:
×
2400
                    self._page_control.show()
×
2401
                    self._page_control.setFocus()
×
2402
                else:
2403
                    self.layout().setCurrentWidget(self._page_control)
×
2404
        elif html:
×
2405
            self._append_html(text)
×
2406
        else:
2407
            self._append_plain_text(text)
×
2408

2409
    def _set_paging(self, paging):
2✔
2410
        """
2411
        Change the pager to `paging` style.
2412

2413
        Parameters
2414
        ----------
2415
        paging : string
2416
            Either "hsplit", "vsplit", or "inside"
2417
        """
2418
        if self._splitter is None:
×
2419
            raise NotImplementedError("""can only switch if --paging=hsplit or
×
2420
                    --paging=vsplit is used.""")
2421
        if paging == 'hsplit':
×
2422
            self._splitter.setOrientation(QtCore.Qt.Horizontal)
×
2423
        elif paging == 'vsplit':
×
2424
            self._splitter.setOrientation(QtCore.Qt.Vertical)
×
2425
        elif paging == 'inside':
×
2426
            raise NotImplementedError("""switching to 'inside' paging not
×
2427
                    supported yet.""")
2428
        else:
2429
            raise ValueError("unknown paging method '%s'" % paging)
×
2430
        self.paging = paging
×
2431

2432
    def _prompt_finished(self):
2✔
2433
        """ Called immediately after a prompt is finished, i.e. when some input
2434
            will be processed and a new prompt displayed.
2435
        """
2436
        self._control.setReadOnly(True)
2✔
2437
        self._prompt_finished_hook()
2✔
2438

2439
    def _prompt_started(self):
2✔
2440
        """ Called immediately after a new prompt is displayed.
2441
        """
2442
        # Temporarily disable the maximum block count to permit undo/redo and
2443
        # to ensure that the prompt position does not change due to truncation.
2444
        self._control.document().setMaximumBlockCount(0)
2✔
2445
        self._control.setUndoRedoEnabled(True)
2✔
2446

2447
        # Work around bug in QPlainTextEdit: input method is not re-enabled
2448
        # when read-only is disabled.
2449
        self._control.setReadOnly(False)
2✔
2450
        self._control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
2✔
2451

2452
        if not self._reading:
2✔
2453
            self._executing = False
2✔
2454
        self._prompt_started_hook()
2✔
2455

2456
        # If the input buffer has changed while executing, load it.
2457
        if self._input_buffer_pending:
2✔
2458
            self.input_buffer = self._input_buffer_pending
×
2459
            self._input_buffer_pending = ''
×
2460

2461
        self._control.moveCursor(QtGui.QTextCursor.End)
2✔
2462

2463
    def _readline(self, prompt='', callback=None, password=False):
2✔
2464
        """ Reads one line of input from the user.
2465

2466
        Parameters
2467
        ----------
2468
        prompt : str, optional
2469
            The prompt to print before reading the line.
2470

2471
        callback : callable, optional
2472
            A callback to execute with the read line. If not specified, input is
2473
            read *synchronously* and this method does not return until it has
2474
            been read.
2475

2476
        Returns
2477
        -------
2478
        If a callback is specified, returns nothing. Otherwise, returns the
2479
        input string with the trailing newline stripped.
2480
        """
2481
        if self._reading:
2✔
2482
            raise RuntimeError('Cannot read a line. Widget is already reading.')
×
2483

2484
        if not callback and not self.isVisible():
2✔
2485
            # If the user cannot see the widget, this function cannot return.
2486
            raise RuntimeError('Cannot synchronously read a line if the widget '
×
2487
                               'is not visible!')
2488

2489
        self._reading = True
2✔
2490
        if password:
2✔
2491
            self._show_prompt('Warning: QtConsole does not support password mode, '
×
2492
                              'the text you type will be visible.', newline=True)
2493

2494
        if 'ipdb' not in prompt.lower():
2✔
2495
            # This is a prompt that asks for input from the user.
2496
            self._show_prompt(prompt, newline=False, separator=False)
2✔
2497
        else:
2498
            self._show_prompt(prompt, newline=False)
2✔
2499

2500
        if callback is None:
2✔
2501
            self._reading_callback = None
×
2502
            while self._reading:
×
2503
                QtCore.QCoreApplication.processEvents()
×
2504
            return self._get_input_buffer(force=True).rstrip('\n')
×
2505
        else:
2506
            self._reading_callback = lambda: \
2✔
2507
                callback(self._get_input_buffer(force=True).rstrip('\n'))
2508

2509
    def _set_continuation_prompt(self, prompt, html=False):
2✔
2510
        """ Sets the continuation prompt.
2511

2512
        Parameters
2513
        ----------
2514
        prompt : str
2515
            The prompt to show when more input is needed.
2516

2517
        html : bool, optional (default False)
2518
            If set, the prompt will be inserted as formatted HTML. Otherwise,
2519
            the prompt will be treated as plain text, though ANSI color codes
2520
            will be handled.
2521
        """
2522
        if html:
2✔
2523
            self._continuation_prompt_html = prompt
2✔
2524
        else:
2525
            self._continuation_prompt = prompt
2✔
2526
            self._continuation_prompt_html = None
2✔
2527

2528
    def _set_cursor(self, cursor):
2✔
2529
        """ Convenience method to set the current cursor.
2530
        """
2531
        self._control.setTextCursor(cursor)
2✔
2532

2533
    def _set_top_cursor(self, cursor):
2✔
2534
        """ Scrolls the viewport so that the specified cursor is at the top.
2535
        """
2536
        scrollbar = self._control.verticalScrollBar()
×
2537
        scrollbar.setValue(scrollbar.maximum())
×
2538
        original_cursor = self._control.textCursor()
×
2539
        self._control.setTextCursor(cursor)
×
2540
        self._control.ensureCursorVisible()
×
2541
        self._control.setTextCursor(original_cursor)
×
2542

2543
    def _show_prompt(self, prompt=None, html=False, newline=True,
2✔
2544
                     separator=True):
2545
        """ Writes a new prompt at the end of the buffer.
2546

2547
        Parameters
2548
        ----------
2549
        prompt : str, optional
2550
            The prompt to show. If not specified, the previous prompt is used.
2551

2552
        html : bool, optional (default False)
2553
            Only relevant when a prompt is specified. If set, the prompt will
2554
            be inserted as formatted HTML. Otherwise, the prompt will be treated
2555
            as plain text, though ANSI color codes will be handled.
2556

2557
        newline : bool, optional (default True)
2558
            If set, a new line will be written before showing the prompt if
2559
            there is not already a newline at the end of the buffer.
2560

2561
        separator : bool, optional (default True)
2562
            If set, a separator will be written before the prompt.
2563
        """
2564
        self._flush_pending_stream()
2✔
2565

2566
        # This is necessary to solve out-of-order insertion of mixed stdin and
2567
        # stdout stream texts.
2568
        # Fixes spyder-ide/spyder#17710
2569
        if sys.platform == 'darwin':
2✔
2570
            # Although this makes our tests hang on Mac, users confirmed that
2571
            # it's needed on that platform too.
2572
            # Fixes spyder-ide/spyder#19888
2573
            if not os.environ.get('QTCONSOLE_TESTING'):
×
2574
                QtCore.QCoreApplication.processEvents()
×
2575
        else:
2576
            QtCore.QCoreApplication.processEvents()
2✔
2577

2578
        cursor = self._get_end_cursor()
2✔
2579

2580
        # Save the current position to support _append*(before_prompt=True).
2581
        # We can't leave the cursor at the end of the document though, because
2582
        # that would cause any further additions to move the cursor. Therefore,
2583
        # we move it back one place and move it forward again at the end of
2584
        # this method. However, we only do this if the cursor isn't already
2585
        # at the start of the text.
2586
        if cursor.position() == 0:
2✔
2587
            move_forward = False
2✔
2588
        else:
2589
            move_forward = True
2✔
2590
            self._append_before_prompt_cursor.setPosition(cursor.position() - 1)
2✔
2591

2592
        # Insert a preliminary newline, if necessary.
2593
        if newline and cursor.position() > 0:
2✔
2594
            cursor.movePosition(QtGui.QTextCursor.Left,
2✔
2595
                                QtGui.QTextCursor.KeepAnchor)
2596
            if cursor.selection().toPlainText() != '\n':
2✔
2597
                self._append_block()
2✔
2598

2599
        # Write the prompt.
2600
        if separator:
2✔
2601
            self._append_plain_text(self._prompt_sep)
2✔
2602

2603
        if prompt is None:
2✔
2604
            if self._prompt_html is None:
2✔
2605
                self._append_plain_text(self._prompt)
2✔
2606
            else:
2607
                self._append_html(self._prompt_html)
×
2608
        else:
2609
            if html:
2✔
2610
                self._prompt = self._append_html_fetching_plain_text(prompt)
2✔
2611
                self._prompt_html = prompt
2✔
2612
            else:
2613
                self._append_plain_text(prompt)
2✔
2614
                self._prompt = prompt
2✔
2615
                self._prompt_html = None
2✔
2616

2617
        self._flush_pending_stream()
2✔
2618
        self._prompt_cursor.setPosition(self._get_end_pos() - 1)
2✔
2619

2620
        if move_forward:
2✔
2621
            self._append_before_prompt_cursor.setPosition(
2✔
2622
                self._append_before_prompt_cursor.position() + 1)
2623
        else:
2624
            # cursor position was 0, set before prompt cursor
2625
            self._append_before_prompt_cursor.setPosition(0)
2✔
2626
        self._prompt_started()
2✔
2627

2628
    #------ Signal handlers ----------------------------------------------------
2629

2630
    def _adjust_scrollbars(self):
2✔
2631
        """ Expands the vertical scrollbar beyond the range set by Qt.
2632
        """
2633
        # This code is adapted from _q_adjustScrollbars in qplaintextedit.cpp
2634
        # and qtextedit.cpp.
2635
        document = self._control.document()
2✔
2636
        scrollbar = self._control.verticalScrollBar()
2✔
2637
        viewport_height = self._control.viewport().height()
2✔
2638
        if isinstance(self._control, QtWidgets.QPlainTextEdit):
2✔
2639
            maximum = max(0, document.lineCount() - 1)
2✔
2640
            step = viewport_height / self._control.fontMetrics().lineSpacing()
2✔
2641
        else:
2642
            # QTextEdit does not do line-based layout and blocks will not in
2643
            # general have the same height. Therefore it does not make sense to
2644
            # attempt to scroll in line height increments.
2645
            maximum = document.size().height()
2✔
2646
            step = viewport_height
2✔
2647
        diff = maximum - scrollbar.maximum()
2✔
2648
        scrollbar.setRange(0, round(maximum))
2✔
2649
        scrollbar.setPageStep(round(step))
2✔
2650

2651
        # Compensate for undesirable scrolling that occurs automatically due to
2652
        # maximumBlockCount() text truncation.
2653
        if diff < 0 and document.blockCount() == document.maximumBlockCount():
2✔
2654
            scrollbar.setValue(round(scrollbar.value() + diff))
×
2655

2656
    def _custom_context_menu_requested(self, pos):
2✔
2657
        """ Shows a context menu at the given QPoint (in widget coordinates).
2658
        """
2659
        menu = self._context_menu_make(pos)
×
2660
        menu.exec_(self._control.mapToGlobal(pos))
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc