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

jupyter / qtconsole / 13365714430

17 Feb 2025 08:14AM UTC coverage: 61.888% (-0.02%) from 61.909%
13365714430

push

github

web-flow
Merge pull request #628 from takluyver/ipython-pygments-lexers

IPython pygments lexers are moving to separate package

1 of 1 new or added line in 1 file covered. (100.0%)

2 existing lines in 2 files now uncovered.

2931 of 4736 relevant lines covered (61.89%)

1.24 hits per line

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

66.59
/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✔
UNCOV
486
                pos = event.globalPosition().toPoint()
1✔
487
            else:
488
                pos = event.globalPos()
1✔
489
            QtWidgets.QToolTip.showText(pos, anchor)
2✔
490

491
        return super().eventFilter(obj, event)
2✔
492

493
    #---------------------------------------------------------------------------
494
    # 'QWidget' interface
495
    #---------------------------------------------------------------------------
496

497
    def sizeHint(self):
2✔
498
        """ Reimplemented to suggest a size that is 80 characters wide and
499
            25 lines high.
500
        """
501
        font_metrics = QtGui.QFontMetrics(self.font)
2✔
502
        margin = (self._control.frameWidth() +
2✔
503
                  self._control.document().documentMargin()) * 2
504
        style = self.style()
2✔
505
        splitwidth = style.pixelMetric(QtWidgets.QStyle.PM_SplitterWidth)
2✔
506

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

515
        if self.paging == 'hsplit':
2✔
516
            width = width * 2 + splitwidth
×
517

518
        height = font_metrics.height() * self.console_height + margin
2✔
519
        if self.paging == 'vsplit':
2✔
520
            height = height * 2 + splitwidth
×
521

522
        return QtCore.QSize(int(width), int(height))
2✔
523

524
    #---------------------------------------------------------------------------
525
    # 'ConsoleWidget' public interface
526
    #---------------------------------------------------------------------------
527

528
    include_other_output = Bool(False, config=True,
2✔
529
        help="""Whether to include output from clients
530
        other than this one sharing the same kernel.
531

532
        Outputs are not displayed until enter is pressed.
533
        """
534
    )
535

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

539
        Only relevant if include_other_output is True.
540
        """
541
    )
542

543
    def can_copy(self):
2✔
544
        """ Returns whether text can be copied to the clipboard.
545
        """
546
        return self._control.textCursor().hasSelection()
×
547

548
    def can_cut(self):
2✔
549
        """ Returns whether text can be cut to the clipboard.
550
        """
551
        cursor = self._control.textCursor()
×
552
        return (cursor.hasSelection() and
×
553
                self._in_buffer(cursor.anchor()) and
554
                self._in_buffer(cursor.position()))
555

556
    def can_paste(self):
2✔
557
        """ Returns whether text can be pasted from the clipboard.
558
        """
559
        if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
×
560
            return bool(QtWidgets.QApplication.clipboard().text())
×
561
        return False
×
562

563
    def clear(self, keep_input=True):
2✔
564
        """ Clear the console.
565

566
        Parameters
567
        ----------
568
        keep_input : bool, optional (default True)
569
            If set, restores the old input buffer if a new prompt is written.
570
        """
571
        if self._executing:
×
572
            self._control.clear()
×
573
        else:
574
            if keep_input:
×
575
                input_buffer = self.input_buffer
×
576
            self._control.clear()
×
577
            self._show_prompt()
×
578
            if keep_input:
×
579
                self.input_buffer = input_buffer
×
580

581
    def copy(self):
2✔
582
        """ Copy the currently selected text to the clipboard.
583
        """
584
        self.layout().currentWidget().copy()
2✔
585

586
    def copy_anchor(self, anchor):
2✔
587
        """ Copy anchor text to the clipboard
588
        """
589
        QtWidgets.QApplication.clipboard().setText(anchor)
×
590

591
    def cut(self):
2✔
592
        """ Copy the currently selected text to the clipboard and delete it
593
            if it's inside the input buffer.
594
        """
595
        self.copy()
×
596
        if self.can_cut():
×
597
            self._control.textCursor().removeSelectedText()
×
598

599
    def _handle_is_complete_reply(self, msg):
2✔
600
        if msg['parent_header'].get('msg_id', 0) != self._is_complete_msg_id:
2✔
601
            return
2✔
602
        status = msg['content'].get('status', 'complete')
2✔
603
        indent = msg['content'].get('indent', '')
2✔
604
        self._trigger_is_complete_callback(status != 'incomplete', indent)
2✔
605

606
    def _trigger_is_complete_callback(self, complete=False, indent=''):
2✔
607
        if self._is_complete_msg_id is not None:
2✔
608
            self._is_complete_msg_id = None
2✔
609
            self._is_complete_callback(complete, indent)
2✔
610

611
    def _register_is_complete_callback(self, source, callback):
2✔
612
        if self._is_complete_msg_id is not None:
2✔
613
            if self._is_complete_max_time < time.time():
×
614
                # Second return while waiting for is_complete
615
                return
×
616
            else:
617
                # request timed out
618
                self._trigger_is_complete_callback()
×
619
        self._is_complete_max_time = time.time() + self._is_complete_timeout
2✔
620
        self._is_complete_callback = callback
2✔
621
        self._is_complete_msg_id = self.kernel_client.is_complete(source)
2✔
622

623
    def execute(self, source=None, hidden=False, interactive=False):
2✔
624
        """ Executes source or the input buffer, possibly prompting for more
625
        input.
626

627
        Parameters
628
        ----------
629
        source : str, optional
630

631
            The source to execute. If not specified, the input buffer will be
632
            used. If specified and 'hidden' is False, the input buffer will be
633
            replaced with the source before execution.
634

635
        hidden : bool, optional (default False)
636

637
            If set, no output will be shown and the prompt will not be modified.
638
            In other words, it will be completely invisible to the user that
639
            an execution has occurred.
640

641
        interactive : bool, optional (default False)
642

643
            Whether the console is to treat the source as having been manually
644
            entered by the user. The effect of this parameter depends on the
645
            subclass implementation.
646

647
        Raises
648
        ------
649
        RuntimeError
650
            If incomplete input is given and 'hidden' is True. In this case,
651
            it is not possible to prompt for more input.
652

653
        Returns
654
        -------
655
        A boolean indicating whether the source was executed.
656
        """
657
        # WARNING: The order in which things happen here is very particular, in
658
        # large part because our syntax highlighting is fragile. If you change
659
        # something, test carefully!
660

661
        # Decide what to execute.
662
        if source is None:
2✔
663
            source = self.input_buffer
2✔
664
        elif not hidden:
2✔
665
            self.input_buffer = source
2✔
666

667
        if hidden:
2✔
668
            self._execute(source, hidden)
×
669
        # Execute the source or show a continuation prompt if it is incomplete.
670
        elif interactive and self.execute_on_complete_input:
2✔
671
            self._register_is_complete_callback(
2✔
672
                source, partial(self.do_execute, source))
673
        else:
674
            self.do_execute(source, True, '')
2✔
675

676
    def do_execute(self, source, complete, indent):
2✔
677
        if complete:
2✔
678
            self._append_plain_text('\n')
2✔
679
            self._input_buffer_executing = self.input_buffer
2✔
680
            self._executing = True
2✔
681
            self._finalize_input_request()
2✔
682

683
            # Perform actual execution.
684
            self._execute(source, False)
2✔
685

686
        else:
687
            # Do this inside an edit block so continuation prompts are
688
            # removed seamlessly via undo/redo.
689
            cursor = self._get_end_cursor()
2✔
690
            cursor.beginEditBlock()
2✔
691
            try:
2✔
692
                cursor.insertText('\n')
2✔
693
                self._insert_continuation_prompt(cursor, indent)
2✔
694
            finally:
695
                cursor.endEditBlock()
2✔
696

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

702
            # Advance where text is inserted
703
            self._insert_text_cursor.movePosition(QtGui.QTextCursor.End)
2✔
704

705
    def export_html(self):
2✔
706
        """ Shows a dialog to export HTML/XML in various formats.
707
        """
708
        self._html_exporter.export()
×
709

710
    def _finalize_input_request(self):
2✔
711
        """
712
        Set the widget to a non-reading state.
713
        """
714
        # Must set _reading to False before calling _prompt_finished
715
        self._reading = False
2✔
716
        self._prompt_finished()
2✔
717

718
        # There is no prompt now, so before_prompt_position is eof
719
        self._append_before_prompt_cursor.setPosition(
2✔
720
            self._get_end_cursor().position())
721

722
        self._insert_text_cursor.setPosition(
2✔
723
            self._get_end_cursor().position())
724

725
        # The maximum block count is only in effect during execution.
726
        # This ensures that _prompt_pos does not become invalid due to
727
        # text truncation.
728
        self._control.document().setMaximumBlockCount(self.buffer_size)
2✔
729

730
        # Setting a positive maximum block count will automatically
731
        # disable the undo/redo history, but just to be safe:
732
        self._control.setUndoRedoEnabled(False)
2✔
733

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

737
        If the console is currently executing, the text that is executing will
738
        always be returned.
739
        """
740
        # If we're executing, the input buffer may not even exist anymore due to
741
        # the limit imposed by 'buffer_size'. Therefore, we store it.
742
        if self._executing and not force:
2✔
743
            return self._input_buffer_executing
2✔
744

745
        cursor = self._get_end_cursor()
2✔
746
        cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
2✔
747
        input_buffer = cursor.selection().toPlainText()
2✔
748

749
        # Strip out continuation prompts.
750
        return input_buffer.replace('\n' + self._continuation_prompt, '\n')
2✔
751

752
    def _set_input_buffer(self, string):
2✔
753
        """ Sets the text in the input buffer.
754

755
        If the console is currently executing, this call has no *immediate*
756
        effect. When the execution is finished, the input buffer will be updated
757
        appropriately.
758
        """
759
        # If we're executing, store the text for later.
760
        if self._executing:
2✔
761
            self._input_buffer_pending = string
2✔
762
            return
2✔
763

764
        # Remove old text.
765
        cursor = self._get_end_cursor()
2✔
766
        cursor.beginEditBlock()
2✔
767
        cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
2✔
768
        cursor.removeSelectedText()
2✔
769

770
        # Insert new text with continuation prompts.
771
        self._insert_plain_text_into_buffer(self._get_prompt_cursor(), string)
2✔
772
        cursor.endEditBlock()
2✔
773
        self._control.moveCursor(QtGui.QTextCursor.End)
2✔
774

775
    input_buffer = property(_get_input_buffer, _set_input_buffer)
2✔
776

777
    def _get_font(self):
2✔
778
        """ The base font being used by the ConsoleWidget.
779
        """
780
        return self._control.document().defaultFont()
2✔
781

782
    def _get_font_width(self, font=None):
2✔
783
        if font is None:
2✔
784
            font = self.font
2✔
785
        font_metrics = QtGui.QFontMetrics(font)
2✔
786
        if hasattr(font_metrics, 'horizontalAdvance'):
2✔
787
            return font_metrics.horizontalAdvance(' ')
2✔
788
        else:
789
            return font_metrics.width(' ')
×
790

791
    def _set_font(self, font):
2✔
792
        """ Sets the base font for the ConsoleWidget to the specified QFont.
793
        """
794
        self._control.setTabStopWidth(
2✔
795
            self.tab_width * self._get_font_width(font)
796
        )
797

798
        self._completion_widget.setFont(font)
2✔
799
        self._control.document().setDefaultFont(font)
2✔
800
        if self._page_control:
2✔
801
            self._page_control.document().setDefaultFont(font)
2✔
802

803
        self.font_changed.emit(font)
