1# -*- coding: utf-8 -*- 2# 3# Copyright © Spyder Project Contributors 4# Licensed under the terms of the MIT License 5# (see spyder/__init__.py for details) 6 7"""QPlainTextEdit base class""" 8 9# pylint: disable=C0103 10# pylint: disable=R0903 11# pylint: disable=R0911 12# pylint: disable=R0201 13 14# Standard library imports 15import os 16import re 17import sys 18from collections import OrderedDict 19 20# Third party imports 21from qtpy.compat import to_qvariant 22from qtpy.QtCore import QEvent, QEventLoop, QPoint, Qt, Signal, Slot 23from qtpy.QtGui import (QClipboard, QColor, QFont, QMouseEvent, QPalette, 24 QTextCharFormat, QTextFormat, QTextOption, QTextCursor) 25from qtpy.QtWidgets import (QAbstractItemView, QApplication, QListWidget, 26 QListWidgetItem, QMainWindow, QPlainTextEdit, 27 QTextEdit, QToolTip) 28 29# Local imports 30from spyder.config.gui import get_font 31from spyder.config.main import CONF 32from spyder.py3compat import PY3, str_lower, to_text_string 33from spyder.utils import icon_manager as ima 34from spyder.widgets.calltip import CallTipWidget 35from spyder.widgets.mixins import BaseEditMixin 36from spyder.widgets.sourcecode.terminal import ANSIEscapeCodeHandler 37 38 39def insert_text_to(cursor, text, fmt): 40 """Helper to print text, taking into account backspaces""" 41 while True: 42 index = text.find(chr(8)) # backspace 43 if index == -1: 44 break 45 cursor.insertText(text[:index], fmt) 46 if cursor.positionInBlock() > 0: 47 cursor.deletePreviousChar() 48 text = text[index+1:] 49 cursor.insertText(text, fmt) 50 51 52class CompletionWidget(QListWidget): 53 """Completion list widget""" 54 55 sig_show_completions = Signal(object) 56 57 def __init__(self, parent, ancestor): 58 QListWidget.__init__(self, ancestor) 59 self.setWindowFlags(Qt.SubWindow | Qt.FramelessWindowHint) 60 self.textedit = parent 61 self.completion_list = None 62 self.case_sensitive = False 63 self.enter_select = None 64 self.hide() 65 self.itemActivated.connect(self.item_selected) 66 67 def setup_appearance(self, size, font): 68 self.resize(*size) 69 self.setFont(font) 70 71 def show_list(self, completion_list, automatic=True): 72 types = [c[1] for c in completion_list] 73 completion_list = [c[0] for c in completion_list] 74 if len(completion_list) == 1 and not automatic: 75 self.textedit.insert_completion(completion_list[0]) 76 return 77 78 self.completion_list = completion_list 79 self.clear() 80 81 icons_map = {'instance': 'attribute', 82 'statement': 'attribute', 83 'method': 'method', 84 'function': 'function', 85 'class': 'class', 86 'module': 'module'} 87 88 self.type_list = types 89 if any(types): 90 for (c, t) in zip(completion_list, types): 91 icon = icons_map.get(t, 'no_match') 92 self.addItem(QListWidgetItem(ima.icon(icon), c)) 93 else: 94 self.addItems(completion_list) 95 96 self.setCurrentRow(0) 97 98 QApplication.processEvents(QEventLoop.ExcludeUserInputEvents) 99 self.show() 100 self.setFocus() 101 self.raise_() 102 103 # Retrieving current screen height 104 desktop = QApplication.desktop() 105 srect = desktop.availableGeometry(desktop.screenNumber(self)) 106 screen_right = srect.right() 107 screen_bottom = srect.bottom() 108 109 point = self.textedit.cursorRect().bottomRight() 110 point.setX(point.x()+self.textedit.get_linenumberarea_width()) 111 point = self.textedit.mapToGlobal(point) 112 113 # Computing completion widget and its parent right positions 114 comp_right = point.x()+self.width() 115 ancestor = self.parent() 116 if ancestor is None: 117 anc_right = screen_right 118 else: 119 anc_right = min([ancestor.x()+ancestor.width(), screen_right]) 120 121 # Moving completion widget to the left 122 # if there is not enough space to the right 123 if comp_right > anc_right: 124 point.setX(point.x()-self.width()) 125 126 # Computing completion widget and its parent bottom positions 127 comp_bottom = point.y()+self.height() 128 ancestor = self.parent() 129 if ancestor is None: 130 anc_bottom = screen_bottom 131 else: 132 anc_bottom = min([ancestor.y()+ancestor.height(), screen_bottom]) 133 134 # Moving completion widget above if there is not enough space below 135 x_position = point.x() 136 if comp_bottom > anc_bottom: 137 point = self.textedit.cursorRect().topRight() 138 point = self.textedit.mapToGlobal(point) 139 point.setX(x_position) 140 point.setY(point.y()-self.height()) 141 142 if ancestor is not None: 143 # Useful only if we set parent to 'ancestor' in __init__ 144 point = ancestor.mapFromGlobal(point) 145 self.move(point) 146 147 if to_text_string(self.textedit.completion_text): 148 # When initialized, if completion text is not empty, we need 149 # to update the displayed list: 150 self.update_current() 151 152 # signal used for testing 153 self.sig_show_completions.emit(completion_list) 154 155 def hide(self): 156 QListWidget.hide(self) 157 self.textedit.setFocus() 158 159 def keyPressEvent(self, event): 160 text, key = event.text(), event.key() 161 alt = event.modifiers() & Qt.AltModifier 162 shift = event.modifiers() & Qt.ShiftModifier 163 ctrl = event.modifiers() & Qt.ControlModifier 164 modifier = shift or ctrl or alt 165 if (key in (Qt.Key_Return, Qt.Key_Enter) and self.enter_select) \ 166 or key == Qt.Key_Tab: 167 self.item_selected() 168 elif key in (Qt.Key_Return, Qt.Key_Enter, 169 Qt.Key_Left, Qt.Key_Right) or text in ('.', ':'): 170 self.hide() 171 self.textedit.keyPressEvent(event) 172 elif key in (Qt.Key_Up, Qt.Key_Down, Qt.Key_PageUp, Qt.Key_PageDown, 173 Qt.Key_Home, Qt.Key_End, 174 Qt.Key_CapsLock) and not modifier: 175 QListWidget.keyPressEvent(self, event) 176 elif len(text) or key == Qt.Key_Backspace: 177 self.textedit.keyPressEvent(event) 178 self.update_current() 179 elif modifier: 180 self.textedit.keyPressEvent(event) 181 else: 182 self.hide() 183 QListWidget.keyPressEvent(self, event) 184 185 def update_current(self): 186 completion_text = to_text_string(self.textedit.completion_text) 187 188 if completion_text: 189 for row, completion in enumerate(self.completion_list): 190 if not self.case_sensitive: 191 print(completion_text) # spyder: test-skip 192 completion = completion.lower() 193 completion_text = completion_text.lower() 194 if completion.startswith(completion_text): 195 self.setCurrentRow(row) 196 self.scrollTo(self.currentIndex(), 197 QAbstractItemView.PositionAtTop) 198 break 199 else: 200 self.hide() 201 else: 202 self.hide() 203 204 205 def focusOutEvent(self, event): 206 event.ignore() 207 # Don't hide it on Mac when main window loses focus because 208 # keyboard input is lost 209 # Fixes Issue 1318 210 if sys.platform == "darwin": 211 if event.reason() != Qt.ActiveWindowFocusReason: 212 self.hide() 213 else: 214 self.hide() 215 216 def item_selected(self, item=None): 217 if item is None: 218 item = self.currentItem() 219 self.textedit.insert_completion( to_text_string(item.text()) ) 220 self.hide() 221 222 223class TextEditBaseWidget(QPlainTextEdit, BaseEditMixin): 224 """Text edit base widget""" 225 BRACE_MATCHING_SCOPE = ('sof', 'eof') 226 cell_separators = None 227 focus_in = Signal() 228 zoom_in = Signal() 229 zoom_out = Signal() 230 zoom_reset = Signal() 231 focus_changed = Signal() 232 sig_eol_chars_changed = Signal(str) 233 234 def __init__(self, parent=None): 235 QPlainTextEdit.__init__(self, parent) 236 BaseEditMixin.__init__(self) 237 self.setAttribute(Qt.WA_DeleteOnClose) 238 239 self.extra_selections_dict = OrderedDict() 240 241 self.textChanged.connect(self.changed) 242 self.cursorPositionChanged.connect(self.cursor_position_changed) 243 244 self.indent_chars = " "*4 245 self.tab_stop_width_spaces = 4 246 247 # Code completion / calltips 248 if parent is not None: 249 mainwin = parent 250 while not isinstance(mainwin, QMainWindow): 251 mainwin = mainwin.parent() 252 if mainwin is None: 253 break 254 if mainwin is not None: 255 parent = mainwin 256 257 self.completion_widget = CompletionWidget(self, parent) 258 self.codecompletion_auto = False 259 self.codecompletion_case = True 260 self.codecompletion_enter = False 261 self.completion_text = "" 262 self.setup_completion() 263 264 self.calltip_widget = CallTipWidget(self, hide_timer_on=False) 265 self.calltips = True 266 self.calltip_position = None 267 268 self.has_cell_separators = False 269 self.highlight_current_cell_enabled = False 270 271 # The color values may be overridden by the syntax highlighter 272 # Highlight current line color 273 self.currentline_color = QColor(Qt.red).lighter(190) 274 self.currentcell_color = QColor(Qt.red).lighter(194) 275 276 # Brace matching 277 self.bracepos = None 278 self.matched_p_color = QColor(Qt.green) 279 self.unmatched_p_color = QColor(Qt.red) 280 281 self.last_cursor_cell = None 282 283 def setup_completion(self): 284 size = CONF.get('main', 'completion/size') 285 font = get_font() 286 self.completion_widget.setup_appearance(size, font) 287 288 def set_indent_chars(self, indent_chars): 289 self.indent_chars = indent_chars 290 291 def set_tab_stop_width_spaces(self, tab_stop_width_spaces): 292 self.tab_stop_width_spaces = tab_stop_width_spaces 293 self.update_tab_stop_width_spaces() 294 295 def update_tab_stop_width_spaces(self): 296 self.setTabStopWidth(self.fontMetrics().width( 297 '9' * self.tab_stop_width_spaces)) 298 299 def set_palette(self, background, foreground): 300 """ 301 Set text editor palette colors: 302 background color and caret (text cursor) color 303 """ 304 palette = QPalette() 305 palette.setColor(QPalette.Base, background) 306 palette.setColor(QPalette.Text, foreground) 307 self.setPalette(palette) 308 309 # Set the right background color when changing color schemes 310 # or creating new Editor windows. This seems to be a Qt bug. 311 # Fixes Issue 2028 312 if sys.platform == 'darwin': 313 if self.objectName(): 314 style = "QPlainTextEdit#%s {background: %s; color: %s;}" % \ 315 (self.objectName(), background.name(), foreground.name()) 316 self.setStyleSheet(style) 317 318 319 #------Extra selections 320 def extra_selection_length(self, key): 321 selection = self.get_extra_selections(key) 322 if selection: 323 cursor = self.extra_selections_dict[key][0].cursor 324 selection_length = cursor.selectionEnd() - cursor.selectionStart() 325 return selection_length 326 else: 327 return 0 328 329 def get_extra_selections(self, key): 330 return self.extra_selections_dict.get(key, []) 331 332 def set_extra_selections(self, key, extra_selections): 333 self.extra_selections_dict[key] = extra_selections 334 self.extra_selections_dict = \ 335 OrderedDict(sorted(self.extra_selections_dict.items(), 336 key=lambda s: self.extra_selection_length(s[0]), 337 reverse=True)) 338 339 def update_extra_selections(self): 340 extra_selections = [] 341 342 # Python 3 compatibility (weird): current line has to be 343 # highlighted first 344 if 'current_cell' in self.extra_selections_dict: 345 extra_selections.extend(self.extra_selections_dict['current_cell']) 346 if 'current_line' in self.extra_selections_dict: 347 extra_selections.extend(self.extra_selections_dict['current_line']) 348 349 for key, extra in list(self.extra_selections_dict.items()): 350 if not (key == 'current_line' or key == 'current_cell'): 351 extra_selections.extend(extra) 352 self.setExtraSelections(extra_selections) 353 354 def clear_extra_selections(self, key): 355 self.extra_selections_dict[key] = [] 356 self.update_extra_selections() 357 358 def changed(self): 359 """Emit changed signal""" 360 self.modificationChanged.emit(self.document().isModified()) 361 362 363 #------Highlight current line 364 def highlight_current_line(self): 365 """Highlight current line""" 366 selection = QTextEdit.ExtraSelection() 367 selection.format.setProperty(QTextFormat.FullWidthSelection, 368 to_qvariant(True)) 369 selection.format.setBackground(self.currentline_color) 370 selection.cursor = self.textCursor() 371 selection.cursor.clearSelection() 372 self.set_extra_selections('current_line', [selection]) 373 self.update_extra_selections() 374 375 def unhighlight_current_line(self): 376 """Unhighlight current line""" 377 self.clear_extra_selections('current_line') 378 379 #------Highlight current cell 380 def highlight_current_cell(self): 381 """Highlight current cell""" 382 if self.cell_separators is None or \ 383 not self.highlight_current_cell_enabled: 384 return 385 selection = QTextEdit.ExtraSelection() 386 selection.format.setProperty(QTextFormat.FullWidthSelection, 387 to_qvariant(True)) 388 selection.format.setBackground(self.currentcell_color) 389 selection.cursor, whole_file_selected, whole_screen_selected =\ 390 self.select_current_cell_in_visible_portion() 391 if whole_file_selected: 392 self.clear_extra_selections('current_cell') 393 elif whole_screen_selected: 394 if self.has_cell_separators: 395 self.set_extra_selections('current_cell', [selection]) 396 self.update_extra_selections() 397 else: 398 self.clear_extra_selections('current_cell') 399 else: 400 self.set_extra_selections('current_cell', [selection]) 401 self.update_extra_selections() 402 403 def unhighlight_current_cell(self): 404 """Unhighlight current cell""" 405 self.clear_extra_selections('current_cell') 406 407 #------Brace matching 408 def find_brace_match(self, position, brace, forward): 409 start_pos, end_pos = self.BRACE_MATCHING_SCOPE 410 if forward: 411 bracemap = {'(': ')', '[': ']', '{': '}'} 412 text = self.get_text(position, end_pos) 413 i_start_open = 1 414 i_start_close = 1 415 else: 416 bracemap = {')': '(', ']': '[', '}': '{'} 417 text = self.get_text(start_pos, position) 418 i_start_open = len(text)-1 419 i_start_close = len(text)-1 420 421 while True: 422 if forward: 423 i_close = text.find(bracemap[brace], i_start_close) 424 else: 425 i_close = text.rfind(bracemap[brace], 0, i_start_close+1) 426 if i_close > -1: 427 if forward: 428 i_start_close = i_close+1 429 i_open = text.find(brace, i_start_open, i_close) 430 else: 431 i_start_close = i_close-1 432 i_open = text.rfind(brace, i_close, i_start_open+1) 433 if i_open > -1: 434 if forward: 435 i_start_open = i_open+1 436 else: 437 i_start_open = i_open-1 438 else: 439 # found matching brace 440 if forward: 441 return position+i_close 442 else: 443 return position-(len(text)-i_close) 444 else: 445 # no matching brace 446 return 447 448 def __highlight(self, positions, color=None, cancel=False): 449 if cancel: 450 self.clear_extra_selections('brace_matching') 451 return 452 extra_selections = [] 453 for position in positions: 454 if position > self.get_position('eof'): 455 return 456 selection = QTextEdit.ExtraSelection() 457 selection.format.setBackground(color) 458 selection.cursor = self.textCursor() 459 selection.cursor.clearSelection() 460 selection.cursor.setPosition(position) 461 selection.cursor.movePosition(QTextCursor.NextCharacter, 462 QTextCursor.KeepAnchor) 463 extra_selections.append(selection) 464 self.set_extra_selections('brace_matching', extra_selections) 465 self.update_extra_selections() 466 467 def cursor_position_changed(self): 468 """Brace matching""" 469 if self.bracepos is not None: 470 self.__highlight(self.bracepos, cancel=True) 471 self.bracepos = None 472 cursor = self.textCursor() 473 if cursor.position() == 0: 474 return 475 cursor.movePosition(QTextCursor.PreviousCharacter, 476 QTextCursor.KeepAnchor) 477 text = to_text_string(cursor.selectedText()) 478 pos1 = cursor.position() 479 if text in (')', ']', '}'): 480 pos2 = self.find_brace_match(pos1, text, forward=False) 481 elif text in ('(', '[', '{'): 482 pos2 = self.find_brace_match(pos1, text, forward=True) 483 else: 484 return 485 if pos2 is not None: 486 self.bracepos = (pos1, pos2) 487 self.__highlight(self.bracepos, color=self.matched_p_color) 488 else: 489 self.bracepos = (pos1,) 490 self.__highlight(self.bracepos, color=self.unmatched_p_color) 491 492 493 #-----Widget setup and options 494 def set_codecompletion_auto(self, state): 495 """Set code completion state""" 496 self.codecompletion_auto = state 497 498 def set_codecompletion_case(self, state): 499 """Case sensitive completion""" 500 self.codecompletion_case = state 501 self.completion_widget.case_sensitive = state 502 503 def set_codecompletion_enter(self, state): 504 """Enable Enter key to select completion""" 505 self.codecompletion_enter = state 506 self.completion_widget.enter_select = state 507 508 def set_calltips(self, state): 509 """Set calltips state""" 510 self.calltips = state 511 512 def set_wrap_mode(self, mode=None): 513 """ 514 Set wrap mode 515 Valid *mode* values: None, 'word', 'character' 516 """ 517 if mode == 'word': 518 wrap_mode = QTextOption.WrapAtWordBoundaryOrAnywhere 519 elif mode == 'character': 520 wrap_mode = QTextOption.WrapAnywhere 521 else: 522 wrap_mode = QTextOption.NoWrap 523 self.setWordWrapMode(wrap_mode) 524 525 526 #------Reimplementing Qt methods 527 @Slot() 528 def copy(self): 529 """ 530 Reimplement Qt method 531 Copy text to clipboard with correct EOL chars 532 """ 533 if self.get_selected_text(): 534 QApplication.clipboard().setText(self.get_selected_text()) 535 536 def toPlainText(self): 537 """ 538 Reimplement Qt method 539 Fix PyQt4 bug on Windows and Python 3 540 """ 541 # Fix what appears to be a PyQt4 bug when getting file 542 # contents under Windows and PY3. This bug leads to 543 # corruptions when saving files with certain combinations 544 # of unicode chars on them (like the one attached on 545 # Issue 1546) 546 if os.name == 'nt' and PY3: 547 text = self.get_text('sof', 'eof') 548 return text.replace('\u2028', '\n').replace('\u2029', '\n')\ 549 .replace('\u0085', '\n') 550 else: 551 return super(TextEditBaseWidget, self).toPlainText() 552 553 def keyPressEvent(self, event): 554 text, key = event.text(), event.key() 555 ctrl = event.modifiers() & Qt.ControlModifier 556 meta = event.modifiers() & Qt.MetaModifier 557 # Use our own copy method for {Ctrl,Cmd}+C to avoid Qt 558 # copying text in HTML (See Issue 2285) 559 if (ctrl or meta) and key == Qt.Key_C: 560 self.copy() 561 else: 562 super(TextEditBaseWidget, self).keyPressEvent(event) 563 564 #------Text: get, set, ... 565 def get_selection_as_executable_code(self): 566 """Return selected text as a processed text, 567 to be executable in a Python/IPython interpreter""" 568 ls = self.get_line_separator() 569 570 _indent = lambda line: len(line)-len(line.lstrip()) 571 572 line_from, line_to = self.get_selection_bounds() 573 text = self.get_selected_text() 574 if not text: 575 return 576 577 lines = text.split(ls) 578 if len(lines) > 1: 579 # Multiline selection -> eventually fixing indentation 580 original_indent = _indent(self.get_text_line(line_from)) 581 text = (" "*(original_indent-_indent(lines[0])))+text 582 583 # If there is a common indent to all lines, find it. 584 # Moving from bottom line to top line ensures that blank 585 # lines inherit the indent of the line *below* it, 586 # which is the desired behavior. 587 min_indent = 999 588 current_indent = 0 589 lines = text.split(ls) 590 for i in range(len(lines)-1, -1, -1): 591 line = lines[i] 592 if line.strip(): 593 current_indent = _indent(line) 594 min_indent = min(current_indent, min_indent) 595 else: 596 lines[i] = ' ' * current_indent 597 if min_indent: 598 lines = [line[min_indent:] for line in lines] 599 600 # Remove any leading whitespace or comment lines 601 # since they confuse the reserved word detector that follows below 602 while lines: 603 first_line = lines[0].lstrip() 604 if first_line == '' or first_line[0] == '#': 605 lines.pop(0) 606 else: 607 break 608 609 # Add an EOL character after indentation blocks that start with some 610 # Python reserved words, so that it gets evaluated automatically 611 # by the console 612 varname = re.compile(r'[a-zA-Z0-9_]*') # Matches valid variable names. 613 maybe = False 614 nextexcept = () 615 for n, line in enumerate(lines): 616 if not _indent(line): 617 word = varname.match(line).group() 618 if maybe and word not in nextexcept: 619 lines[n-1] += ls 620 maybe = False 621 if word: 622 if word in ('def', 'for', 'while', 'with', 'class'): 623 maybe = True 624 nextexcept = () 625 elif word == 'if': 626 maybe = True 627 nextexcept = ('elif', 'else') 628 elif word == 'try': 629 maybe = True 630 nextexcept = ('except', 'finally') 631 if maybe: 632 if lines[-1].strip() == '': 633 lines[-1] += ls 634 else: 635 lines.append(ls) 636 637 return ls.join(lines) 638 639 def __exec_cell(self): 640 init_cursor = QTextCursor(self.textCursor()) 641 start_pos, end_pos = self.__save_selection() 642 cursor, whole_file_selected = self.select_current_cell() 643 if not whole_file_selected: 644 self.setTextCursor(cursor) 645 text = self.get_selection_as_executable_code() 646 self.last_cursor_cell = init_cursor 647 self.__restore_selection(start_pos, end_pos) 648 if text is not None: 649 text = text.rstrip() 650 return text 651 652 def get_cell_as_executable_code(self): 653 """Return cell contents as executable code""" 654 return self.__exec_cell() 655 656 def get_last_cell_as_executable_code(self): 657 text = None 658 if self.last_cursor_cell: 659 self.setTextCursor(self.last_cursor_cell) 660 self.highlight_current_cell() 661 text = self.__exec_cell() 662 return text 663 664 def is_cell_separator(self, cursor=None, block=None): 665 """Return True if cursor (or text block) is on a block separator""" 666 assert cursor is not None or block is not None 667 if cursor is not None: 668 cursor0 = QTextCursor(cursor) 669 cursor0.select(QTextCursor.BlockUnderCursor) 670 text = to_text_string(cursor0.selectedText()) 671 else: 672 text = to_text_string(block.text()) 673 if self.cell_separators is None: 674 return False 675 else: 676 return text.lstrip().startswith(self.cell_separators) 677 678 def select_current_cell(self): 679 """Select cell under cursor 680 cell = group of lines separated by CELL_SEPARATORS 681 returns the textCursor and a boolean indicating if the 682 entire file is selected""" 683 cursor = self.textCursor() 684 cursor.movePosition(QTextCursor.StartOfBlock) 685 cur_pos = prev_pos = cursor.position() 686 687 # Moving to the next line that is not a separator, if we are 688 # exactly at one of them 689 while self.is_cell_separator(cursor): 690 cursor.movePosition(QTextCursor.NextBlock) 691 prev_pos = cur_pos 692 cur_pos = cursor.position() 693 if cur_pos == prev_pos: 694 return cursor, False 695 prev_pos = cur_pos 696 # If not, move backwards to find the previous separator 697 while not self.is_cell_separator(cursor): 698 cursor.movePosition(QTextCursor.PreviousBlock) 699 prev_pos = cur_pos 700 cur_pos = cursor.position() 701 if cur_pos == prev_pos: 702 if self.is_cell_separator(cursor): 703 return cursor, False 704 else: 705 break 706 cursor.setPosition(prev_pos) 707 cell_at_file_start = cursor.atStart() 708 # Once we find it (or reach the beginning of the file) 709 # move to the next separator (or the end of the file) 710 # so we can grab the cell contents 711 while not self.is_cell_separator(cursor): 712 cursor.movePosition(QTextCursor.NextBlock, 713 QTextCursor.KeepAnchor) 714 cur_pos = cursor.position() 715 if cur_pos == prev_pos: 716 cursor.movePosition(QTextCursor.EndOfBlock, 717 QTextCursor.KeepAnchor) 718 break 719 prev_pos = cur_pos 720 cell_at_file_end = cursor.atEnd() 721 return cursor, cell_at_file_start and cell_at_file_end 722 723 def select_current_cell_in_visible_portion(self): 724 """Select cell under cursor in the visible portion of the file 725 cell = group of lines separated by CELL_SEPARATORS 726 returns 727 -the textCursor 728 -a boolean indicating if the entire file is selected 729 -a boolean indicating if the entire visible portion of the file is selected""" 730 cursor = self.textCursor() 731 cursor.movePosition(QTextCursor.StartOfBlock) 732 cur_pos = prev_pos = cursor.position() 733 734 beg_pos = self.cursorForPosition(QPoint(0, 0)).position() 735 bottom_right = QPoint(self.viewport().width() - 1, 736 self.viewport().height() - 1) 737 end_pos = self.cursorForPosition(bottom_right).position() 738 739 # Moving to the next line that is not a separator, if we are 740 # exactly at one of them 741 while self.is_cell_separator(cursor): 742 cursor.movePosition(QTextCursor.NextBlock) 743 prev_pos = cur_pos 744 cur_pos = cursor.position() 745 if cur_pos == prev_pos: 746 return cursor, False, False 747 prev_pos = cur_pos 748 # If not, move backwards to find the previous separator 749 while not self.is_cell_separator(cursor)\ 750 and cursor.position() >= beg_pos: 751 cursor.movePosition(QTextCursor.PreviousBlock) 752 prev_pos = cur_pos 753 cur_pos = cursor.position() 754 if cur_pos == prev_pos: 755 if self.is_cell_separator(cursor): 756 return cursor, False, False 757 else: 758 break 759 cell_at_screen_start = cursor.position() <= beg_pos 760 cursor.setPosition(prev_pos) 761 cell_at_file_start = cursor.atStart() 762 # Selecting cell header 763 if not cell_at_file_start: 764 cursor.movePosition(QTextCursor.PreviousBlock) 765 cursor.movePosition(QTextCursor.NextBlock, 766 QTextCursor.KeepAnchor) 767 # Once we find it (or reach the beginning of the file) 768 # move to the next separator (or the end of the file) 769 # so we can grab the cell contents 770 while not self.is_cell_separator(cursor)\ 771 and cursor.position() <= end_pos: 772 cursor.movePosition(QTextCursor.NextBlock, 773 QTextCursor.KeepAnchor) 774 cur_pos = cursor.position() 775 if cur_pos == prev_pos: 776 cursor.movePosition(QTextCursor.EndOfBlock, 777 QTextCursor.KeepAnchor) 778 break 779 prev_pos = cur_pos 780 cell_at_file_end = cursor.atEnd() 781 cell_at_screen_end = cursor.position() >= end_pos 782 return cursor,\ 783 cell_at_file_start and cell_at_file_end,\ 784 cell_at_screen_start and cell_at_screen_end 785 786 def go_to_next_cell(self): 787 """Go to the next cell of lines""" 788 cursor = self.textCursor() 789 cursor.movePosition(QTextCursor.NextBlock) 790 cur_pos = prev_pos = cursor.position() 791 while not self.is_cell_separator(cursor): 792 # Moving to the next code cell 793 cursor.movePosition(QTextCursor.NextBlock) 794 prev_pos = cur_pos 795 cur_pos = cursor.position() 796 if cur_pos == prev_pos: 797 return 798 self.setTextCursor(cursor) 799 800 def go_to_previous_cell(self): 801 """Go to the previous cell of lines""" 802 cursor = self.textCursor() 803 cur_pos = prev_pos = cursor.position() 804 805 if self.is_cell_separator(cursor): 806 # Move to the previous cell 807 cursor.movePosition(QTextCursor.PreviousBlock) 808 cur_pos = prev_pos = cursor.position() 809 810 while not self.is_cell_separator(cursor): 811 # Move to the previous cell or the beginning of the current cell 812 cursor.movePosition(QTextCursor.PreviousBlock) 813 prev_pos = cur_pos 814 cur_pos = cursor.position() 815 if cur_pos == prev_pos: 816 return 817 818 self.setTextCursor(cursor) 819 820 def get_line_count(self): 821 """Return document total line number""" 822 return self.blockCount() 823 824 def __save_selection(self): 825 """Save current cursor selection and return position bounds""" 826 cursor = self.textCursor() 827 return cursor.selectionStart(), cursor.selectionEnd() 828 829 def __restore_selection(self, start_pos, end_pos): 830 """Restore cursor selection from position bounds""" 831 cursor = self.textCursor() 832 cursor.setPosition(start_pos) 833 cursor.setPosition(end_pos, QTextCursor.KeepAnchor) 834 self.setTextCursor(cursor) 835 836 def __duplicate_line_or_selection(self, after_current_line=True): 837 """Duplicate current line or selected text""" 838 cursor = self.textCursor() 839 cursor.beginEditBlock() 840 start_pos, end_pos = self.__save_selection() 841 if to_text_string(cursor.selectedText()): 842 cursor.setPosition(end_pos) 843 # Check if end_pos is at the start of a block: if so, starting 844 # changes from the previous block 845 cursor.movePosition(QTextCursor.StartOfBlock, 846 QTextCursor.KeepAnchor) 847 if not to_text_string(cursor.selectedText()): 848 cursor.movePosition(QTextCursor.PreviousBlock) 849 end_pos = cursor.position() 850 851 cursor.setPosition(start_pos) 852 cursor.movePosition(QTextCursor.StartOfBlock) 853 while cursor.position() <= end_pos: 854 cursor.movePosition(QTextCursor.EndOfBlock, QTextCursor.KeepAnchor) 855 if cursor.atEnd(): 856 cursor_temp = QTextCursor(cursor) 857 cursor_temp.clearSelection() 858 cursor_temp.insertText(self.get_line_separator()) 859 break 860 cursor.movePosition(QTextCursor.NextBlock, QTextCursor.KeepAnchor) 861 text = cursor.selectedText() 862 cursor.clearSelection() 863 864 if not after_current_line: 865 # Moving cursor before current line/selected text 866 cursor.setPosition(start_pos) 867 cursor.movePosition(QTextCursor.StartOfBlock) 868 start_pos += len(text) 869 end_pos += len(text) 870 871 cursor.insertText(text) 872 cursor.endEditBlock() 873 self.setTextCursor(cursor) 874 self.__restore_selection(start_pos, end_pos) 875 876 def duplicate_line(self): 877 """ 878 Duplicate current line or selected text 879 Paste the duplicated text *after* the current line/selected text 880 """ 881 self.__duplicate_line_or_selection(after_current_line=True) 882 883 def copy_line(self): 884 """ 885 Copy current line or selected text 886 Paste the duplicated text *before* the current line/selected text 887 """ 888 self.__duplicate_line_or_selection(after_current_line=False) 889 890 def __move_line_or_selection(self, after_current_line=True): 891 """Move current line or selected text""" 892 cursor = self.textCursor() 893 cursor.beginEditBlock() 894 start_pos, end_pos = self.__save_selection() 895 last_line = False 896 897 # ------ Select text 898 899 # Get selection start location 900 cursor.setPosition(start_pos) 901 cursor.movePosition(QTextCursor.StartOfBlock) 902 start_pos = cursor.position() 903 904 # Get selection end location 905 cursor.setPosition(end_pos) 906 if not cursor.atBlockStart() or end_pos == start_pos: 907 cursor.movePosition(QTextCursor.EndOfBlock) 908 cursor.movePosition(QTextCursor.NextBlock) 909 end_pos = cursor.position() 910 911 # Check if selection ends on the last line of the document 912 if cursor.atEnd(): 913 if not cursor.atBlockStart() or end_pos == start_pos: 914 last_line = True 915 916 # ------ Stop if at document boundary 917 918 cursor.setPosition(start_pos) 919 if cursor.atStart() and not after_current_line: 920 # Stop if selection is already at top of the file while moving up 921 cursor.endEditBlock() 922 self.setTextCursor(cursor) 923 self.__restore_selection(start_pos, end_pos) 924 return 925 926 cursor.setPosition(end_pos, QTextCursor.KeepAnchor) 927 if last_line and after_current_line: 928 # Stop if selection is already at end of the file while moving down 929 cursor.endEditBlock() 930 self.setTextCursor(cursor) 931 self.__restore_selection(start_pos, end_pos) 932 return 933 934 # ------ Move text 935 936 sel_text = to_text_string(cursor.selectedText()) 937 cursor.removeSelectedText() 938 939 940 if after_current_line: 941 # Shift selection down 942 text = to_text_string(cursor.block().text()) 943 sel_text = os.linesep + sel_text[0:-1] # Move linesep at the start 944 cursor.movePosition(QTextCursor.EndOfBlock) 945 start_pos += len(text)+1 946 end_pos += len(text) 947 if not cursor.atEnd(): 948 end_pos += 1 949 else: 950 # Shift selection up 951 if last_line: 952 # Remove the last linesep and add it to the selected text 953 cursor.deletePreviousChar() 954 sel_text = sel_text + os.linesep 955 cursor.movePosition(QTextCursor.StartOfBlock) 956 end_pos += 1 957 else: 958 cursor.movePosition(QTextCursor.PreviousBlock) 959 text = to_text_string(cursor.block().text()) 960 start_pos -= len(text)+1 961 end_pos -= len(text)+1 962 963 cursor.insertText(sel_text) 964 965 cursor.endEditBlock() 966 self.setTextCursor(cursor) 967 self.__restore_selection(start_pos, end_pos) 968 969 def move_line_up(self): 970 """Move up current line or selected text""" 971 self.__move_line_or_selection(after_current_line=False) 972 973 def move_line_down(self): 974 """Move down current line or selected text""" 975 self.__move_line_or_selection(after_current_line=True) 976 977 def extend_selection_to_complete_lines(self): 978 """Extend current selection to complete lines""" 979 cursor = self.textCursor() 980 start_pos, end_pos = cursor.selectionStart(), cursor.selectionEnd() 981 cursor.setPosition(start_pos) 982 cursor.setPosition(end_pos, QTextCursor.KeepAnchor) 983 if cursor.atBlockStart(): 984 cursor.movePosition(QTextCursor.PreviousBlock, 985 QTextCursor.KeepAnchor) 986 cursor.movePosition(QTextCursor.EndOfBlock, 987 QTextCursor.KeepAnchor) 988 self.setTextCursor(cursor) 989 990 def delete_line(self): 991 """Delete current line""" 992 cursor = self.textCursor() 993 if self.has_selected_text(): 994 self.extend_selection_to_complete_lines() 995 start_pos, end_pos = cursor.selectionStart(), cursor.selectionEnd() 996 cursor.setPosition(start_pos) 997 else: 998 start_pos = end_pos = cursor.position() 999 cursor.beginEditBlock() 1000 cursor.setPosition(start_pos) 1001 cursor.movePosition(QTextCursor.StartOfBlock) 1002 while cursor.position() <= end_pos: 1003 cursor.movePosition(QTextCursor.EndOfBlock, QTextCursor.KeepAnchor) 1004 if cursor.atEnd(): 1005 break 1006 cursor.movePosition(QTextCursor.NextBlock, QTextCursor.KeepAnchor) 1007 cursor.removeSelectedText() 1008 cursor.endEditBlock() 1009 self.ensureCursorVisible() 1010 1011 def set_selection(self, start, end): 1012 cursor = self.textCursor() 1013 cursor.setPosition(start) 1014 cursor.setPosition(end, QTextCursor.KeepAnchor) 1015 self.setTextCursor(cursor) 1016 1017 def truncate_selection(self, position_from): 1018 """Unselect read-only parts in shell, like prompt""" 1019 position_from = self.get_position(position_from) 1020 cursor = self.textCursor() 1021 start, end = cursor.selectionStart(), cursor.selectionEnd() 1022 if start < end: 1023 start = max([position_from, start]) 1024 else: 1025 end = max([position_from, end]) 1026 self.set_selection(start, end) 1027 1028 def restrict_cursor_position(self, position_from, position_to): 1029 """In shell, avoid editing text except between prompt and EOF""" 1030 position_from = self.get_position(position_from) 1031 position_to = self.get_position(position_to) 1032 cursor = self.textCursor() 1033 cursor_position = cursor.position() 1034 if cursor_position < position_from or cursor_position > position_to: 1035 self.set_cursor_position(position_to) 1036 1037 #------Code completion / Calltips 1038 def hide_tooltip_if_necessary(self, key): 1039 """Hide calltip when necessary""" 1040 try: 1041 calltip_char = self.get_character(self.calltip_position) 1042 before = self.is_cursor_before(self.calltip_position, 1043 char_offset=1) 1044 other = key in (Qt.Key_ParenRight, Qt.Key_Period, Qt.Key_Tab) 1045 if calltip_char not in ('?', '(') or before or other: 1046 QToolTip.hideText() 1047 except (IndexError, TypeError): 1048 QToolTip.hideText() 1049 1050 def show_completion_widget(self, textlist, automatic=True): 1051 """Show completion widget""" 1052 self.completion_widget.show_list(textlist, automatic=automatic) 1053 1054 def hide_completion_widget(self): 1055 """Hide completion widget""" 1056 self.completion_widget.hide() 1057 1058 def show_completion_list(self, completions, completion_text="", 1059 automatic=True): 1060 """Display the possible completions""" 1061 if not completions: 1062 return 1063 if not isinstance(completions[0], tuple): 1064 completions = [(c, '') for c in completions] 1065 if len(completions) == 1 and completions[0][0] == completion_text: 1066 return 1067 self.completion_text = completion_text 1068 # Sorting completion list (entries starting with underscore are 1069 # put at the end of the list): 1070 underscore = set([(comp, t) for (comp, t) in completions 1071 if comp.startswith('_')]) 1072 1073 completions = sorted(set(completions) - underscore, 1074 key=lambda x: str_lower(x[0])) 1075 completions += sorted(underscore, key=lambda x: str_lower(x[0])) 1076 self.show_completion_widget(completions, automatic=automatic) 1077 1078 def select_completion_list(self): 1079 """Completion list is active, Enter was just pressed""" 1080 self.completion_widget.item_selected() 1081 1082 def insert_completion(self, text): 1083 if text: 1084 cursor = self.textCursor() 1085 cursor.movePosition(QTextCursor.PreviousCharacter, 1086 QTextCursor.KeepAnchor, 1087 len(self.completion_text)) 1088 cursor.removeSelectedText() 1089 self.insert_text(text) 1090 1091 def is_completion_widget_visible(self): 1092 """Return True is completion list widget is visible""" 1093 return self.completion_widget.isVisible() 1094 1095 1096 #------Standard keys 1097 def stdkey_clear(self): 1098 if not self.has_selected_text(): 1099 self.moveCursor(QTextCursor.NextCharacter, QTextCursor.KeepAnchor) 1100 self.remove_selected_text() 1101 1102 def stdkey_backspace(self): 1103 if not self.has_selected_text(): 1104 self.moveCursor(QTextCursor.PreviousCharacter, 1105 QTextCursor.KeepAnchor) 1106 self.remove_selected_text() 1107 1108 def __get_move_mode(self, shift): 1109 return QTextCursor.KeepAnchor if shift else QTextCursor.MoveAnchor 1110 1111 def stdkey_up(self, shift): 1112 self.moveCursor(QTextCursor.Up, self.__get_move_mode(shift)) 1113 1114 def stdkey_down(self, shift): 1115 self.moveCursor(QTextCursor.Down, self.__get_move_mode(shift)) 1116 1117 def stdkey_tab(self): 1118 self.insert_text(self.indent_chars) 1119 1120 def stdkey_home(self, shift, ctrl, prompt_pos=None): 1121 """Smart HOME feature: cursor is first moved at 1122 indentation position, then at the start of the line""" 1123 move_mode = self.__get_move_mode(shift) 1124 if ctrl: 1125 self.moveCursor(QTextCursor.Start, move_mode) 1126 else: 1127 cursor = self.textCursor() 1128 if prompt_pos is None: 1129 start_position = self.get_position('sol') 1130 else: 1131 start_position = self.get_position(prompt_pos) 1132 text = self.get_text(start_position, 'eol') 1133 indent_pos = start_position+len(text)-len(text.lstrip()) 1134 if cursor.position() != indent_pos: 1135 cursor.setPosition(indent_pos, move_mode) 1136 else: 1137 cursor.setPosition(start_position, move_mode) 1138 self.setTextCursor(cursor) 1139 1140 def stdkey_end(self, shift, ctrl): 1141 move_mode = self.__get_move_mode(shift) 1142 if ctrl: 1143 self.moveCursor(QTextCursor.End, move_mode) 1144 else: 1145 self.moveCursor(QTextCursor.EndOfBlock, move_mode) 1146 1147 def stdkey_pageup(self): 1148 pass 1149 1150 def stdkey_pagedown(self): 1151 pass 1152 1153 def stdkey_escape(self): 1154 pass 1155 1156 1157 #----Qt Events 1158 def mousePressEvent(self, event): 1159 """Reimplement Qt method""" 1160 if sys.platform.startswith('linux') and event.button() == Qt.MidButton: 1161 self.calltip_widget.hide() 1162 self.setFocus() 1163 event = QMouseEvent(QEvent.MouseButtonPress, event.pos(), 1164 Qt.LeftButton, Qt.LeftButton, Qt.NoModifier) 1165 QPlainTextEdit.mousePressEvent(self, event) 1166 QPlainTextEdit.mouseReleaseEvent(self, event) 1167 # Send selection text to clipboard to be able to use 1168 # the paste method and avoid the strange Issue 1445 1169 # NOTE: This issue seems a focusing problem but it 1170 # seems really hard to track 1171 mode_clip = QClipboard.Clipboard 1172 mode_sel = QClipboard.Selection 1173 text_clip = QApplication.clipboard().text(mode=mode_clip) 1174 text_sel = QApplication.clipboard().text(mode=mode_sel) 1175 QApplication.clipboard().setText(text_sel, mode=mode_clip) 1176 self.paste() 1177 QApplication.clipboard().setText(text_clip, mode=mode_clip) 1178 else: 1179 self.calltip_widget.hide() 1180 QPlainTextEdit.mousePressEvent(self, event) 1181 1182 def focusInEvent(self, event): 1183 """Reimplemented to handle focus""" 1184 self.focus_changed.emit() 1185 self.focus_in.emit() 1186 self.highlight_current_cell() 1187 QPlainTextEdit.focusInEvent(self, event) 1188 1189 def focusOutEvent(self, event): 1190 """Reimplemented to handle focus""" 1191 self.focus_changed.emit() 1192 QPlainTextEdit.focusOutEvent(self, event) 1193 1194 def wheelEvent(self, event): 1195 """Reimplemented to emit zoom in/out signals when Ctrl is pressed""" 1196 # This feature is disabled on MacOS, see Issue 1510 1197 if sys.platform != 'darwin': 1198 if event.modifiers() & Qt.ControlModifier: 1199 if hasattr(event, 'angleDelta'): 1200 if event.angleDelta().y() < 0: 1201 self.zoom_out.emit() 1202 elif event.angleDelta().y() > 0: 1203 self.zoom_in.emit() 1204 elif hasattr(event, 'delta'): 1205 if event.delta() < 0: 1206 self.zoom_out.emit() 1207 elif event.delta() > 0: 1208 self.zoom_in.emit() 1209 return 1210 QPlainTextEdit.wheelEvent(self, event) 1211 self.highlight_current_cell() 1212 1213 1214class QtANSIEscapeCodeHandler(ANSIEscapeCodeHandler): 1215 def __init__(self): 1216 ANSIEscapeCodeHandler.__init__(self) 1217 self.base_format = None 1218 self.current_format = None 1219 1220 def set_light_background(self, state): 1221 if state: 1222 self.default_foreground_color = 30 1223 self.default_background_color = 47 1224 else: 1225 self.default_foreground_color = 37 1226 self.default_background_color = 40 1227 1228 def set_base_format(self, base_format): 1229 self.base_format = base_format 1230 1231 def get_format(self): 1232 return self.current_format 1233 1234 def set_style(self): 1235 """ 1236 Set font style with the following attributes: 1237 'foreground_color', 'background_color', 'italic', 1238 'bold' and 'underline' 1239 """ 1240 if self.current_format is None: 1241 assert self.base_format is not None 1242 self.current_format = QTextCharFormat(self.base_format) 1243 # Foreground color 1244 if self.foreground_color is None: 1245 qcolor = self.base_format.foreground() 1246 else: 1247 cstr = self.ANSI_COLORS[self.foreground_color-30][self.intensity] 1248 qcolor = QColor(cstr) 1249 self.current_format.setForeground(qcolor) 1250 # Background color 1251 if self.background_color is None: 1252 qcolor = self.base_format.background() 1253 else: 1254 cstr = self.ANSI_COLORS[self.background_color-40][self.intensity] 1255 qcolor = QColor(cstr) 1256 self.current_format.setBackground(qcolor) 1257 1258 font = self.current_format.font() 1259 # Italic 1260 if self.italic is None: 1261 italic = self.base_format.fontItalic() 1262 else: 1263 italic = self.italic 1264 font.setItalic(italic) 1265 # Bold 1266 if self.bold is None: 1267 bold = self.base_format.font().bold() 1268 else: 1269 bold = self.bold 1270 font.setBold(bold) 1271 # Underline 1272 if self.underline is None: 1273 underline = self.base_format.font().underline() 1274 else: 1275 underline = self.underline 1276 font.setUnderline(underline) 1277 self.current_format.setFont(font) 1278 1279 1280def inverse_color(color): 1281 color.setHsv(color.hue(), color.saturation(), 255-color.value()) 1282 1283 1284class ConsoleFontStyle(object): 1285 def __init__(self, foregroundcolor, backgroundcolor, 1286 bold, italic, underline): 1287 self.foregroundcolor = foregroundcolor 1288 self.backgroundcolor = backgroundcolor 1289 self.bold = bold 1290 self.italic = italic 1291 self.underline = underline 1292 self.format = None 1293 1294 def apply_style(self, font, light_background, is_default): 1295 self.format = QTextCharFormat() 1296 self.format.setFont(font) 1297 foreground = QColor(self.foregroundcolor) 1298 if not light_background and is_default: 1299 inverse_color(foreground) 1300 self.format.setForeground(foreground) 1301 background = QColor(self.backgroundcolor) 1302 if not light_background: 1303 inverse_color(background) 1304 self.format.setBackground(background) 1305 font = self.format.font() 1306 font.setBold(self.bold) 1307 font.setItalic(self.italic) 1308 font.setUnderline(self.underline) 1309 self.format.setFont(font) 1310 1311 1312class ConsoleBaseWidget(TextEditBaseWidget): 1313 """Console base widget""" 1314 BRACE_MATCHING_SCOPE = ('sol', 'eol') 1315 COLOR_PATTERN = re.compile(r'\x01?\x1b\[(.*?)m\x02?') 1316 exception_occurred = Signal(str, bool) 1317 userListActivated = Signal(int, str) 1318 completion_widget_activated = Signal(str) 1319 1320 def __init__(self, parent=None): 1321 TextEditBaseWidget.__init__(self, parent) 1322 1323 self.light_background = True 1324 1325 self.setMaximumBlockCount(300) 1326 1327 # ANSI escape code handler 1328 self.ansi_handler = QtANSIEscapeCodeHandler() 1329 1330 # Disable undo/redo (nonsense for a console widget...): 1331 self.setUndoRedoEnabled(False) 1332 1333 self.userListActivated.connect(lambda user_id, text: 1334 self.completion_widget_activated.emit(text)) 1335 1336 self.default_style = ConsoleFontStyle( 1337 foregroundcolor=0x000000, backgroundcolor=0xFFFFFF, 1338 bold=False, italic=False, underline=False) 1339 self.error_style = ConsoleFontStyle( 1340 foregroundcolor=0xFF0000, backgroundcolor=0xFFFFFF, 1341 bold=False, italic=False, underline=False) 1342 self.traceback_link_style = ConsoleFontStyle( 1343 foregroundcolor=0x0000FF, backgroundcolor=0xFFFFFF, 1344 bold=True, italic=False, underline=True) 1345 self.prompt_style = ConsoleFontStyle( 1346 foregroundcolor=0x00AA00, backgroundcolor=0xFFFFFF, 1347 bold=True, italic=False, underline=False) 1348 self.font_styles = (self.default_style, self.error_style, 1349 self.traceback_link_style, self.prompt_style) 1350 self.set_pythonshell_font() 1351 self.setMouseTracking(True) 1352 1353 def set_light_background(self, state): 1354 self.light_background = state 1355 if state: 1356 self.set_palette(background=QColor(Qt.white), 1357 foreground=QColor(Qt.darkGray)) 1358 else: 1359 self.set_palette(background=QColor(Qt.black), 1360 foreground=QColor(Qt.lightGray)) 1361 self.ansi_handler.set_light_background(state) 1362 self.set_pythonshell_font() 1363 1364 #------Python shell 1365 def insert_text(self, text): 1366 """Reimplement TextEditBaseWidget method""" 1367 # Eventually this maybe should wrap to insert_text_to if 1368 # backspace-handling is required 1369 self.textCursor().insertText(text, self.default_style.format) 1370 1371 def paste(self): 1372 """Reimplement Qt method""" 1373 if self.has_selected_text(): 1374 self.remove_selected_text() 1375 self.insert_text(QApplication.clipboard().text()) 1376 1377 def append_text_to_shell(self, text, error, prompt): 1378 """ 1379 Append text to Python shell 1380 In a way, this method overrides the method 'insert_text' when text is 1381 inserted at the end of the text widget for a Python shell 1382 1383 Handles error messages and show blue underlined links 1384 Handles ANSI color sequences 1385 Handles ANSI FF sequence 1386 """ 1387 cursor = self.textCursor() 1388 cursor.movePosition(QTextCursor.End) 1389 if '\r' in text: # replace \r\n with \n 1390 text = text.replace('\r\n', '\n') 1391 text = text.replace('\r', '\n') 1392 while True: 1393 index = text.find(chr(12)) 1394 if index == -1: 1395 break 1396 text = text[index+1:] 1397 self.clear() 1398 if error: 1399 is_traceback = False 1400 for text in text.splitlines(True): 1401 if text.startswith(' File') \ 1402 and not text.startswith(' File "<'): 1403 is_traceback = True 1404 # Show error links in blue underlined text 1405 cursor.insertText(' ', self.default_style.format) 1406 cursor.insertText(text[2:], 1407 self.traceback_link_style.format) 1408 else: 1409 # Show error/warning messages in red 1410 cursor.insertText(text, self.error_style.format) 1411 self.exception_occurred.emit(text, is_traceback) 1412 elif prompt: 1413 # Show prompt in green 1414 insert_text_to(cursor, text, self.prompt_style.format) 1415 else: 1416 # Show other outputs in black 1417 last_end = 0 1418 for match in self.COLOR_PATTERN.finditer(text): 1419 insert_text_to(cursor, text[last_end:match.start()], 1420 self.default_style.format) 1421 last_end = match.end() 1422 try: 1423 for code in [int(_c) for _c in match.group(1).split(';')]: 1424 self.ansi_handler.set_code(code) 1425 except ValueError: 1426 pass 1427 self.default_style.format = self.ansi_handler.get_format() 1428 insert_text_to(cursor, text[last_end:], self.default_style.format) 1429# # Slower alternative: 1430# segments = self.COLOR_PATTERN.split(text) 1431# cursor.insertText(segments.pop(0), self.default_style.format) 1432# if segments: 1433# for ansi_tags, text in zip(segments[::2], segments[1::2]): 1434# for ansi_tag in ansi_tags.split(';'): 1435# self.ansi_handler.set_code(int(ansi_tag)) 1436# self.default_style.format = self.ansi_handler.get_format() 1437# cursor.insertText(text, self.default_style.format) 1438 self.set_cursor_position('eof') 1439 self.setCurrentCharFormat(self.default_style.format) 1440 1441 def set_pythonshell_font(self, font=None): 1442 """Python Shell only""" 1443 if font is None: 1444 font = QFont() 1445 for style in self.font_styles: 1446 style.apply_style(font=font, 1447 light_background=self.light_background, 1448 is_default=style is self.default_style) 1449 self.ansi_handler.set_base_format(self.default_style.format) 1450