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