2✔
804

805
    font = property(_get_font, _set_font)
2✔
806

807
    def _set_completion_widget(self, gui_completion):
2✔
808
        """ Set gui completion widget.
809
        """
810
        if gui_completion == 'ncurses':
×
811
            self._completion_widget = CompletionHtml(self)
×
812
        elif gui_completion == 'droplist':
×
813
            self._completion_widget = CompletionWidget(self)
×
814
        elif gui_completion == 'plain':
×
815
            self._completion_widget = CompletionPlain(self)
×
816

817
        self.gui_completion = gui_completion
×
818

819
    def open_anchor(self, anchor):
2✔
820
        """ Open selected anchor in the default webbrowser
821
        """
822
        webbrowser.open( anchor )
×
823

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

827
        Parameters
828
        ----------
829
        mode : QClipboard::Mode, optional [default QClipboard::Clipboard]
830

831
            Controls which part of the system clipboard is used. This can be
832
            used to access the selection clipboard in X11 and the Find buffer
833
            in Mac OS. By default, the regular clipboard is used.
834
        """
835
        if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
2✔
836
            # Make sure the paste is safe.
837
            self._keep_cursor_in_buffer()
2✔
838
            cursor = self._control.textCursor()
2✔
839

840
            # Remove any trailing newline, which confuses the GUI and forces the
841
            # user to backspace.
842
            text = QtWidgets.QApplication.clipboard().text(mode).rstrip()
2✔
843

844
            # dedent removes "common leading whitespace" but to preserve relative
845
            # indent of multiline code, we have to compensate for any
846
            # leading space on the first line, if we're pasting into
847
            # an indented position.
848
            cursor_offset = cursor.position() - self._get_line_start_pos()
2✔
849
            if text.startswith(' ' * cursor_offset):
2✔
850
                text = text[cursor_offset:]
2✔
851

852
            self._insert_plain_text_into_buffer(cursor, dedent(text))
2✔
853

854
    def print_(self, printer=None):
2✔
855
        """ Print the contents of the ConsoleWidget to the specified QPrinter.
856
        """
857
        if not printer:
×
858
            printer = QtPrintSupport.QPrinter()
×
859
            if QtPrintSupport.QPrintDialog(printer).exec_() != QtPrintSupport.QPrintDialog.Accepted:
×
860
                return
×
861
        self._control.print_(printer)
×
862

863
    def prompt_to_top(self):
2✔
864
        """ Moves the prompt to the top of the viewport.
865
        """
866
        if not self._executing:
×
867
            prompt_cursor = self._get_prompt_cursor()
×
868
            if self._get_cursor().blockNumber() < prompt_cursor.blockNumber():
×
869
                self._set_cursor(prompt_cursor)
×
870
            self._set_top_cursor(prompt_cursor)
×
871

872
    def redo(self):
2✔
873
        """ Redo the last operation. If there is no operation to redo, nothing
874
            happens.
875
        """
876
        self._control.redo()
×
877

878
    def reset_font(self):
2✔
879
        """ Sets the font to the default fixed-width font for this platform.
880
        """
881
        if sys.platform == 'win32':
2✔
882
            # Consolas ships with Vista/Win7, fallback to Courier if needed
883
            fallback = 'Courier'
×
884
        elif sys.platform == 'darwin':
2✔
885
            # OSX always has Monaco
886
            fallback = 'Monaco'
×
887
        else:
888
            # Monospace should always exist
889
            fallback = 'Monospace'
2✔
890
        font = get_font(self.font_family, fallback)
2✔
891
        if self.font_size:
2✔
892
            font.setPointSize(self.font_size)
×
893
        else:
894
            font.setPointSize(QtWidgets.QApplication.instance().font().pointSize())
2✔
895
        font.setStyleHint(QtGui.QFont.TypeWriter)
2✔
896
        self._set_font(font)
2✔
897

898
    def change_font_size(self, delta):
2✔
899
        """Change the font size by the specified amount (in points).
900
        """
901
        font = self.font
×
902
        size = max(font.pointSize() + delta, 1) # minimum 1 point
×
903
        font.setPointSize(size)
×
904
        self._set_font(font)
×
905

906
    def _increase_font_size(self):
2✔
907
        self.change_font_size(1)
×
908

909
    def _decrease_font_size(self):
2✔
910
        self.change_font_size(-1)
×
911

912
    def select_all_smart(self):
2✔
913
        """ Select current cell, or, if already selected, the whole document.
914
        """
915
        c = self._get_cursor()
2✔
916
        sel_range = c.selectionStart(), c.selectionEnd()
2✔
917

918
        c.clearSelection()
2✔
919
        c.setPosition(self._get_prompt_cursor().position())
2✔
920
        c.setPosition(self._get_end_pos(),
2✔
921
                      mode=QtGui.QTextCursor.KeepAnchor)
922
        new_sel_range = c.selectionStart(), c.selectionEnd()
2✔
923
        if sel_range == new_sel_range:
2✔
924
            # cell already selected, expand selection to whole document
925
            self.select_document()
2✔
926
        else:
927
            # set cell selection as active selection
928
            self._control.setTextCursor(c)
2✔
929

930
    def select_document(self):
2✔
931
        """ Selects all the text in the buffer.
932
        """
933
        self._control.selectAll()
2✔
934

935
    def _get_tab_width(self):
2✔
936
        """ The width (in terms of space characters) for tab characters.
937
        """
938
        return self._tab_width
2✔
939

940
    def _set_tab_width(self, tab_width):
2✔
941
        """ Sets the width (in terms of space characters) for tab characters.
942
        """
943
        self._control.setTabStopWidth(tab_width * self._get_font_width())
2✔
944

945
        self._tab_width = tab_width
2✔
946

947
    tab_width = property(_get_tab_width, _set_tab_width)
2✔
948

949
    def undo(self):
2✔
950
        """ Undo the last operation. If there is no operation to undo, nothing
951
            happens.
952
        """
953
        self._control.undo()
×
954

955
    #---------------------------------------------------------------------------
956
    # 'ConsoleWidget' abstract interface
957
    #---------------------------------------------------------------------------
958

959
    def _is_complete(self, source, interactive):
2✔
960
        """ Returns whether 'source' can be executed. When triggered by an
961
            Enter/Return key press, 'interactive' is True; otherwise, it is
962
            False.
963
        """
964
        raise NotImplementedError
×
965

966
    def _execute(self, source, hidden):
2✔
967
        """ Execute 'source'. If 'hidden', do not show any output.
968
        """
969
        raise NotImplementedError
×
970

971
    def _prompt_started_hook(self):
2✔
972
        """ Called immediately after a new prompt is displayed.
973
        """
974
        pass
2✔
975

976
    def _prompt_finished_hook(self):
2✔
977
        """ Called immediately after a prompt is finished, i.e. when some input
978
            will be processed and a new prompt displayed.
979
        """
980
        pass
2✔
981

982
    def _up_pressed(self, shift_modifier):
2✔
983
        """ Called when the up key is pressed. Returns whether to continue
984
            processing the event.
985
        """
986
        return True
×
987

988
    def _down_pressed(self, shift_modifier):
2✔
989
        """ Called when the down key is pressed. Returns whether to continue
990
            processing the event.
991
        """
992
        return True
×
993

994
    def _tab_pressed(self):
2✔
995
        """ Called when the tab key is pressed. Returns whether to continue
996
            processing the event.
997
        """
998
        return True
2✔
999

1000
    #--------------------------------------------------------------------------
1001
    # 'ConsoleWidget' protected interface
1002
    #--------------------------------------------------------------------------
1003

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

1007
        If 'before_prompt' is enabled, the content will be inserted before the
1008
        current prompt, if there is one.
1009
        """
1010
        # Determine where to insert the content.
1011
        cursor = self._insert_text_cursor
2✔
1012
        if before_prompt and (self._reading or not self._executing):
2✔
1013
            self._flush_pending_stream()
2✔
1014

1015
            # Jump to before prompt, if there is one
1016
            if cursor.position() >= self._append_before_prompt_pos \
2✔
1017
                    and self._append_before_prompt_pos != self._get_end_pos():
1018
                cursor.setPosition(self._append_before_prompt_pos)
2✔
1019

1020
                # If we're appending on the same line as the prompt, use insert mode.
1021
                # If so, the character at self._append_before_prompt_pos will not be a newline
1022
                cursor.movePosition(QtGui.QTextCursor.Right,
2✔
1023
                                    QtGui.QTextCursor.KeepAnchor)
1024
                if cursor.selection().toPlainText() != '\n':
2✔
1025
                    cursor._insert_mode = True
2✔
1026
                cursor.movePosition(QtGui.QTextCursor.Left)
2✔
1027
        else:
1028
            # Insert at current printing point.
1029
            # If cursor is before prompt jump to end, but only if there
1030
            # is a prompt (before_prompt_pos != end)
1031
            if cursor.position() <= self._append_before_prompt_pos \
2✔
1032
                    and self._append_before_prompt_pos != self._get_end_pos():
1033
                cursor.movePosition(QtGui.QTextCursor.End)
2✔
1034

1035
            if insert != self._insert_plain_text:
2✔
1036
                self._flush_pending_stream()
2✔
1037

1038
        # Perform the insertion.
1039
        result = insert(cursor, input, *args, **kwargs)
2✔
1040

1041
        # Remove insert mode tag
1042
        if hasattr(cursor, '_insert_mode'):
2✔
1043
            del cursor._insert_mode
2✔
1044

1045
        return result
2✔
1046

1047
    def _append_block(self, block_format=None, before_prompt=False):
2✔
1048
        """ Appends an new QTextBlock to the end of the console buffer.
1049
        """
1050
        self._append_custom(self._insert_block, block_format, before_prompt)
2✔
1051

1052
    def _append_html(self, html, before_prompt=False):
2✔
1053
        """ Appends HTML at the end of the console buffer.
1054
        """
1055
        self._append_custom(self._insert_html, html, before_prompt)
2✔
1056

1057
    def _append_html_fetching_plain_text(self, html, before_prompt=False):
2✔
1058
        """ Appends HTML, then returns the plain text version of it.
1059
        """
1060
        return self._append_custom(self._insert_html_fetching_plain_text,
2✔
1061
                                   html, before_prompt)
1062

1063
    def _append_plain_text(self, text, before_prompt=False):
2✔
1064
        """ Appends plain text, processing ANSI codes if enabled.
1065
        """
1066
        self._append_custom(self._insert_plain_text, text, before_prompt)
2✔
1067

1068
    def _cancel_completion(self):
2✔
1069
        """ If text completion is progress, cancel it.
1070
        """
1071
        self._completion_widget.cancel_completion()
2✔
1072

1073
    def _clear_temporary_buffer(self):
2✔
1074
        """ Clears the "temporary text" buffer, i.e. all the text following
1075
            the prompt region.
1076
        """
1077
        # Select and remove all text below the input buffer.
1078
        cursor = self._get_prompt_cursor()
2✔
1079
        prompt = self._continuation_prompt.lstrip()
2✔
1080
        if self._temp_buffer_filled:
2✔
1081
            self._temp_buffer_filled = False
×
1082
            while cursor.movePosition(QtGui.QTextCursor.NextBlock):
×
1083
                temp_cursor = QtGui.QTextCursor(cursor)
×
1084
                temp_cursor.select(QtGui.QTextCursor.BlockUnderCursor)
×
1085
                text = temp_cursor.selection().toPlainText().lstrip()
×
1086
                if not text.startswith(prompt):
×
1087
                    break
×
1088
        else:
1089
            # We've reached the end of the input buffer and no text follows.
1090
            return
2✔
1091
        cursor.movePosition(QtGui.QTextCursor.Left) # Grab the newline.
×
1092
        cursor.movePosition(QtGui.QTextCursor.End,
×
1093
                            QtGui.QTextCursor.KeepAnchor)
1094
        cursor.removeSelectedText()
×
1095

1096
        # After doing this, we have no choice but to clear the undo/redo
1097
        # history. Otherwise, the text is not "temporary" at all, because it
1098
        # can be recalled with undo/redo. Unfortunately, Qt does not expose
1099
        # fine-grained control to the undo/redo system.
1100
        if self._control.isUndoRedoEnabled():
×
1101
            self._control.setUndoRedoEnabled(False)
×
1102
            self._control.setUndoRedoEnabled(True)
×
1103

1104
    def _complete_with_items(self, cursor, items):
2✔
1105
        """ Performs completion with 'items' at the specified cursor location.
1106
        """
1107
        self._cancel_completion()
×
1108

1109
        if len(items) == 1:
×
1110
            cursor.setPosition(self._control.textCursor().position(),
×
1111
                               QtGui.QTextCursor.KeepAnchor)
1112
            cursor.insertText(items[0])
×
1113

1114
        elif len(items) > 1:
×
1115
            current_pos = self._control.textCursor().position()
×
1116
            prefix = os.path.commonprefix(items)
×
1117
            if prefix:
×
1118
                cursor.setPosition(current_pos, QtGui.QTextCursor.KeepAnchor)
×
1119
                cursor.insertText(prefix)
×
1120
                current_pos = cursor.position()
×
1121

1122
            self._completion_widget.show_items(cursor, items,
×
1123
                                               prefix_length=len(prefix))
1124

1125
    def _fill_temporary_buffer(self, cursor, text, html=False):
2✔
1126
        """fill the area below the active editting zone with text"""
1127

1128
        current_pos = self._control.textCursor().position()
×
1129

1130
        cursor.beginEditBlock()
×
1131
        self._append_plain_text('\n')
×
1132
        self._page(text, html=html)
×
1133
        cursor.endEditBlock()
×
1134

1135
        cursor.setPosition(current_pos)
×
1136
        self._control.moveCursor(QtGui.QTextCursor.End)
×
1137
        self._control.setTextCursor(cursor)
×
1138

1139
        self._temp_buffer_filled = True
×
1140

1141

1142
    def _context_menu_make(self, pos):
2✔
1143
        """ Creates a context menu for the given QPoint (in widget coordinates).
1144
        """
1145
        menu = QtWidgets.QMenu(self)
×
1146

1147
        self.cut_action = menu.addAction('Cut', self.cut)
×
1148
        self.cut_action.setEnabled(self.can_cut())
×
1149
        self.cut_action.setShortcut(QtGui.QKeySequence.Cut)
×
1150

1151
        self.copy_action = menu.addAction('Copy', self.copy)
×
1152
        self.copy_action.setEnabled(self.can_copy())
×
1153
        self.copy_action.setShortcut(QtGui.QKeySequence.Copy)
×
1154

1155
        self.paste_action = menu.addAction('Paste', self.paste)
×
1156
        self.paste_action.setEnabled(self.can_paste())
×
1157
        self.paste_action.setShortcut(QtGui.QKeySequence.Paste)
×
1158

1159
        anchor = self._control.anchorAt(pos)
×
1160
        if anchor:
×
1161
            menu.addSeparator()
×
1162
            self.copy_link_action = menu.addAction(
×
1163
                'Copy Link Address', lambda: self.copy_anchor(anchor=anchor))
1164
            self.open_link_action = menu.addAction(
×
1165
                'Open Link', lambda: self.open_anchor(anchor=anchor))
1166

1167
        menu.addSeparator()
×
1168
        menu.addAction(self.select_all_action)
×
1169

1170
        menu.addSeparator()
×
1171
        menu.addAction(self.export_action)
×
1172
        menu.addAction(self.print_action)
×
1173

1174
        return menu
×
1175

1176
    def _control_key_down(self, modifiers, include_command=False):
2✔
1177
        """ Given a KeyboardModifiers flags object, return whether the Control
1178
        key is down.
1179

1180
        Parameters
1181
        ----------
1182
        include_command : bool, optional (default True)
1183
            Whether to treat the Command key as a (mutually exclusive) synonym
1184
            for Control when in Mac OS.
1185
        """
1186
        # Note that on Mac OS, ControlModifier corresponds to the Command key
1187
        # while MetaModifier corresponds to the Control key.
1188
        if sys.platform == 'darwin':
2✔
1189
            down = include_command and (modifiers & QtCore.Qt.ControlModifier)
×
1190
            return bool(down) ^ bool(modifiers & QtCore.Qt.MetaModifier)
×
1191
        else:
1192
            return bool(modifiers & QtCore.Qt.ControlModifier)
2✔
1193

1194
    def _create_control(self):
2✔
1195
        """ Creates and connects the underlying text widget.
1196
        """
1197
        # Create the underlying control.
1198
        if self.custom_control:
2✔
1199
            control = self.custom_control()
×
1200
        elif self.kind == 'plain':
2✔
1201
            control = QtWidgets.QPlainTextEdit()
2✔
1202
        elif self.kind == 'rich':
2✔
1203
            control = QtWidgets.QTextEdit()
2✔
1204
            control.setAcceptRichText(False)
2✔
1205
            control.setMouseTracking(True)
2✔
1206

1207
        # Prevent the widget from handling drops, as we already provide
1208
        # the logic in this class.
1209
        control.setAcceptDrops(False)
2✔
1210

1211
        # Install event filters. The filter on the viewport is needed for
1212
        # mouse events.
1213
        control.installEventFilter(self)
2✔
1214
        control.viewport().installEventFilter(self)
2✔
1215

1216
        # Connect signals.
1217
        control.customContextMenuRequested.connect(
2✔
1218
            self._custom_context_menu_requested)
1219
        control.copyAvailable.connect(self.copy_available)
2✔
1220
        control.redoAvailable.connect(self.redo_available)
2✔
1221
        control.undoAvailable.connect(self.undo_available)
2✔
1222

1223
        # Hijack the document size change signal to prevent Qt from adjusting
1224
        # the viewport's scrollbar. We are relying on an implementation detail
1225
        # of Q(Plain)TextEdit here, which is potentially dangerous, but without
1226
        # this functionality we cannot create a nice terminal interface.
1227
        layout = control.document().documentLayout()
2✔
1228
        layout.documentSizeChanged.disconnect()
2✔
1229
        layout.documentSizeChanged.connect(self._adjust_scrollbars)
2✔
1230

1231
        # Configure the scrollbar policy
1232
        if self.scrollbar_visibility:
2✔
1233
            scrollbar_policy = QtCore.Qt.ScrollBarAlwaysOn
2✔
1234
        else :
1235
            scrollbar_policy = QtCore.Qt.ScrollBarAlwaysOff
×
1236

1237
        # Configure the control.
1238
        control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
2✔
1239
        control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
2✔
1240
        control.setReadOnly(True)
2✔
1241
        control.setUndoRedoEnabled(False)
2✔
1242
        control.setVerticalScrollBarPolicy(scrollbar_policy)
2✔
1243
        return control
2✔
1244

1245
    def _create_page_control(self):
2✔
1246
        """ Creates and connects the underlying paging widget.
1247
        """
1248
        if self.custom_page_control:
2✔
1249
            control = self.custom_page_control()
×
1250
        elif self.kind == 'plain':
2✔
1251
            control = QtWidgets.QPlainTextEdit()
2✔
1252
        elif self.kind == 'rich':
2✔
1253
            control = QtWidgets.QTextEdit()
2✔
1254
        control.installEventFilter(self)
2✔
1255
        viewport = control.viewport()
2✔
1256
        viewport.installEventFilter(self)
2✔
1257
        control.setReadOnly(True)
2✔
1258
        control.setUndoRedoEnabled(False)
2✔
1259

1260
        # Configure the scrollbar policy
1261
        if self.scrollbar_visibility:
2✔
1262
            scrollbar_policy = QtCore.Qt.ScrollBarAlwaysOn
2✔
1263
        else :
1264
            scrollbar_policy = QtCore.Qt.ScrollBarAlwaysOff
×
1265

1266
        control.setVerticalScrollBarPolicy(scrollbar_policy)
2✔
1267
        return control
2✔
1268

1269
    def _event_filter_console_keypress(self, event):
2✔
1270
        """ Filter key events for the underlying text widget to create a
1271
            console-like interface.
1272
        """
1273
        intercepted = False
2✔
1274
        cursor = self._control.textCursor()
2✔
1275
        position = cursor.position()
2✔
1276
        key = event.key()
2✔
1277
        ctrl_down = self._control_key_down(event.modifiers())
2✔
1278
        alt_down = event.modifiers() & QtCore.Qt.AltModifier
2✔
1279
        shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
2✔
1280

1281
        cmd_down = (
2✔
1282
            sys.platform == "darwin" and
1283
            self._control_key_down(event.modifiers(), include_command=True)
1284
        )
1285
        if cmd_down:
2✔
1286
            if key == QtCore.Qt.Key_Left:
×
1287
                key = QtCore.Qt.Key_Home
×
1288
            elif key == QtCore.Qt.Key_Right:
×
1289
                key = QtCore.Qt.Key_End
×
1290
            elif key == QtCore.Qt.Key_Up:
×
1291
                ctrl_down = True
×
1292
                key = QtCore.Qt.Key_Home
×
1293
            elif key == QtCore.Qt.Key_Down:
×
1294
                ctrl_down = True
×
1295
                key = QtCore.Qt.Key_End
×
1296
        #------ Special modifier logic -----------------------------------------
1297

1298
        if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
2✔
1299
            intercepted = True
2✔
1300

1301
            # Special handling when tab completing in text mode.
1302
            self._cancel_completion()
2✔
1303

1304
            if self._in_buffer(position):
2✔
1305
                # Special handling when a reading a line of raw input.
1306
                if self._reading:
2✔
1307
                    self._append_plain_text('\n')
2✔
1308
                    self._reading = False
2✔
1309
                    if self._reading_callback:
2✔
1310
                        self._reading_callback()
2✔
1311

1312
                # If the input buffer is a single line or there is only
1313
                # whitespace after the cursor, execute. Otherwise, split the
1314
                # line with a continuation prompt.
1315
                elif not self._executing:
2✔
1316
                    cursor.movePosition(QtGui.QTextCursor.End,
2✔
1317
                                        QtGui.QTextCursor.KeepAnchor)
1318
                    at_end = len(cursor.selectedText().strip()) == 0
2✔
1319
                    single_line = (self._get_end_cursor().blockNumber() ==
2✔
1320
                                   self._get_prompt_cursor().blockNumber())
1321
                    if (at_end or shift_down or single_line) and not ctrl_down:
2✔
1322
                        self.execute(interactive = not shift_down)
2✔
1323
                    else:
1324
                        # Do this inside an edit block for clean undo/redo.
1325
                        pos = self._get_input_buffer_cursor_pos()
×
1326
                        def callback(complete, indent):
×
1327
                            try:
×
1328
                                cursor.beginEditBlock()
×
1329
                                cursor.setPosition(position)
×
1330
                                cursor.insertText('\n')
×
1331
                                self._insert_continuation_prompt(cursor)
×
1332
                                if indent:
×
1333
                                    cursor.insertText(indent)
×
1334
                            finally:
1335
                                cursor.endEditBlock()
×
1336

1337
                            # Ensure that the whole input buffer is visible.
1338
                            # FIXME: This will not be usable if the input buffer is
1339
                            # taller than the console widget.
1340
                            self._control.moveCursor(QtGui.QTextCursor.End)
×
1341
                            self._control.setTextCursor(cursor)
×
1342
                        self._register_is_complete_callback(
×
1343
                            self._get_input_buffer()[:pos], callback)
1344

1345
        #------ Control/Cmd modifier -------------------------------------------
1346

1347
        elif ctrl_down:
2✔
1348
            if key == QtCore.Qt.Key_G:
2✔
1349
                self._keyboard_quit()
×
1350
                intercepted = True
×
1351

1352
            elif key == QtCore.Qt.Key_K:
2✔
1353
                if self._in_buffer(position):
2✔
1354
                    cursor.clearSelection()
2✔
1355
                    cursor.movePosition(QtGui.QTextCursor.EndOfLine,
2✔
1356
                                        QtGui.QTextCursor.KeepAnchor)
1357
                    if not cursor.hasSelection():
2✔
1358
                        # Line deletion (remove continuation prompt)
1359
                        cursor.movePosition(QtGui.QTextCursor.NextBlock,
×
1360
                                            QtGui.QTextCursor.KeepAnchor)
1361
                        cursor.movePosition(QtGui.QTextCursor.Right,
×
1362
                                            QtGui.QTextCursor.KeepAnchor,
1363
                                            len(self._continuation_prompt))
1364
                    self._kill_ring.kill_cursor(cursor)
2✔
1365
                    self._set_cursor(cursor)
2✔
1366
                intercepted = True
2✔
1367

1368
            elif key == QtCore.Qt.Key_L:
2✔
1369
                self.prompt_to_top()
×
1370
                intercepted = True
×
1371

1372
            elif key == QtCore.Qt.Key_O:
2✔
1373
                if self._page_control and self._page_control.isVisible():
×
1374
                    self._page_control.setFocus()
×
1375
                intercepted = True
×
1376

1377
            elif key == QtCore.Qt.Key_U:
2✔
1378
                if self._in_buffer(position):
×
1379
                    cursor.clearSelection()
×
1380
                    start_line = cursor.blockNumber()
×
1381
                    if start_line == self._get_prompt_cursor().blockNumber():
×
1382
                        offset = len(self._prompt)
×
1383
                    else:
1384
                        offset = len(self._continuation_prompt)
×
1385
                    cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
×
1386
                                        QtGui.QTextCursor.KeepAnchor)
1387
                    cursor.movePosition(QtGui.QTextCursor.Right,
×
1388
                                        QtGui.QTextCursor.KeepAnchor, offset)
1389
                    self._kill_ring.kill_cursor(cursor)
×
1390
                    self._set_cursor(cursor)
×
1391
                intercepted = True
×
1392

1393
            elif key == QtCore.Qt.Key_Y:
2✔
1394
                self._keep_cursor_in_buffer()
×
1395
                self._kill_ring.yank()
×
1396
                intercepted = True
×
1397

1398
            elif key in (QtCore.Qt.Key_Backspace, QtCore.Qt.Key_Delete):
2✔
1399
                if key == QtCore.Qt.Key_Backspace:
2✔
1400
                    cursor = self._get_word_start_cursor(position)
2✔
1401
                else: # key == QtCore.Qt.Key_Delete
1402
                    cursor = self._get_word_end_cursor(position)
2✔
1403
                cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
2✔
1404
                self._kill_ring.kill_cursor(cursor)
2✔
1405
                intercepted = True
2✔
1406

1407
            elif key == QtCore.Qt.Key_D:
2✔
1408
                if len(self.input_buffer) == 0 and not self._executing:
×
1409
                    self.exit_requested.emit(self)
×
1410
                # if executing and input buffer empty
1411
                elif len(self._get_input_buffer(force=True)) == 0:
×
1412
                    # input a EOT ansi control character
1413
                    self._control.textCursor().insertText(chr(4))
×
1414
                    new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
×
1415
                                                QtCore.Qt.Key_Return,
1416
                                                QtCore.Qt.NoModifier)
1417
                    QtWidgets.QApplication.instance().sendEvent(self._control, new_event)
×
1418
                    intercepted = True
×
1419
                else:
1420
                    new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
×
1421
                                                QtCore.Qt.Key_Delete,
1422
                                                QtCore.Qt.NoModifier)
1423
                    QtWidgets.QApplication.instance().sendEvent(self._control, new_event)
×
1424
                    intercepted = True
×
1425

1426
            elif key == QtCore.Qt.Key_Down:
2✔
1427
                self._scroll_to_end()
×
1428

1429
            elif key == QtCore.Qt.Key_Up:
2✔
1430
                self._control.verticalScrollBar().setValue(0)
×
1431
        #------ Alt modifier ---------------------------------------------------
1432

1433
        elif alt_down:
2✔
1434
            if key == QtCore.Qt.Key_B:
×
1435
                self._set_cursor(self._get_word_start_cursor(position))
×
1436
                intercepted = True
×
1437

1438
            elif key == QtCore.Qt.Key_F:
×
1439
                self._set_cursor(self._get_word_end_cursor(position))
×
1440
                intercepted = True
×
1441

1442
            elif key == QtCore.Qt.Key_Y:
×
1443
                self._kill_ring.rotate()
×
1444
                intercepted = True
×
1445

1446
            elif key == QtCore.Qt.Key_Backspace:
×
1447
                cursor = self._get_word_start_cursor(position)
×
1448
                cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
×
1449
                self._kill_ring.kill_cursor(cursor)
×
1450
                intercepted = True
×
1451

1452
            elif key == QtCore.Qt.Key_D:
×
1453
                cursor = self._get_word_end_cursor(position)
×
1454
                cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
×
1455
                self._kill_ring.kill_cursor(cursor)
×
1456
                intercepted = True
×
1457

1458
            elif key == QtCore.Qt.Key_Delete:
×
1459
                intercepted = True
×
1460

1461
            elif key == QtCore.Qt.Key_Greater:
×
1462
                self._control.moveCursor(QtGui.QTextCursor.End)
×
1463
                intercepted = True
×
1464

1465
            elif key == QtCore.Qt.Key_Less:
×
1466
                self._control.setTextCursor(self._get_prompt_cursor())
×
1467
                intercepted = True
×
1468

1469
        #------ No modifiers ---------------------------------------------------
1470

1471
        else:
1472
            self._trigger_is_complete_callback()
2✔
1473
            if shift_down:
2✔
1474
                anchormode = QtGui.QTextCursor.KeepAnchor
2✔
1475
            else:
1476
                anchormode = QtGui.QTextCursor.MoveAnchor
2✔
1477

1478
            if key == QtCore.Qt.Key_Escape:
2✔
1479
                self._keyboard_quit()
×
1480
                intercepted = True
×
1481

1482
            elif key == QtCore.Qt.Key_Up and not shift_down:
2✔
1483
                if self._reading or not self._up_pressed(shift_down):
×
1484
                    intercepted = True
×
1485
                else:
1486
                    prompt_line = self._get_prompt_cursor().blockNumber()
×
1487
                    intercepted = cursor.blockNumber() <= prompt_line
×
1488

1489
            elif key == QtCore.Qt.Key_Down and not shift_down:
2✔
1490
                if self._reading or not self._down_pressed(shift_down):
×
1491
                    intercepted = True
×
1492
                else:
1493
                    end_line = self._get_end_cursor().blockNumber()
×
1494
                    intercepted = cursor.blockNumber() == end_line
×
1495

1496
            elif key == QtCore.Qt.Key_Tab:
2✔
1497
                if not self._reading:
2✔
1498
                    if self._tab_pressed():
2✔
1499
                        self._indent(dedent=False)
2✔
1500
                    intercepted = True
2✔
1501

1502
            elif key == QtCore.Qt.Key_Backtab:
2✔
1503
                self._indent(dedent=True)
2✔
1504
                intercepted = True
2✔
1505

1506
            elif key == QtCore.Qt.Key_Left and not shift_down:
2✔
1507

1508
                # Move to the previous line
1509
                line, col = cursor.blockNumber(), cursor.columnNumber()
2✔
1510
                if line > self._get_prompt_cursor().blockNumber() and \
2✔
1511
                        col == len(self._continuation_prompt):
1512
                    self._control.moveCursor(QtGui.QTextCursor.PreviousBlock,
2✔
1513
                                             mode=anchormode)
1514
                    self._control.moveCursor(QtGui.QTextCursor.EndOfBlock,
2✔
1515
                                             mode=anchormode)
1516
                    intercepted = True
2✔
1517

1518
                # Regular left movement
1519
                else:
1520
                    intercepted = not self._in_buffer(position - 1)
2✔
1521

1522
            elif key == QtCore.Qt.Key_Right and not shift_down:
2✔
1523
                #original_block_number = cursor.blockNumber()
1524
                if position == self._get_line_end_pos():
2✔
1525
                    cursor.movePosition(QtGui.QTextCursor.NextBlock, mode=anchormode)
2✔
1526
                    cursor.movePosition(QtGui.QTextCursor.Right,
2✔
1527
                                        mode=anchormode,
1528
                                        n=len(self._continuation_prompt))
1529
                    self._control.setTextCursor(cursor)
2✔
1530
                else:
1531
                    self._control.moveCursor(QtGui.QTextCursor.Right,
×
1532
                                             mode=anchormode)
1533
                intercepted = True
2✔
1534

1535
            elif key == QtCore.Qt.Key_Home:
2✔
1536
                start_pos = self._get_line_start_pos()
×
1537

1538
                c = self._get_cursor()
×
1539
                spaces = self._get_leading_spaces()
×
1540
                if (c.position() > start_pos + spaces or
×
1541
                        c.columnNumber() == len(self._continuation_prompt)):
1542
                    start_pos += spaces     # Beginning of text
×
1543

1544
                if shift_down and self._in_buffer(position):
×
1545
                    if c.selectedText():
×
1546
                        sel_max = max(c.selectionStart(), c.selectionEnd())
×
1547
                        cursor.setPosition(sel_max,
×
1548
                                           QtGui.QTextCursor.MoveAnchor)
1549
                    cursor.setPosition(start_pos, QtGui.QTextCursor.KeepAnchor)
×
1550
                else:
1551
                    cursor.setPosition(start_pos)
×
1552
                self._set_cursor(cursor)
×
1553
                intercepted = True
×
1554

1555
            elif key == QtCore.Qt.Key_Backspace:
2✔
1556

1557
                # Line deletion (remove continuation prompt)
1558
                line, col = cursor.blockNumber(), cursor.columnNumber()
2✔
1559
                if not self._reading and \
2✔
1560
                        col == len(self._continuation_prompt) and \
1561
                        line > self._get_prompt_cursor().blockNumber():
1562
                    cursor.beginEditBlock()
×
1563
                    cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
×
1564
                                        QtGui.QTextCursor.KeepAnchor)
1565
                    cursor.removeSelectedText()
×
1566
                    cursor.deletePreviousChar()
×
1567
                    cursor.endEditBlock()
×
1568
                    intercepted = True
×
1569

1570
                # Regular backwards deletion
1571
                else:
1572
                    anchor = cursor.anchor()
2✔
1573
                    if anchor == position:
2✔
1574
                        intercepted = not self._in_buffer(position - 1)
2✔
1575
                    else:
1576
                        intercepted = not self._in_buffer(min(anchor, position))
×
1577

1578
            elif key == QtCore.Qt.Key_Delete:
2✔
1579

1580
                # Line deletion (remove continuation prompt)
1581
                if not self._reading and self._in_buffer(position) and \
×
1582
                        cursor.atBlockEnd() and not cursor.hasSelection():
1583
                    cursor.movePosition(QtGui.QTextCursor.NextBlock,
×
1584
                                        QtGui.QTextCursor.KeepAnchor)
1585
                    cursor.movePosition(QtGui.QTextCursor.Right,
×
1586
                                        QtGui.QTextCursor.KeepAnchor,
1587
                                        len(self._continuation_prompt))
1588
                    cursor.removeSelectedText()
×
1589
                    intercepted = True
×
1590

1591
                # Regular forwards deletion:
1592
                else:
1593
                    anchor = cursor.anchor()
×
1594
                    intercepted = (not self._in_buffer(anchor) or
×
1595
                                   not self._in_buffer(position))
1596

1597
        #------ Special sequences ----------------------------------------------
1598

1599
        if not intercepted:
2✔
1600
            if event.matches(QtGui.QKeySequence.Copy):
2✔
1601
                self.copy()
2✔
1602
                intercepted = True
2✔
1603

1604
            elif event.matches(QtGui.QKeySequence.Cut):
2✔
1605
                self.cut()
×
1606
                intercepted = True
×
1607

1608
            elif event.matches(QtGui.QKeySequence.Paste):
2✔
1609
                self.paste()
2✔
1610
                intercepted = True
2✔
1611

1612
        # Don't move the cursor if Control/Cmd is pressed to allow copy-paste
1613
        # using the keyboard in any part of the buffer. Also, permit scrolling
1614
        # with Page Up/Down keys. Finally, if we're executing, don't move the
1615
        # cursor (if even this made sense, we can't guarantee that the prompt
1616
        # position is still valid due to text truncation).
1617
        if not (self._control_key_down(event.modifiers(), include_command=True)
2✔
1618
                or key in (QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown)
1619
                or (self._executing and not self._reading)
1620
                or (event.text() == "" and not
1621
                    (not shift_down and key in (QtCore.Qt.Key_Up, QtCore.Qt.Key_Down)))):
1622
            self._keep_cursor_in_buffer()
2✔
1623

1624
        return intercepted
2✔
1625

1626
    def _event_filter_page_keypress(self, event):
2✔
1627
        """ Filter key events for the paging widget to create console-like
1628
            interface.
1629
        """
1630
        key = event.key()
×
1631
        ctrl_down = self._control_key_down(event.modifiers())
×
1632
        alt_down = event.modifiers() & QtCore.Qt.AltModifier
×
1633

1634
        if ctrl_down:
×
1635
            if key == QtCore.Qt.Key_O:
×
1636
                self._control.setFocus()
×
1637
                return True
×
1638

1639
        elif alt_down:
×
1640
            if key == QtCore.Qt.Key_Greater:
×
1641
                self._page_control.moveCursor(QtGui.QTextCursor.End)
×
1642
                return True
×
1643

1644
            elif key == QtCore.Qt.Key_Less:
×
1645
                self._page_control.moveCursor(QtGui.QTextCursor.Start)
×
1646
                return True
×
1647

1648
        elif key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
×
1649
            if self._splitter:
×
1650
                self._page_control.hide()
×
1651
                self._control.setFocus()
×
1652
            else:
1653
                self.layout().setCurrentWidget(self._control)
×
1654
                # re-enable buffer truncation after paging
1655
                self._control.document().setMaximumBlockCount(self.buffer_size)
×
1656
            return True
×
1657

1658
        elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return,
×
1659
                     QtCore.Qt.Key_Tab):
1660
            new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
×
1661
                                        QtCore.Qt.Key_PageDown,
1662
                                        QtCore.Qt.NoModifier)
1663
            QtWidgets.QApplication.instance().sendEvent(self._page_control, new_event)
×
1664
            return True
×
1665

1666
        elif key == QtCore.Qt.Key_Backspace:
×
1667
            new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
×
1668
                                        QtCore.Qt.Key_PageUp,
1669
                                        QtCore.Qt.NoModifier)
1670
            QtWidgets.QApplication.instance().sendEvent(self._page_control, new_event)
×
1671
            return True
×
1672

1673
        # vi/less -like key bindings
1674
        elif key == QtCore.Qt.Key_J:
×
1675
            new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
×
1676
                                        QtCore.Qt.Key_Down,
1677
                                        QtCore.Qt.NoModifier)
1678
            QtWidgets.QApplication.instance().sendEvent(self._page_control, new_event)
×
1679
            return True
×
1680

1681
        # vi/less -like key bindings
1682
        elif key == QtCore.Qt.Key_K:
×
1683
            new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
×
1684
                                        QtCore.Qt.Key_Up,
1685
                                        QtCore.Qt.NoModifier)
1686
            QtWidgets.QApplication.instance().sendEvent(self._page_control, new_event)
×
1687
            return True
×
1688

1689
        return False
×
1690

1691
    def _on_flush_pending_stream_timer(self):
2✔
1692
        """ Flush pending text into the widget on console timer trigger.
1693
        """
1694
        self._flush_pending_stream()
2✔
1695

1696
    def _flush_pending_stream(self):
2✔
1697
        """
1698
        Flush pending text into the widget.
1699

1700
        It only applies to text that is pending when the console is in the
1701
        running state. Text printed when console is not running is shown
1702
        immediately, and does not wait to be flushed.
1703
        """
1704
        text = self._pending_insert_text
2✔
1705
        self._pending_insert_text = []
2✔
1706
        buffer_size = self._control.document().maximumBlockCount()
2✔
1707
        if buffer_size > 0:
2✔
1708
            text = self._get_last_lines_from_list(text, buffer_size)
2✔
1709
        text = ''.join(text)
2✔
1710
        t = time.time()
2✔
1711
        self._insert_plain_text(self._insert_text_cursor, text, flush=True)
2✔
1712
        # Set the flush interval to equal the maximum time to update text.
1713
        self._pending_text_flush_interval.setInterval(
2✔
1714
            int(max(100, (time.time() - t) * 1000))
1715
        )
1716

1717
    def _get_cursor(self):
2✔
1718
        """ Get a cursor at the current insert position.
1719
        """
1720
        return self._control.textCursor()
2✔
1721

1722
    def _get_end_cursor(self):
2✔
1723
        """ Get a cursor at the last character of the current cell.
1724
        """
1725
        cursor = self._control.textCursor()
2✔
1726
        cursor.movePosition(QtGui.QTextCursor.End)
2✔
1727
        return cursor
2✔
1728

1729
    def _get_end_pos(self):
2✔
1730
        """ Get the position of the last character of the current cell.
1731
        """
1732
        return self._get_end_cursor().position()
2✔
1733

1734
    def _get_line_start_cursor(self):
2✔
1735
        """ Get a cursor at the first character of the current line.
1736
        """
1737
        cursor = self._control.textCursor()
2✔
1738
        start_line = cursor.blockNumber()
2✔
1739
        if start_line == self._get_prompt_cursor().blockNumber():
2✔
1740
            cursor.setPosition(self._prompt_pos)
2✔
1741
        else:
1742
            cursor.movePosition(QtGui.QTextCursor.StartOfLine)
2✔
1743
            cursor.setPosition(cursor.position() +
2✔
1744
                               len(self._continuation_prompt))
1745
        return cursor
2✔
1746

1747
    def _get_line_start_pos(self):
2✔
1748
        """ Get the position of the first character of the current line.
1749
        """
1750
        return self._get_line_start_cursor().position()
2✔
1751

1752
    def _get_line_end_cursor(self):
2✔
1753
        """ Get a cursor at the last character of the current line.
1754
        """
1755
        cursor = self._control.textCursor()
2✔
1756
        cursor.movePosition(QtGui.QTextCursor.EndOfLine)
2✔
1757
        return cursor
2✔
1758

1759
    def _get_line_end_pos(self):
2✔
1760
        """ Get the position of the last character of the current line.
1761
        """
1762
        return self._get_line_end_cursor().position()
2✔
1763

1764
    def _get_input_buffer_cursor_column(self):
2✔
1765
        """ Get the column of the cursor in the input buffer, excluding the
1766
            contribution by the prompt, or -1 if there is no such column.
1767
        """
1768
        prompt = self._get_input_buffer_cursor_prompt()
2✔
1769
        if prompt is None:
2✔
1770
            return -1
2✔
1771
        else:
1772
            cursor = self._control.textCursor()
×
1773
            return cursor.columnNumber() - len(prompt)
×
1774

1775
    def _get_input_buffer_cursor_line(self):
2✔
1776
        """ Get the text of the line of the input buffer that contains the
1777
            cursor, or None if there is no such line.
1778
        """
1779
        prompt = self._get_input_buffer_cursor_prompt()
×
1780
        if prompt is None:
×
1781
            return None
×
1782
        else:
1783
            cursor = self._control.textCursor()
×
1784
            text = cursor.block().text()
×
1785
            return text[len(prompt):]
×
1786

1787
    def _get_input_buffer_cursor_pos(self):
2✔
1788
        """Get the cursor position within the input buffer."""
1789
        cursor = self._control.textCursor()
2✔
1790
        cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
2✔
1791
        input_buffer = cursor.selection().toPlainText()
2✔
1792

1793
        # Don't count continuation prompts
1794
        return len(input_buffer.replace('\n' + self._continuation_prompt, '\n'))
2✔
1795

1796
    def _get_input_buffer_cursor_prompt(self):
2✔
1797
        """ Returns the (plain text) prompt for line of the input buffer that
1798
            contains the cursor, or None if there is no such line.
1799
        """
1800
        if self._executing:
2✔
1801
            return None
2✔
1802
        cursor = self._control.textCursor()
×
1803
        if cursor.position() >= self._prompt_pos:
×
1804
            if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
×
1805
                return self._prompt
×
1806
            else:
1807
                return self._continuation_prompt
×
1808
        else:
1809
            return None
×
1810

1811
    def _get_last_lines(self, text, num_lines, return_count=False):
2✔
1812
        """ Get the last specified number of lines of text (like `tail -n`).
1813
        If return_count is True, returns a tuple of clipped text and the
1814
        number of lines in the clipped text.
1815
        """
1816
        pos = len(text)
2✔
1817
        if pos < num_lines:
2✔
1818
            if return_count:
2✔
1819
                return text, text.count('\n') if return_count else text
2✔
1820
            else:
1821
                return text
2✔
1822
        i = 0
2✔
1823
        while i < num_lines:
2✔
1824
            pos = text.rfind('\n', None, pos)
2✔
1825
            if pos == -1:
2✔
1826
                pos = None
2✔
1827
                break
2✔
1828
            i += 1
2✔
1829
        if return_count:
2✔
1830
            return text[pos:], i
2✔
1831
        else:
1832
            return text[pos:]
2✔
1833

1834
    def _get_last_lines_from_list(self, text_list, num_lines):
2✔
1835
        """ Get the list of text clipped to last specified lines.
1836
        """
1837
        ret = []
2✔
1838
        lines_pending = num_lines
2✔
1839
        for text in reversed(text_list):
2✔
1840
            text, lines_added = self._get_last_lines(text, lines_pending,
2✔
1841
                                                     return_count=True)
1842
            ret.append(text)
2✔
1843
            lines_pending -= lines_added
2✔
1844
            if lines_pending <= 0:
2✔
1845
                break
×
1846
        return ret[::-1]
2✔
1847

1848
    def _get_leading_spaces(self):
2✔
1849
        """ Get the number of leading spaces of the current line.
1850
        """
1851

1852
        cursor = self._get_cursor()
2✔
1853
        start_line = cursor.blockNumber()
2✔
1854
        if start_line == self._get_prompt_cursor().blockNumber():
2✔
1855
            # first line
1856
            offset = len(self._prompt)
2✔
1857
        else:
1858
            # continuation
1859
            offset = len(self._continuation_prompt)
2✔
1860
        cursor.select(QtGui.QTextCursor.LineUnderCursor)
2✔
1861
        text = cursor.selectedText()[offset:]
2✔
1862
        return len(text) - len(text.lstrip())
2✔
1863

1864
    @property
2✔
1865
    def _prompt_pos(self):
2✔
1866
        """ Find the position in the text right after the prompt.
1867
        """
1868
        return min(self._prompt_cursor.position() + 1, self._get_end_pos())
2✔
1869

1870
    @property
2✔
1871
    def _append_before_prompt_pos(self):
2✔
1872
        """ Find the position in the text right before the prompt.
1873
        """
1874
        return min(self._append_before_prompt_cursor.position(),
2✔
1875
                   self._get_end_pos())
1876

1877
    def _get_prompt_cursor(self):
2✔
1878
        """ Get a cursor at the prompt position of the current cell.
1879
        """
1880
        cursor = self._control.textCursor()
2✔
1881
        cursor.setPosition(self._prompt_pos)
2✔
1882
        return cursor
2✔
1883

1884
    def _get_selection_cursor(self, start, end):
2✔
1885
        """ Get a cursor with text selected between the positions 'start' and
1886
            'end'.
1887
        """
1888
        cursor = self._control.textCursor()
×
1889
        cursor.setPosition(start)
×
1890
        cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
×
1891
        return cursor
×
1892

1893
    def _get_word_start_cursor(self, position):
2✔
1894
        """ Find the start of the word to the left the given position. If a
1895
            sequence of non-word characters precedes the first word, skip over
1896
            them. (This emulates the behavior of bash, emacs, etc.)
1897
        """
1898
        document = self._control.document()
2✔
1899
        cursor = self._control.textCursor()
2✔
1900
        line_start_pos = self._get_line_start_pos()
2✔
1901

1902
        if position == self._prompt_pos:
2✔
1903
            return cursor
×
1904
        elif position == line_start_pos:
2✔
1905
            # Cursor is at the beginning of a line, move to the last
1906
            # non-whitespace character of the previous line
1907
            cursor = self._control.textCursor()
2✔
1908
            cursor.setPosition(position)
2✔
1909
            cursor.movePosition(QtGui.QTextCursor.PreviousBlock)
2✔
1910
            cursor.movePosition(QtGui.QTextCursor.EndOfBlock)
2✔
1911
            position = cursor.position()
2✔
1912
            while (
2✔
1913
                position >= self._prompt_pos and
1914
                is_whitespace(document.characterAt(position))
1915
            ):
1916
                position -= 1
2✔
1917
            cursor.setPosition(position + 1)
2✔
1918
        else:
1919
            position -= 1
2✔
1920

1921
            # Find the last alphanumeric char, but don't move across lines
1922
            while (
2✔
1923
                position >= self._prompt_pos and
1924
                position >= line_start_pos and
1925
                not is_letter_or_number(document.characterAt(position))
1926
            ):
1927
                position -= 1
2✔
1928

1929
            # Find the first alphanumeric char, but don't move across lines
1930
            while (
2✔
1931
                position >= self._prompt_pos and
1932
                position >= line_start_pos and
1933
                is_letter_or_number(document.characterAt(position))
1934
            ):
1935
                position -= 1
2✔
1936

1937
            cursor.setPosition(position + 1)
2✔
1938

1939
        return cursor
2✔
1940

1941
    def _get_word_end_cursor(self, position):
2✔
1942
        """ Find the end of the word to the right the given position. If a
1943
            sequence of non-word characters precedes the first word, skip over
1944
            them. (This emulates the behavior of bash, emacs, etc.)
1945
        """
1946
        document = self._control.document()
2✔
1947
        cursor = self._control.textCursor()
2✔
1948
        end_pos = self._get_end_pos()
2✔
1949
        line_end_pos = self._get_line_end_pos()
2✔
1950

1951
        if position == end_pos:
2✔
1952
            # Cursor is at the very end of the buffer
1953
            return cursor
×
1954
        elif position == line_end_pos:
2✔
1955
            # Cursor is at the end of a line, move to the first
1956
            # non-whitespace character of the next line
1957
            cursor = self._control.textCursor()
2✔
1958
            cursor.setPosition(position)
2✔
1959
            cursor.movePosition(QtGui.QTextCursor.NextBlock)
2✔
1960
            position = cursor.position() + len(self._continuation_prompt)
2✔
1961
            while (
2✔
1962
                position < end_pos and
1963
                is_whitespace(document.characterAt(position))
1964
            ):
1965
                position += 1
2✔
1966
            cursor.setPosition(position)
2✔
1967
        else:
1968
            if is_whitespace(document.characterAt(position)):
2✔
1969
                # The next character is whitespace. If this is part of
1970
                # indentation whitespace, skip to the first non-whitespace
1971
                # character.
1972
                is_indentation_whitespace = True
2✔
1973
                back_pos = position - 1
2✔
1974
                line_start_pos = self._get_line_start_pos()
2✔
1975
                while back_pos >= line_start_pos:
2✔
1976
                    if not is_whitespace(document.characterAt(back_pos)):
×
1977
                        is_indentation_whitespace = False
×
1978
                        break
×
1979
                    back_pos -= 1
×
1980
                if is_indentation_whitespace:
2✔
1981
                    # Skip to the first non-whitespace character
1982
                    while (
2✔
1983
                        position < end_pos and
1984
                        position < line_end_pos and
1985
                        is_whitespace(document.characterAt(position))
1986
                    ):
1987
                        position += 1
2✔
1988
                    cursor.setPosition(position)
2✔
1989
                    return cursor
2✔
1990

1991
            while (
2✔
1992
                position < end_pos and
1993
                position < line_end_pos and
1994
                not is_letter_or_number(document.characterAt(position))
1995
            ):
1996
                position += 1
2✔
1997

1998
            while (
2✔
1999
                position < end_pos and
2000
                position < line_end_pos and
2001
                is_letter_or_number(document.characterAt(position))
2002
            ):
2003
                position += 1
2✔
2004

2005
            cursor.setPosition(position)
2✔
2006
        return cursor
2✔
2007

2008
    def _indent(self, dedent=True):
2✔
2009
        """ Indent/Dedent current line or current text selection.
2010
        """
2011
        num_newlines = self._get_cursor().selectedText().count("\u2029")
2✔
2012
        save_cur = self._get_cursor()
2✔
2013
        cur = self._get_cursor()
2✔
2014

2015
        # move to first line of selection, if present
2016
        cur.setPosition(cur.selectionStart())
2✔
2017
        self._control.setTextCursor(cur)
2✔
2018
        spaces = self._get_leading_spaces()
2✔
2019
        # calculate number of spaces neded to align/indent to 4-space multiple
2020
        step = self._tab_width - (spaces % self._tab_width)
2✔
2021

2022
        # insertText shouldn't replace if selection is active
2023
        cur.clearSelection()
2✔
2024

2025
        # indent all lines in selection (ir just current) by `step`
2026
        for _ in range(num_newlines+1):
2✔
2027
            # update underlying cursor for _get_line_start_pos
2028
            self._control.setTextCursor(cur)
2✔
2029
            # move to first non-ws char on line
2030
            cur.setPosition(self._get_line_start_pos())
2✔
2031
            if dedent:
2✔
2032
                spaces = min(step, self._get_leading_spaces())
2✔
2033
                safe_step = spaces % self._tab_width
2✔
2034
                cur.movePosition(QtGui.QTextCursor.Right,
2✔
2035
                                 QtGui.QTextCursor.KeepAnchor,
2036
                                 min(spaces, safe_step if safe_step != 0
2037
                                    else self._tab_width))
2038
                cur.removeSelectedText()
2✔
2039
            else:
2040
                cur.insertText(' '*step)
2✔
2041
            cur.movePosition(QtGui.QTextCursor.Down)
2✔
2042

2043
        # restore cursor
2044
        self._control.setTextCursor(save_cur)
2✔
2045

2046
    def _insert_continuation_prompt(self, cursor, indent=''):
2✔
2047
        """ Inserts new continuation prompt using the specified cursor.
2048
        """
2049
        if self._continuation_prompt_html is None:
2✔
2050
            self._insert_plain_text(cursor, self._continuation_prompt)
2✔
2051
        else:
2052
            self._continuation_prompt = self._insert_html_fetching_plain_text(
2✔
2053
                cursor, self._continuation_prompt_html)
2054
        if indent:
2✔
2055
            cursor.insertText(indent)
2✔
2056

2057
    def _insert_block(self, cursor, block_format=None):
2✔
2058
        """ Inserts an empty QTextBlock using the specified cursor.
2059
        """
2060
        if block_format is None:
2✔
2061
            block_format = QtGui.QTextBlockFormat()
2✔
2062
        cursor.insertBlock(block_format)
2✔
2063

2064
    def _insert_html(self, cursor, html):
2✔
2065
        """ Inserts HTML using the specified cursor in such a way that future
2066
            formatting is unaffected.
2067
        """
2068
        cursor.beginEditBlock()
2✔
2069
        cursor.insertHtml(html)
2✔
2070

2071
        # After inserting HTML, the text document "remembers" it's in "html
2072
        # mode", which means that subsequent calls adding plain text will result
2073
        # in unwanted formatting, lost tab characters, etc. The following code
2074
        # hacks around this behavior, which I consider to be a bug in Qt, by
2075
        # (crudely) resetting the document's style state.
2076
        cursor.movePosition(QtGui.QTextCursor.Left,
2✔
2077
                            QtGui.QTextCursor.KeepAnchor)
2078
        if cursor.selection().toPlainText() == ' ':
2✔
2079
            cursor.removeSelectedText()
2✔
2080
        else:
2081
            cursor.movePosition(QtGui.QTextCursor.Right)
2✔
2082
        cursor.insertText(' ', QtGui.QTextCharFormat())
2✔
2083
        cursor.endEditBlock()
2✔
2084

2085
    def _insert_html_fetching_plain_text(self, cursor, html):
2✔
2086
        """ Inserts HTML using the specified cursor, then returns its plain text
2087
            version.
2088
        """
2089
        cursor.beginEditBlock()
2✔
2090
        cursor.removeSelectedText()
2✔
2091

2092
        start = cursor.position()
2✔
2093
        self._insert_html(cursor, html)
2✔
2094
        end = cursor.position()
2✔
2095
        cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
2✔
2096
        text = cursor.selection().toPlainText()
2✔
2097

2098
        cursor.setPosition(end)
2✔
2099
        cursor.endEditBlock()
2✔
2100
        return text
2✔
2101

2102
    def _viewport_at_end(self):
2✔
2103
        """Check if the viewport is at the end of the document."""
2104
        viewport = self._control.viewport()
2✔
2105
        end_scroll_pos = self._control.cursorForPosition(
2✔
2106
            QtCore.QPoint(viewport.width() - 1, viewport.height() - 1)
2107
            ).position()
2108
        end_doc_pos = self._get_end_pos()
2✔
2109
        return end_doc_pos - end_scroll_pos <= 1
2✔
2110

2111
    def _scroll_to_end(self):
2✔
2112
        """Scroll to the end of the document."""
2113
        end_scroll = (self._control.verticalScrollBar().maximum()
2✔
2114
                      - self._control.verticalScrollBar().pageStep())
2115
        # Only scroll down
2116
        if end_scroll > self._control.verticalScrollBar().value():
2✔
2117
            self._control.verticalScrollBar().setValue(end_scroll)
2✔
2118

2119
    def _insert_plain_text(self, cursor, text, flush=False):
2✔
2120
        """ Inserts plain text using the specified cursor, processing ANSI codes
2121
            if enabled.
2122
        """
2123
        should_autoscroll = self._viewport_at_end()
2✔
2124
        # maximumBlockCount() can be different from self.buffer_size in
2125
        # case input prompt is active.
2126
        buffer_size = self._control.document().maximumBlockCount()
2✔
2127

2128
        if (self._executing and not flush and
2✔
2129
                self._pending_text_flush_interval.isActive() and
2130
                cursor.position() == self._insert_text_cursor.position()):
2131
            # Queue the text to insert in case it is being inserted at end
2132
            self._pending_insert_text.append(text)
2✔
2133
            if buffer_size > 0:
2✔
2134
                self._pending_insert_text = self._get_last_lines_from_list(
2✔
2135
                    self._pending_insert_text, buffer_size)
2136
            return
2✔
2137

2138
        if self._executing and not self._pending_text_flush_interval.isActive():
2✔
2139
            self._pending_text_flush_interval.start()
2✔
2140

2141
        # Clip the text to last `buffer_size` lines.
2142
        if buffer_size > 0:
2✔
2143
            text = self._get_last_lines(text, buffer_size)
2✔
2144

2145
        cursor.beginEditBlock()
2✔
2146
        if self.ansi_codes:
2✔
2147
            for substring in self._ansi_processor.split_string(text):
2✔
2148
                for act in self._ansi_processor.actions:
2✔
2149

2150
                    # Unlike real terminal emulators, we don't distinguish
2151
                    # between the screen and the scrollback buffer. A screen
2152
                    # erase request clears everything.
2153
                    if act.action == 'erase':
2✔
2154
                        remove = False
2✔
2155
                        fill = False
2✔
2156
                        if act.area == 'screen':
2✔
2157
                            cursor.select(QtGui.QTextCursor.Document)
2✔
2158
                            remove = True
2✔
2159
                        if act.area == 'line':
2✔
2160
                            if act.erase_to == 'all':
2✔
2161
                                cursor.select(QtGui.QTextCursor.LineUnderCursor)
2✔
2162
                                remove = True
2✔
2163
                            elif act.erase_to == 'start':
2✔
2164
                                cursor.movePosition(
2✔
2165
                                    QtGui.QTextCursor.StartOfLine,
2166
                                    QtGui.QTextCursor.KeepAnchor)
2167
                                remove = True
2✔
2168
                                fill = True
2✔
2169
                            elif act.erase_to == 'end':
2✔
2170
                                cursor.movePosition(
2✔
2171
                                    QtGui.QTextCursor.EndOfLine,
2172
                                    QtGui.QTextCursor.KeepAnchor)
2173
                                remove = True
2✔
2174
                        if remove:
2✔
2175
                            nspace=cursor.selectionEnd()-cursor.selectionStart() if fill else 0
2✔
2176
                            cursor.removeSelectedText()
2✔
2177
                            if nspace>0: cursor.insertText(' '*nspace) # replace text by space, to keep cursor position as specified
2✔
2178

2179
                    # Simulate a form feed by scrolling just past the last line.
2180
                    elif act.action == 'scroll' and act.unit == 'page':
2✔
2181
                        cursor.insertText('\n')
×
2182
                        cursor.endEditBlock()
×
2183
                        self._set_top_cursor(cursor)
×
2184
                        cursor.joinPreviousEditBlock()
×
2185
                        cursor.deletePreviousChar()
×
2186

2187
                        if os.name == 'nt':
×
2188
                            cursor.select(QtGui.QTextCursor.Document)
×
2189
                            cursor.removeSelectedText()
×
2190

2191
                    elif act.action == 'move' and act.unit == 'line':
2✔
2192
                        if act.dir == 'up':
×
2193
                            for i in range(act.count):
×
2194
                                cursor.movePosition(
×
2195
                                    QtGui.QTextCursor.Up
2196
                                )
2197
                        elif act.dir == 'down':
×
2198
                            for i in range(act.count):
×
2199
                                cursor.movePosition(
×
2200
                                    QtGui.QTextCursor.Down
2201
                                )
2202
                        elif act.dir == 'leftup':
×
2203
                            for i in range(act.count):
×
2204
                                cursor.movePosition(
×
2205
                                    QtGui.QTextCursor.Up
2206
                                )
2207
                            cursor.movePosition(
×
2208
                                QtGui.QTextCursor.StartOfLine,
2209
                                QtGui.QTextCursor.MoveAnchor
2210
                            )
2211

2212
                    elif act.action == 'carriage-return':
2✔
2213
                        cursor.movePosition(
2✔
2214
                            QtGui.QTextCursor.StartOfLine,
2215
                            QtGui.QTextCursor.MoveAnchor)
2216

2217
                    elif act.action == 'beep':
2✔
2218
                        QtWidgets.QApplication.instance().beep()
×
2219

2220
                    elif act.action == 'backspace':
2✔
2221
                        if not cursor.atBlockStart():
2✔
2222
                            cursor.movePosition(
2✔
2223
                                QtGui.QTextCursor.PreviousCharacter,
2224
                                QtGui.QTextCursor.MoveAnchor)
2225

2226
                    elif act.action == 'newline':
2✔
2227
                        if (
2✔
2228
                            cursor.block() != cursor.document().lastBlock()
2229
                            and not cursor.document()
2230
                            .toPlainText()
2231
                            .endswith(self._prompt)
2232
                        ):
2233
                            cursor.movePosition(QtGui.QTextCursor.NextBlock)
2✔
2234
                        else:
2235
                            cursor.movePosition(
2✔
2236
                                QtGui.QTextCursor.EndOfLine,
2237
                                QtGui.QTextCursor.MoveAnchor,
2238
                            )
2239
                            cursor.insertText("\n")
2✔
2240

2241
                # simulate replacement mode
2242
                if substring is not None:
2✔
2243
                    format = self._ansi_processor.get_format()
2✔
2244

2245
                    # Note that using _insert_mode means the \r ANSI sequence will not swallow characters.
2246
                    if not (hasattr(cursor, '_insert_mode') and cursor._insert_mode):
2✔
2247
                        pos = cursor.position()
2✔
2248
                        cursor2 = QtGui.QTextCursor(cursor)  # self._get_line_end_pos() is the previous line, don't use it
2✔
2249
                        cursor2.movePosition(QtGui.QTextCursor.EndOfLine)
2✔
2250
                        remain = cursor2.position() - pos    # number of characters until end of line
2✔
2251
                        n=len(substring)
2✔
2252
                        swallow = min(n, remain)             # number of character to swallow
2✔
2253
                        cursor.setPosition(pos + swallow, QtGui.QTextCursor.KeepAnchor)
2✔
2254
                    cursor.insertText(substring, format)
2✔
2255
        else:
2256
            cursor.insertText(text)
×
2257
        cursor.endEditBlock()
2✔
2258

2259
        if should_autoscroll:
2✔
2260
            self._scroll_to_end()
2✔
2261

2262
    def _insert_plain_text_into_buffer(self, cursor, text):
2✔
2263
        """ Inserts text into the input buffer using the specified cursor (which
2264
            must be in the input buffer), ensuring that continuation prompts are
2265
            inserted as necessary.
2266
        """
2267
        lines = text.splitlines(True)
2✔
2268
        if lines:
2✔
2269
            if lines[-1].endswith('\n'):
2✔
2270
                # If the text ends with a newline, add a blank line so a new
2271
                # continuation prompt is produced.
2272
                lines.append('')
2✔
2273
            cursor.beginEditBlock()
2✔
2274
            cursor.insertText(lines[0])
2✔
2275
            for line in lines[1:]:
2✔
2276
                if self._continuation_prompt_html is None:
2✔
2277
                    cursor.insertText(self._continuation_prompt)
2✔
2278
                else:
2279
                    self._continuation_prompt = \
2✔
2280
                        self._insert_html_fetching_plain_text(
2281
                            cursor, self._continuation_prompt_html)
2282
                cursor.insertText(line)
2✔
2283
            cursor.endEditBlock()
2✔
2284

2285
    def _in_buffer(self, position):
2✔
2286
        """
2287
        Returns whether the specified position is inside the editing region.
2288
        """
2289
        return position == self._move_position_in_buffer(position)
2✔
2290

2291
    def _move_position_in_buffer(self, position):
2✔
2292
        """
2293
        Return the next position in buffer.
2294
        """
2295
        cursor = self._control.textCursor()
2✔
2296
        cursor.setPosition(position)
2✔
2297
        line = cursor.blockNumber()
2✔
2298
        prompt_line = self._get_prompt_cursor().blockNumber()
2✔
2299
        if line == prompt_line:
2✔
2300
            if position >= self._prompt_pos:
2✔
2301
                return position
2✔
2302
            return self._prompt_pos
2✔
2303
        if line > prompt_line:
2✔
2304
            cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
2✔
2305
            prompt_pos = cursor.position() + len(self._continuation_prompt)
2✔
2306
            if position >= prompt_pos:
2✔
2307
                return position
2✔
2308
            return prompt_pos
2✔
2309
        return self._prompt_pos
×
2310

2311
    def _keep_cursor_in_buffer(self):
2✔
2312
        """ Ensures that the cursor is inside the editing region. Returns
2313
            whether the cursor was moved.
2314
        """
2315
        cursor = self._control.textCursor()
2✔
2316
        endpos = cursor.selectionEnd()
2✔
2317

2318
        if endpos < self._prompt_pos:
2✔
2319
            cursor.setPosition(endpos)
×
2320
            line = cursor.blockNumber()
×
2321
            prompt_line = self._get_prompt_cursor().blockNumber()
×
2322
            if line == prompt_line:
×
2323
                # Cursor is on prompt line, move to start of buffer
2324
                cursor.setPosition(self._prompt_pos)
×
2325
            else:
2326
                # Cursor is not in buffer, move to the end
2327
                cursor.movePosition(QtGui.QTextCursor.End)
×
2328
            self._control.setTextCursor(cursor)
×
2329
            return True
×
2330

2331
        startpos = cursor.selectionStart()
2✔
2332

2333
        new_endpos = self._move_position_in_buffer(endpos)
2✔
2334
        new_startpos = self._move_position_in_buffer(startpos)
2✔
2335
        if new_endpos == endpos and new_startpos == startpos:
2✔
2336
            return False
2✔
2337

2338
        cursor.setPosition(new_startpos)
2✔
2339
        cursor.setPosition(new_endpos, QtGui.QTextCursor.KeepAnchor)
2✔
2340
        self._control.setTextCursor(cursor)
2✔
2341
        return True
2✔
2342

2343
    def _keyboard_quit(self):
2✔
2344
        """ Cancels the current editing task ala Ctrl-G in Emacs.
2345
        """
2346
        if self._temp_buffer_filled :
×
2347
            self._cancel_completion()
×
2348
            self._clear_temporary_buffer()
×
2349
        else:
2350
            self.input_buffer = ''
×
2351

2352
    def _page(self, text, html=False):
2✔
2353
        """ Displays text using the pager if it exceeds the height of the
2354
        viewport.
2355

2356
        Parameters
2357
        ----------
2358
        html : bool, optional (default False)
2359
            If set, the text will be interpreted as HTML instead of plain text.
2360
        """
2361
        line_height = QtGui.QFontMetrics(self.font).height()
×
2362
        minlines = self._control.viewport().height() / line_height
×
2363
        if self.paging != 'none' and \
×
2364
                re.match("(?:[^\n]*\n){%i}" % minlines, text):
2365
            if self.paging == 'custom':
×
2366
                self.custom_page_requested.emit(text)
×
2367
            else:
2368
                # disable buffer truncation during paging
2369
                self._control.document().setMaximumBlockCount(0)
×
2370
                self._page_control.clear()
×
2371
                cursor = self._page_control.textCursor()
×
2372
                if html:
×
2373
                    self._insert_html(cursor, text)
×
2374
                else:
2375
                    self._insert_plain_text(cursor, text)
×
2376
                self._page_control.moveCursor(QtGui.QTextCursor.Start)
×
2377

2378
                self._page_control.viewport().resize(self._control.size())
×
2379
                if self._splitter:
×
2380
                    self._page_control.show()
×
2381
                    self._page_control.setFocus()
×
2382
                else:
2383
                    self.layout().setCurrentWidget(self._page_control)
×
2384
        elif html:
×
2385
            self._append_html(text)
×
2386
        else:
2387
            self._append_plain_text(text)
×
2388

2389
    def _set_paging(self, paging):
2✔
2390
        """
2391
        Change the pager to `paging` style.
2392

2393
        Parameters
2394
        ----------
2395
        paging : string
2396
            Either "hsplit", "vsplit", or "inside"
2397
        """
2398
        if self._splitter is None:
×
2399
            raise NotImplementedError("""can only switch if --paging=hsplit or
×
2400
                    --paging=vsplit is used.""")
2401
        if paging == 'hsplit':
×
2402
            self._splitter.setOrientation(QtCore.Qt.Horizontal)
×
2403
        elif paging == 'vsplit':
×
2404
            self._splitter.setOrientation(QtCore.Qt.Vertical)
×
2405
        elif paging == 'inside':
×
2406
            raise NotImplementedError("""switching to 'inside' paging not
×
2407
                    supported yet.""")
2408
        else:
2409
            raise ValueError("unknown paging method '%s'" % paging)
×
2410
        self.paging = paging
×
2411

2412
    def _prompt_finished(self):
2✔
2413
        """ Called immediately after a prompt is finished, i.e. when some input
2414
            will be processed and a new prompt displayed.
2415
        """
2416
        self._control.setReadOnly(True)
2✔
2417
        self._prompt_finished_hook()
2✔
2418

2419
    def _prompt_started(self):
2✔
2420
        """ Called immediately after a new prompt is displayed.
2421
        """
2422
        # Temporarily disable the maximum block count to permit undo/redo and
2423
        # to ensure that the prompt position does not change due to truncation.
2424
        self._control.document().setMaximumBlockCount(0)
2✔
2425
        self._control.setUndoRedoEnabled(True)
2✔
2426

2427
        # Work around bug in QPlainTextEdit: input method is not re-enabled
2428
        # when read-only is disabled.
2429
        self._control.setReadOnly(False)
2✔
2430
        self._control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
2✔
2431

2432
        if not self._reading:
2✔
2433
            self._executing = False
2✔
2434
        self._prompt_started_hook()
2✔
2435

2436
        # If the input buffer has changed while executing, load it.
2437
        if self._input_buffer_pending:
2✔
2438
            self.input_buffer = self._input_buffer_pending
×
2439
            self._input_buffer_pending = ''
×
2440

2441
        self._control.moveCursor(QtGui.QTextCursor.End)
2✔
2442

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

2446
        Parameters
2447
        ----------
2448
        prompt : str, optional
2449
            The prompt to print before reading the line.
2450

2451
        callback : callable, optional
2452
            A callback to execute with the read line. If not specified, input is
2453
            read *synchronously* and this method does not return until it has
2454
            been read.
2455

2456
        Returns
2457
        -------
2458
        If a callback is specified, returns nothing. Otherwise, returns the
2459
        input string with the trailing newline stripped.
2460
        """
2461
        if self._reading:
2✔
2462
            raise RuntimeError('Cannot read a line. Widget is already reading.')
×
2463

2464
        if not callback and not self.isVisible():
2✔
2465
            # If the user cannot see the widget, this function cannot return.
2466
            raise RuntimeError('Cannot synchronously read a line if the widget '
×
2467
                               'is not visible!')
2468

2469
        self._reading = True
2✔
2470
        if password:
2✔
2471
            self._show_prompt('Warning: QtConsole does not support password mode, '
×
2472
                              'the text you type will be visible.', newline=True)
2473

2474
        if 'ipdb' not in prompt.lower():
2✔
2475
            # This is a prompt that asks for input from the user.
2476
            self._show_prompt(prompt, newline=False, separator=False)
2✔
2477
        else:
2478
            self._show_prompt(prompt, newline=False)
2✔
2479

2480
        if callback is None:
2✔
2481
            self._reading_callback = None
×
2482
            while self._reading:
×
2483
                QtCore.QCoreApplication.processEvents()
×
2484
            return self._get_input_buffer(force=True).rstrip('\n')
×
2485
        else:
2486
            self._reading_callback = lambda: \
2✔
2487
                callback(self._get_input_buffer(force=True).rstrip('\n'))
2488

2489
    def _set_continuation_prompt(self, prompt, html=False):
2✔
2490
        """ Sets the continuation prompt.
2491

2492
        Parameters
2493
        ----------
2494
        prompt : str
2495
            The prompt to show when more input is needed.
2496

2497
        html : bool, optional (default False)
2498
            If set, the prompt will be inserted as formatted HTML. Otherwise,
2499
            the prompt will be treated as plain text, though ANSI color codes
2500
            will be handled.
2501
        """
2502
        if html:
2✔
2503
            self._continuation_prompt_html = prompt
2✔
2504
        else:
2505
            self._continuation_prompt = prompt
2✔
2506
            self._continuation_prompt_html = None
2✔
2507

2508
    def _set_cursor(self, cursor):
2✔
2509
        """ Convenience method to set the current cursor.
2510
        """
2511
        self._control.setTextCursor(cursor)
2✔
2512

2513
    def _set_top_cursor(self, cursor):
2✔
2514
        """ Scrolls the viewport so that the specified cursor is at the top.
2515
        """
2516
        scrollbar = self._control.verticalScrollBar()
×
2517
        scrollbar.setValue(scrollbar.maximum())
×
2518
        original_cursor = self._control.textCursor()
×
2519
        self._control.setTextCursor(cursor)
×
2520
        self._control.ensureCursorVisible()
×
2521
        self._control.setTextCursor(original_cursor)
×
2522

2523
    def _show_prompt(self, prompt=None, html=False, newline=True,
2✔
2524
                     separator=True):
2525
        """ Writes a new prompt at the end of the buffer.
2526

2527
        Parameters
2528
        ----------
2529
        prompt : str, optional
2530
            The prompt to show. If not specified, the previous prompt is used.
2531

2532
        html : bool, optional (default False)
2533
            Only relevant when a prompt is specified. If set, the prompt will
2534
            be inserted as formatted HTML. Otherwise, the prompt will be treated
2535
            as plain text, though ANSI color codes will be handled.
2536

2537
        newline : bool, optional (default True)
2538
            If set, a new line will be written before showing the prompt if
2539
            there is not already a newline at the end of the buffer.
2540

2541
        separator : bool, optional (default True)
2542
            If set, a separator will be written before the prompt.
2543
        """
2544
        self._flush_pending_stream()
2✔
2545

2546
        # This is necessary to solve out-of-order insertion of mixed stdin and
2547
        # stdout stream texts.
2548
        # Fixes spyder-ide/spyder#17710
2549
        if sys.platform == 'darwin':
2✔
2550
            # Although this makes our tests hang on Mac, users confirmed that
2551
            # it's needed on that platform too.
2552
            # Fixes spyder-ide/spyder#19888
2553
            if not os.environ.get('QTCONSOLE_TESTING'):
×
2554
                QtCore.QCoreApplication.processEvents()
×
2555
        else:
2556
            QtCore.QCoreApplication.processEvents()
2✔
2557

2558
        cursor = self._get_end_cursor()
2✔
2559

2560
        # Save the current position to support _append*(before_prompt=True).
2561
        # We can't leave the cursor at the end of the document though, because
2562
        # that would cause any further additions to move the cursor. Therefore,
2563
        # we move it back one place and move it forward again at the end of
2564
        # this method. However, we only do this if the cursor isn't already
2565
        # at the start of the text.
2566
        if cursor.position() == 0:
2✔
2567
            move_forward = False
2✔
2568
        else:
2569
            move_forward = True
2✔
2570
            self._append_before_prompt_cursor.setPosition(cursor.position() - 1)
2✔
2571

2572
        # Insert a preliminary newline, if necessary.
2573
        if newline and cursor.position() > 0:
2✔
2574
            cursor.movePosition(QtGui.QTextCursor.Left,
2✔
2575
                                QtGui.QTextCursor.KeepAnchor)
2576
            if cursor.selection().toPlainText() != '\n':
2✔
2577
                self._append_block()
2✔
2578

2579
        # Write the prompt.
2580
        if separator:
2✔
2581
            self._append_plain_text(self._prompt_sep)
2✔
2582

2583
        if prompt is None:
2✔
2584
            if self._prompt_html is None:
2✔
2585
                self._append_plain_text(self._prompt)
2✔
2586
            else:
2587
                self._append_html(self._prompt_html)
×
2588
        else:
2589
            if html:
2✔
2590
                self._prompt = self._append_html_fetching_plain_text(prompt)
2✔
2591
                self._prompt_html = prompt
2✔
2592
            else:
2593
                self._append_plain_text(prompt)
2✔
2594
                self._prompt = prompt
2✔
2595
                self._prompt_html = None
2✔
2596

2597
        self._flush_pending_stream()
2✔
2598
        self._prompt_cursor.setPosition(self._get_end_pos() - 1)
2✔
2599

2600
        if move_forward:
2✔
2601
            self._append_before_prompt_cursor.setPosition(
2✔
2602
                self._append_before_prompt_cursor.position() + 1)
2603
        else:
2604
            # cursor position was 0, set before prompt cursor
2605
            self._append_before_prompt_cursor.setPosition(0)
2✔
2606
        self._prompt_started()
2✔
2607

2608
    #------ Signal handlers ----------------------------------------------------
2609

2610
    def _adjust_scrollbars(self):
2✔
2611
        """ Expands the vertical scrollbar beyond the range set by Qt.
2612
        """
2613
        # This code is adapted from _q_adjustScrollbars in qplaintextedit.cpp
2614
        # and qtextedit.cpp.
2615
        document = self._control.document()
2✔
2616
        scrollbar = self._control.verticalScrollBar()
2✔
2617
        viewport_height = self._control.viewport().height()
2✔
2618
        if isinstance(self._control, QtWidgets.QPlainTextEdit):
2✔
2619
            maximum = max(0, document.lineCount() - 1)
2✔
2620
            step = viewport_height / self._control.fontMetrics().lineSpacing()
2✔
2621
        else:
2622
            # QTextEdit does not do line-based layout and blocks will not in
2623
            # general have the same height. Therefore it does not make sense to
2624
            # attempt to scroll in line height increments.
2625
            maximum = document.size().height()
2✔
2626
            step = viewport_height
2✔
2627
        diff = maximum - scrollbar.maximum()
2✔
2628
        scrollbar.setRange(0, round(maximum))
2✔
2629
        scrollbar.setPageStep(round(step))
2✔
2630

2631
        # Compensate for undesirable scrolling that occurs automatically due to
2632
        # maximumBlockCount() text truncation.
2633
        if diff < 0 and document.blockCount() == document.maximumBlockCount():
2✔
2634
            scrollbar.setValue(round(scrollbar.value() + diff))
×
2635

2636
    def _custom_context_menu_requested(self, pos):
2✔
2637
        """ Shows a context menu at the given QPoint (in widget coordinates).
2638
        """
2639
        menu = self._context_menu_make(pos)
×
2640
        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