1# -*- coding: utf-8 -*- 2 3# Copyright (c) 2002 - 2021 Detlev Offenbach <detlev@die-offenbachs.de> 4# 5 6""" 7Module implementing the editor component of the eric IDE. 8""" 9 10import os 11import re 12import difflib 13import contextlib 14 15import editorconfig 16 17from PyQt5.QtCore import ( 18 pyqtSignal, pyqtSlot, Qt, QDir, QTimer, QModelIndex, QFileInfo, 19 QCryptographicHash, QEvent, QDateTime, QPoint, QSize 20) 21from PyQt5.QtGui import QPalette, QFont, QPixmap, QPainter 22from PyQt5.QtWidgets import ( 23 QLineEdit, QActionGroup, QDialog, QInputDialog, QApplication, QMenu 24) 25from PyQt5.QtPrintSupport import QPrinter, QPrintDialog, QAbstractPrintDialog 26from PyQt5.Qsci import QsciScintilla, QsciMacro, QsciStyledText 27 28from E5Gui.E5Application import e5App 29from E5Gui import E5FileDialog, E5MessageBox 30from E5Gui.E5OverrideCursor import E5OverrideCursor 31 32from E5Utilities.E5Cache import E5Cache 33 34from .QsciScintillaCompat import QsciScintillaCompat 35from .EditorMarkerMap import EditorMarkerMap 36from .SpellChecker import SpellChecker 37 38import Preferences 39import Utilities 40from Utilities import MouseUtilities 41 42import UI.PixmapCache 43 44EditorAutoCompletionListID = 1 45TemplateCompletionListID = 2 46 47 48class Editor(QsciScintillaCompat): 49 """ 50 Class implementing the editor component of the eric IDE. 51 52 @signal modificationStatusChanged(bool, QsciScintillaCompat) emitted when 53 the modification status has changed 54 @signal undoAvailable(bool) emitted to signal the undo availability 55 @signal redoAvailable(bool) emitted to signal the redo availability 56 @signal cursorChanged(str, int, int) emitted when the cursor position 57 was changed 58 @signal cursorLineChanged(int) emitted when the cursor line was changed 59 @signal editorAboutToBeSaved(str) emitted before the editor is saved 60 @signal editorSaved(str) emitted after the editor has been saved 61 @signal editorRenamed(str) emitted after the editor got a new name 62 (i.e. after a 'Save As') 63 @signal captionChanged(str, QsciScintillaCompat) emitted when the caption 64 is updated. Typically due to a readOnly attribute change. 65 @signal breakpointToggled(QsciScintillaCompat) emitted when a breakpoint 66 is toggled 67 @signal bookmarkToggled(QsciScintillaCompat) emitted when a bookmark is 68 toggled 69 @signal syntaxerrorToggled(QsciScintillaCompat) emitted when a syntax error 70 was discovered 71 @signal autoCompletionAPIsAvailable(bool) emitted after the autocompletion 72 function has been configured 73 @signal coverageMarkersShown(bool) emitted after the coverage markers have 74 been shown or cleared 75 @signal taskMarkersUpdated(QsciScintillaCompat) emitted when the task 76 markers were updated 77 @signal changeMarkersUpdated(QsciScintillaCompat) emitted when the change 78 markers were updated 79 @signal showMenu(str, QMenu, QsciScintillaCompat) emitted when a menu is 80 about to be shown. The name of the menu, a reference to the menu and 81 a reference to the editor are given. 82 @signal languageChanged(str) emitted when the editors language was set. The 83 language is passed as a parameter. 84 @signal eolChanged(str) emitted when the editors eol type was set. The eol 85 string is passed as a parameter. 86 @signal encodingChanged(str) emitted when the editors encoding was set. The 87 encoding name is passed as a parameter. 88 @signal spellLanguageChanged(str) emitted when the editor spell check 89 language was set. The language is passed as a parameter. 90 @signal lastEditPositionAvailable() emitted when a last edit position is 91 available 92 @signal refreshed() emitted to signal a refresh of the editor contents 93 @signal settingsRead() emitted to signal, that the settings have been read 94 and set 95 @signal mouseDoubleClick(position, buttons) emitted to signal a mouse 96 double click somewhere in the editor area 97 """ 98 modificationStatusChanged = pyqtSignal(bool, QsciScintillaCompat) 99 undoAvailable = pyqtSignal(bool) 100 redoAvailable = pyqtSignal(bool) 101 cursorChanged = pyqtSignal(str, int, int) 102 cursorLineChanged = pyqtSignal(int) 103 editorAboutToBeSaved = pyqtSignal(str) 104 editorSaved = pyqtSignal(str) 105 editorRenamed = pyqtSignal(str) 106 captionChanged = pyqtSignal(str, QsciScintillaCompat) 107 breakpointToggled = pyqtSignal(QsciScintillaCompat) 108 bookmarkToggled = pyqtSignal(QsciScintillaCompat) 109 syntaxerrorToggled = pyqtSignal(QsciScintillaCompat) 110 autoCompletionAPIsAvailable = pyqtSignal(bool) 111 coverageMarkersShown = pyqtSignal(bool) 112 taskMarkersUpdated = pyqtSignal(QsciScintillaCompat) 113 changeMarkersUpdated = pyqtSignal(QsciScintillaCompat) 114 showMenu = pyqtSignal(str, QMenu, QsciScintillaCompat) 115 languageChanged = pyqtSignal(str) 116 eolChanged = pyqtSignal(str) 117 encodingChanged = pyqtSignal(str) 118 spellLanguageChanged = pyqtSignal(str) 119 lastEditPositionAvailable = pyqtSignal() 120 refreshed = pyqtSignal() 121 settingsRead = pyqtSignal() 122 mouseDoubleClick = pyqtSignal(QPoint, int) 123 124 WarningCode = 1 125 WarningStyle = 2 126 127 # Autocompletion icon definitions 128 ClassID = 1 129 ClassProtectedID = 2 130 ClassPrivateID = 3 131 MethodID = 4 132 MethodProtectedID = 5 133 MethodPrivateID = 6 134 AttributeID = 7 135 AttributeProtectedID = 8 136 AttributePrivateID = 9 137 EnumID = 10 138 KeywordsID = 11 139 ModuleID = 12 140 141 FromDocumentID = 99 142 143 TemplateImageID = 100 144 145 # Cooperation related definitions 146 Separator = "@@@" 147 148 StartEditToken = "START_EDIT" 149 EndEditToken = "END_EDIT" 150 CancelEditToken = "CANCEL_EDIT" 151 RequestSyncToken = "REQUEST_SYNC" 152 SyncToken = "SYNC" 153 154 VcsConflictMarkerLineRegExpList = ( 155 r"""^<<<<<<< .*?$""", 156 r"""^\|\|\|\|\|\|\| .*?$""", 157 r"""^=======.*?$""", 158 r"""^>>>>>>> .*?$""", 159 ) 160 161 EncloseChars = { 162 '"': '"', 163 "'": "'", 164 "(": "()", 165 ")": "()", 166 "{": "{}", # __IGNORE_WARNING_M613__ 167 "}": "{}", # __IGNORE_WARNING_M613__ 168 "[": "[]", 169 "]": "[]", 170 "<": "<>", 171 ">": "<>", 172 } 173 174 def __init__(self, dbs, fn="", vm=None, 175 filetype="", editor=None, tv=None, 176 parent=None): 177 """ 178 Constructor 179 180 @param dbs reference to the debug server object 181 @type DebugServer 182 @param fn name of the file to be opened. If it is None, a new (empty) 183 editor is opened. 184 @type str 185 @param vm reference to the view manager object 186 @type ViewManager 187 @param filetype type of the source file 188 @type str 189 @param editor reference to an Editor object, if this is a cloned view 190 @type Editor 191 @param tv reference to the task viewer object 192 @type TaskViewer 193 @param parent reference to the parent widget 194 @type QWidget 195 @exception OSError raised to indicate an issue accessing the file 196 """ 197 super().__init__(parent) 198 self.setAttribute(Qt.WidgetAttribute.WA_KeyCompression) 199 self.setUtf8(True) 200 201 self.dbs = dbs 202 self.taskViewer = tv 203 self.__setFileName(fn) 204 self.vm = vm 205 self.filetype = filetype 206 self.filetypeByFlag = False 207 self.noName = "" 208 self.project = e5App().getObject("Project") 209 210 # clear some variables 211 self.lastHighlight = None # remember the last highlighted line 212 self.lastErrorMarker = None # remember the last error line 213 self.lastCurrMarker = None # remember the last current line 214 215 self.breaks = {} 216 # key: marker handle, 217 # value: (lineno, condition, temporary, 218 # enabled, ignorecount) 219 self.bookmarks = [] 220 # bookmarks are just a list of handles to the 221 # bookmark markers 222 self.syntaxerrors = {} 223 # key: marker handle 224 # value: list of (error message, error index) 225 self.warnings = {} 226 # key: marker handle 227 # value: list of (warning message, warning type) 228 self.notcoveredMarkers = [] # just a list of marker handles 229 self.showingNotcoveredMarkers = False 230 231 self.lexer_ = None 232 self.apiLanguage = '' 233 234 self.__loadEditorConfig() 235 236 self.condHistory = [] 237 self.__lexerReset = False 238 self.completer = None 239 self.encoding = self.__getEditorConfig("DefaultEncoding") 240 self.lastModified = 0 241 self.line = -1 242 self.inReopenPrompt = False 243 # true if the prompt to reload a changed source is present 244 self.inFileRenamed = False 245 # true if we are propagating a rename action 246 self.inLanguageChanged = False 247 # true if we are propagating a language change 248 self.inEolChanged = False 249 # true if we are propagating an eol change 250 self.inEncodingChanged = False 251 # true if we are propagating an encoding change 252 self.inDragDrop = False 253 # true if we are in drop mode 254 self.inLinesChanged = False 255 # true if we are propagating a lines changed event 256 self.__hasTaskMarkers = False 257 # no task markers present 258 259 self.macros = {} # list of defined macros 260 self.curMacro = None 261 self.recording = False 262 263 self.acAPI = False 264 265 self.__lastEditPosition = None 266 self.__annotationLines = 0 267 268 self.__docstringGenerator = None 269 270 # list of clones 271 self.__clones = [] 272 273 # clear QScintilla defined keyboard commands 274 # we do our own handling through the view manager 275 self.clearAlternateKeys() 276 self.clearKeys() 277 278 self.__markerMap = EditorMarkerMap(self) 279 280 # initialize the mark occurrences timer 281 self.__markOccurrencesTimer = QTimer(self) 282 self.__markOccurrencesTimer.setSingleShot(True) 283 self.__markOccurrencesTimer.setInterval( 284 Preferences.getEditor("MarkOccurrencesTimeout")) 285 self.__markOccurrencesTimer.timeout.connect(self.__markOccurrences) 286 self.__markedText = "" 287 self.__searchIndicatorLines = [] 288 289 # initialize some spellchecking stuff 290 self.spell = None 291 self.lastLine = 0 292 self.lastIndex = 0 293 self.__inSpellLanguageChanged = False 294 295 # initialize some cooperation stuff 296 self.__isSyncing = False 297 self.__receivedWhileSyncing = [] 298 self.__savedText = "" 299 self.__inSharedEdit = False 300 self.__isShared = False 301 self.__inRemoteSharedEdit = False 302 303 # connect signals before loading the text 304 self.modificationChanged.connect(self.__modificationChanged) 305 self.cursorPositionChanged.connect(self.__cursorPositionChanged) 306 self.modificationAttempted.connect(self.__modificationReadOnly) 307 308 # define the margins markers 309 self.__changeMarkerSaved = self.markerDefine( 310 self.__createChangeMarkerPixmap( 311 "OnlineChangeTraceMarkerSaved")) 312 self.__changeMarkerUnsaved = self.markerDefine( 313 self.__createChangeMarkerPixmap( 314 "OnlineChangeTraceMarkerUnsaved")) 315 self.breakpoint = self.markerDefine( 316 UI.PixmapCache.getPixmap("break")) 317 self.cbreakpoint = self.markerDefine( 318 UI.PixmapCache.getPixmap("cBreak")) 319 self.tbreakpoint = self.markerDefine( 320 UI.PixmapCache.getPixmap("tBreak")) 321 self.tcbreakpoint = self.markerDefine( 322 UI.PixmapCache.getPixmap("tCBreak")) 323 self.dbreakpoint = self.markerDefine( 324 UI.PixmapCache.getPixmap("breakDisabled")) 325 self.bookmark = self.markerDefine( 326 UI.PixmapCache.getPixmap("bookmark16")) 327 self.syntaxerror = self.markerDefine( 328 UI.PixmapCache.getPixmap("syntaxError")) 329 self.notcovered = self.markerDefine( 330 UI.PixmapCache.getPixmap("notcovered")) 331 self.taskmarker = self.markerDefine( 332 UI.PixmapCache.getPixmap("task")) 333 self.warning = self.markerDefine( 334 UI.PixmapCache.getPixmap("warning")) 335 336 # define the line markers 337 if Preferences.getEditor("LineMarkersBackground"): 338 self.currentline = self.markerDefine( 339 QsciScintilla.MarkerSymbol.Background) 340 self.errorline = self.markerDefine( 341 QsciScintilla.MarkerSymbol.Background) 342 self.__setLineMarkerColours() 343 else: 344 self.currentline = self.markerDefine( 345 UI.PixmapCache.getPixmap("currentLineMarker")) 346 self.errorline = self.markerDefine( 347 UI.PixmapCache.getPixmap("errorLineMarker")) 348 349 self.breakpointMask = ( 350 (1 << self.breakpoint) | 351 (1 << self.cbreakpoint) | 352 (1 << self.tbreakpoint) | 353 (1 << self.tcbreakpoint) | 354 (1 << self.dbreakpoint) 355 ) 356 357 self.changeMarkersMask = ( 358 (1 << self.__changeMarkerSaved) | 359 (1 << self.__changeMarkerUnsaved) 360 ) 361 362 # configure the margins 363 self.__setMarginsDisplay() 364 self.linesChanged.connect(self.__resizeLinenoMargin) 365 366 self.marginClicked.connect(self.__marginClicked) 367 368 # set the eol mode 369 self.__setEolMode() 370 371 # set the text display 372 self.__setTextDisplay() 373 374 # initialize the online syntax check timer 375 try: 376 self.syntaxCheckService = e5App().getObject('SyntaxCheckService') 377 self.syntaxCheckService.syntaxChecked.connect( 378 self.__processSyntaxCheckResult) 379 self.syntaxCheckService.error.connect( 380 self.__processSyntaxCheckError) 381 self.__initOnlineSyntaxCheck() 382 except KeyError: 383 self.syntaxCheckService = None 384 385 self.isResourcesFile = False 386 if editor is None: 387 if self.fileName: 388 if ( 389 (QFileInfo(self.fileName).size() // 1024) > 390 Preferences.getEditor("WarnFilesize") 391 ): 392 res = E5MessageBox.yesNo( 393 self, 394 self.tr("Open File"), 395 self.tr("""<p>The size of the file <b>{0}</b>""" 396 """ is <b>{1} KB</b>.""" 397 """ Do you really want to load it?</p>""") 398 .format(self.fileName, 399 QFileInfo(self.fileName).size() // 1024), 400 icon=E5MessageBox.Warning) 401 if not res: 402 raise OSError() 403 self.readFile(self.fileName, True) 404 self.__bindLexer(self.fileName) 405 self.__bindCompleter(self.fileName) 406 self.checkSyntax() 407 self.isResourcesFile = self.fileName.endswith(".qrc") 408 409 self.__convertTabs() 410 411 self.recolor() 412 else: 413 # clone the given editor 414 self.setDocument(editor.document()) 415 self.breaks = editor.breaks 416 self.bookmarks = editor.bookmarks 417 self.syntaxerrors = editor.syntaxerrors 418 self.notcoveredMarkers = editor.notcoveredMarkers 419 self.showingNotcoveredMarkers = editor.showingNotcoveredMarkers 420 self.isResourcesFile = editor.isResourcesFile 421 self.lastModified = editor.lastModified 422 423 self.addClone(editor) 424 editor.addClone(self) 425 426 self.gotoLine(1) 427 428 # set the text display again 429 self.__setTextDisplay() 430 431 # set the auto-completion function 432 self.__acContext = True 433 self.__acText = "" 434 self.__acCompletions = set() 435 self.__acCompletionsFinished = 0 436 self.__acCache = E5Cache( 437 size=Preferences.getEditor("AutoCompletionCacheSize")) 438 self.__acCache.setMaximumCacheTime( 439 Preferences.getEditor("AutoCompletionCacheTime")) 440 self.__acCacheEnabled = Preferences.getEditor( 441 "AutoCompletionCacheEnabled") 442 self.__acTimer = QTimer(self) 443 self.__acTimer.setSingleShot(True) 444 self.__acTimer.setInterval( 445 Preferences.getEditor("AutoCompletionTimeout")) 446 self.__acTimer.timeout.connect(self.__autoComplete) 447 448 self.__acWatchdog = QTimer(self) 449 self.__acWatchdog.setSingleShot(True) 450 self.__acWatchdog.setInterval( 451 Preferences.getEditor("AutoCompletionWatchdogTime")) 452 self.__acWatchdog.timeout.connect(self.autoCompleteQScintilla) 453 454 self.userListActivated.connect(self.__completionListSelected) 455 self.SCN_CHARADDED.connect(self.__charAdded) 456 self.SCN_AUTOCCANCELLED.connect(self.__autocompletionCancelled) 457 458 self.__completionListHookFunctions = {} 459 self.__completionListAsyncHookFunctions = {} 460 self.__setAutoCompletion() 461 462 # set the call-tips function 463 self.__ctHookFunctions = {} 464 self.__setCallTips() 465 466 # set the mouse click handlers (fired on mouse release) 467 self.__mouseClickHandlers = {} 468 # dictionary with tuple of keyboard modifier and mouse button as key 469 # and tuple of plug-in name and function as value 470 471 sh = self.sizeHint() 472 if sh.height() < 300: 473 sh.setHeight(300) 474 self.resize(sh) 475 476 # Make sure tabbing through a QWorkspace works. 477 self.setFocusPolicy(Qt.FocusPolicy.StrongFocus) 478 479 self.__updateReadOnly(True) 480 481 self.setWhatsThis(self.tr( 482 """<b>A Source Editor Window</b>""" 483 """<p>This window is used to display and edit a source file.""" 484 """ You can open as many of these as you like. The name of the""" 485 """ file is displayed in the window's titlebar.</p>""" 486 """<p>In order to set breakpoints just click in the space""" 487 """ between the line numbers and the fold markers. Via the""" 488 """ context menu of the margins they may be edited.</p>""" 489 """<p>In order to set bookmarks just Shift click in the space""" 490 """ between the line numbers and the fold markers.</p>""" 491 """<p>These actions can be reversed via the context menu.</p>""" 492 """<p>Ctrl clicking on a syntax error marker shows some info""" 493 """ about this error.</p>""" 494 )) 495 496 # Set the editors size, if it is too big for the view manager. 497 if self.vm is not None: 498 req = self.size() 499 bnd = req.boundedTo(self.vm.size()) 500 501 if bnd.width() < req.width() or bnd.height() < req.height(): 502 self.resize(bnd) 503 504 # set the autosave flag 505 self.autosaveEnabled = Preferences.getEditor("AutosaveInterval") > 0 506 self.autosaveManuallyDisabled = False 507 508 self.__initContextMenu() 509 self.__initContextMenuMargins() 510 511 self.__checkEol() 512 if editor is None: 513 self.__checkLanguage() 514 self.__checkEncoding() 515 self.__checkSpellLanguage() 516 else: 517 # it's a clone 518 self.__languageChanged(editor.apiLanguage, propagate=False) 519 self.__encodingChanged(editor.encoding, propagate=False) 520 self.__spellLanguageChanged(editor.getSpellingLanguage(), 521 propagate=False) 522 # link the warnings to the original editor 523 self.warnings = editor.warnings 524 525 self.setAcceptDrops(True) 526 527 # breakpoint handling 528 self.breakpointModel = self.dbs.getBreakPointModel() 529 self.__restoreBreakpoints() 530 self.breakpointModel.rowsAboutToBeRemoved.connect( 531 self.__deleteBreakPoints) 532 self.breakpointModel.dataAboutToBeChanged.connect( 533 self.__breakPointDataAboutToBeChanged) 534 self.breakpointModel.dataChanged.connect( 535 self.__changeBreakPoints) 536 self.breakpointModel.rowsInserted.connect( 537 self.__addBreakPoints) 538 self.SCN_MODIFIED.connect(self.__modified) 539 540 # establish connection to some ViewManager action groups 541 self.addActions(self.vm.editorActGrp.actions()) 542 self.addActions(self.vm.editActGrp.actions()) 543 self.addActions(self.vm.copyActGrp.actions()) 544 self.addActions(self.vm.viewActGrp.actions()) 545 546 # register images to be shown in autocompletion lists 547 self.__registerImages() 548 549 # connect signals after loading the text 550 self.textChanged.connect(self.__textChanged) 551 552 # initialize the online change trace timer 553 self.__initOnlineChangeTrace() 554 555 if ( 556 self.fileName and 557 self.project.isOpen() and 558 self.project.isProjectSource(self.fileName) 559 ): 560 self.project.projectPropertiesChanged.connect( 561 self.__projectPropertiesChanged) 562 563 self.grabGesture(Qt.GestureType.PinchGesture) 564 565 self.SCN_ZOOM.connect(self.__markerMap.update) 566 self.__markerMap.update() 567 568 def __setFileName(self, name): 569 """ 570 Private method to set the file name of the current file. 571 572 @param name name of the current file 573 @type str 574 """ 575 self.fileName = name 576 577 if self.fileName: 578 self.__fileNameExtension = ( 579 os.path.splitext(self.fileName)[1][1:].lower() 580 ) 581 else: 582 self.__fileNameExtension = "" 583 584 def __registerImages(self): 585 """ 586 Private method to register images for autocompletion lists. 587 """ 588 # finale size of the completion images 589 imageSize = QSize(22, 22) 590 591 self.registerImage( 592 self.ClassID, 593 UI.PixmapCache.getPixmap("class", imageSize)) 594 self.registerImage( 595 self.ClassProtectedID, 596 UI.PixmapCache.getPixmap("class_protected", imageSize)) 597 self.registerImage( 598 self.ClassPrivateID, 599 UI.PixmapCache.getPixmap("class_private", imageSize)) 600 self.registerImage( 601 self.MethodID, 602 UI.PixmapCache.getPixmap("method", imageSize)) 603 self.registerImage( 604 self.MethodProtectedID, 605 UI.PixmapCache.getPixmap("method_protected", imageSize)) 606 self.registerImage( 607 self.MethodPrivateID, 608 UI.PixmapCache.getPixmap("method_private", imageSize)) 609 self.registerImage( 610 self.AttributeID, 611 UI.PixmapCache.getPixmap("attribute", imageSize)) 612 self.registerImage( 613 self.AttributeProtectedID, 614 UI.PixmapCache.getPixmap("attribute_protected", imageSize)) 615 self.registerImage( 616 self.AttributePrivateID, 617 UI.PixmapCache.getPixmap("attribute_private", imageSize)) 618 self.registerImage( 619 self.EnumID, 620 UI.PixmapCache.getPixmap("enum", imageSize)) 621 self.registerImage( 622 self.KeywordsID, 623 UI.PixmapCache.getPixmap("keywords", imageSize)) 624 self.registerImage( 625 self.ModuleID, 626 UI.PixmapCache.getPixmap("module", imageSize)) 627 628 self.registerImage( 629 self.FromDocumentID, 630 UI.PixmapCache.getPixmap("editor", imageSize)) 631 632 self.registerImage( 633 self.TemplateImageID, 634 UI.PixmapCache.getPixmap("templateViewer", imageSize)) 635 636 def addClone(self, editor): 637 """ 638 Public method to add a clone to our list. 639 640 @param editor reference to the cloned editor 641 @type Editor 642 """ 643 self.__clones.append(editor) 644 645 editor.editorRenamed.connect(self.fileRenamed) 646 editor.languageChanged.connect(self.languageChanged) 647 editor.eolChanged.connect(self.__eolChanged) 648 editor.encodingChanged.connect(self.__encodingChanged) 649 editor.spellLanguageChanged.connect(self.__spellLanguageChanged) 650 651 def removeClone(self, editor): 652 """ 653 Public method to remove a clone from our list. 654 655 @param editor reference to the cloned editor 656 @type Editor 657 """ 658 if editor in self.__clones: 659 editor.editorRenamed.disconnect(self.fileRenamed) 660 editor.languageChanged.disconnect(self.languageChanged) 661 editor.eolChanged.disconnect(self.__eolChanged) 662 editor.encodingChanged.disconnect(self.__encodingChanged) 663 editor.spellLanguageChanged.disconnect(self.__spellLanguageChanged) 664 self.__clones.remove(editor) 665 666 def isClone(self, editor): 667 """ 668 Public method to test, if the given editor is a clone. 669 670 @param editor reference to the cloned editor 671 @type Editor 672 @return flag indicating a clone 673 @rtype bool 674 """ 675 return editor in self.__clones 676 677 def __bindName(self, line0): 678 """ 679 Private method to generate a dummy filename for binding a lexer. 680 681 @param line0 first line of text to use in the generation process 682 (string) 683 @return dummy file name to be used for binding a lexer (string) 684 """ 685 bindName = "" 686 line0 = line0.lower() 687 688 # check first line if it does not start with #! 689 if line0.startswith(("<html", "<!doctype html", "<?php")): 690 bindName = "dummy.html" 691 elif line0.startswith(("<?xml", "<!doctype")): 692 bindName = "dummy.xml" 693 elif line0.startswith("index: "): 694 bindName = "dummy.diff" 695 elif line0.startswith("\\documentclass"): 696 bindName = "dummy.tex" 697 698 if not bindName and self.filetype: 699 # check filetype 700 from . import Lexers 701 supportedLanguages = Lexers.getSupportedLanguages() 702 if self.filetype in supportedLanguages: 703 bindName = supportedLanguages[self.filetype][1] 704 elif self.filetype in ["Python", "Python3", "MicroPython"]: 705 bindName = "dummy.py" 706 707 if not bindName and line0.startswith("#!"): 708 # #! marker detection 709 if ( 710 "python3" in line0 or 711 "python" in line0 or 712 "pypy3" in line0 or 713 "pypy" in line0 714 ): 715 bindName = "dummy.py" 716 self.filetype = "Python3" 717 elif ("/bash" in line0 or "/sh" in line0): 718 bindName = "dummy.sh" 719 elif "ruby" in line0: 720 bindName = "dummy.rb" 721 self.filetype = "Ruby" 722 elif "perl" in line0: 723 bindName = "dummy.pl" 724 elif "lua" in line0: 725 bindName = "dummy.lua" 726 elif "dmd" in line0: 727 bindName = "dummy.d" 728 self.filetype = "D" 729 730 if not bindName: 731 # mode line detection: -*- mode: python -*- 732 match = re.search(r"mode[:=]\s*([-\w_.]+)", line0) 733 if match: 734 mode = match.group(1).lower() 735 if mode in ["python3", "pypy3"]: 736 bindName = "dummy.py" 737 self.filetype = "Python3" 738 elif mode == "ruby": 739 bindName = "dummy.rb" 740 self.filetype = "Ruby" 741 elif mode == "perl": 742 bindName = "dummy.pl" 743 elif mode == "lua": 744 bindName = "dummy.lua" 745 elif mode in ["dmd", "d"]: 746 bindName = "dummy.d" 747 self.filetype = "D" 748 749 if not bindName: 750 bindName = self.fileName 751 752 return bindName 753 754 def getMenu(self, menuName): 755 """ 756 Public method to get a reference to the main context menu or a submenu. 757 758 @param menuName name of the menu (string) 759 @return reference to the requested menu (QMenu) or None 760 """ 761 try: 762 return self.__menus[menuName] 763 except KeyError: 764 return None 765 766 def hasMiniMenu(self): 767 """ 768 Public method to check the miniMenu flag. 769 770 @return flag indicating a minimized context menu (boolean) 771 """ 772 return self.miniMenu 773 774 def __initContextMenu(self): 775 """ 776 Private method used to setup the context menu. 777 """ 778 self.miniMenu = Preferences.getEditor("MiniContextMenu") 779 780 self.menuActs = {} 781 self.menu = QMenu() 782 self.__menus = { 783 "Main": self.menu, 784 } 785 786 self.languagesMenu = self.__initContextMenuLanguages() 787 self.__menus["Languages"] = self.languagesMenu 788 if self.isResourcesFile: 789 self.resourcesMenu = self.__initContextMenuResources() 790 self.__menus["Resources"] = self.resourcesMenu 791 else: 792 self.checksMenu = self.__initContextMenuChecks() 793 self.menuShow = self.__initContextMenuShow() 794 self.graphicsMenu = self.__initContextMenuGraphics() 795 self.autocompletionMenu = self.__initContextMenuAutocompletion() 796 self.__menus["Checks"] = self.checksMenu 797 self.__menus["Show"] = self.menuShow 798 self.__menus["Graphics"] = self.graphicsMenu 799 self.__menus["Autocompletion"] = self.autocompletionMenu 800 self.toolsMenu = self.__initContextMenuTools() 801 self.__menus["Tools"] = self.toolsMenu 802 self.exportersMenu = self.__initContextMenuExporters() 803 self.__menus["Exporters"] = self.exportersMenu 804 self.eolMenu = self.__initContextMenuEol() 805 self.__menus["Eol"] = self.eolMenu 806 self.encodingsMenu = self.__initContextMenuEncodings() 807 self.__menus["Encodings"] = self.encodingsMenu 808 self.spellLanguagesMenu = self.__initContextMenuSpellLanguages() 809 self.__menus["SpellLanguages"] = self.spellLanguagesMenu 810 811 self.menuActs["Undo"] = self.menu.addAction( 812 UI.PixmapCache.getIcon("editUndo"), 813 self.tr('Undo'), self.undo) 814 self.menuActs["Redo"] = self.menu.addAction( 815 UI.PixmapCache.getIcon("editRedo"), 816 self.tr('Redo'), self.redo) 817 self.menuActs["Revert"] = self.menu.addAction( 818 self.tr("Revert to last saved state"), 819 self.revertToUnmodified) 820 self.menu.addSeparator() 821 self.menuActs["Cut"] = self.menu.addAction( 822 UI.PixmapCache.getIcon("editCut"), 823 self.tr('Cut'), self.cut) 824 self.menuActs["Copy"] = self.menu.addAction( 825 UI.PixmapCache.getIcon("editCopy"), 826 self.tr('Copy'), self.copy) 827 self.menuActs["Paste"] = self.menu.addAction( 828 UI.PixmapCache.getIcon("editPaste"), 829 self.tr('Paste'), self.paste) 830 if not self.miniMenu: 831 self.menuActs["ExecuteSelection"] = self.menu.addAction( 832 self.tr("Execute Selection In Console"), 833 self.__executeSelection) 834 self.menu.addSeparator() 835 self.menu.addAction( 836 UI.PixmapCache.getIcon("editIndent"), 837 self.tr('Indent'), self.indentLineOrSelection) 838 self.menu.addAction( 839 UI.PixmapCache.getIcon("editUnindent"), 840 self.tr('Unindent'), self.unindentLineOrSelection) 841 self.menuActs["Comment"] = self.menu.addAction( 842 UI.PixmapCache.getIcon("editComment"), 843 self.tr('Comment'), self.commentLineOrSelection) 844 self.menuActs["Uncomment"] = self.menu.addAction( 845 UI.PixmapCache.getIcon("editUncomment"), 846 self.tr('Uncomment'), self.uncommentLineOrSelection) 847 self.menuActs["StreamComment"] = self.menu.addAction( 848 self.tr('Stream Comment'), 849 self.streamCommentLineOrSelection) 850 self.menuActs["BoxComment"] = self.menu.addAction( 851 self.tr('Box Comment'), 852 self.boxCommentLineOrSelection) 853 self.menu.addSeparator() 854 self.menuActs["Docstring"] = self.menu.addAction( 855 self.tr("Generate Docstring"), 856 self.__insertDocstring) 857 self.menu.addSeparator() 858 self.menu.addAction( 859 self.tr('Select to brace'), self.selectToMatchingBrace) 860 self.menu.addAction(self.tr('Select all'), self.__selectAll) 861 self.menu.addAction( 862 self.tr('Deselect all'), self.__deselectAll) 863 else: 864 self.menuActs["ExecuteSelection"] = None 865 self.menu.addSeparator() 866 self.menuActs["SpellCheck"] = self.menu.addAction( 867 UI.PixmapCache.getIcon("spellchecking"), 868 self.tr('Check spelling...'), self.checkSpelling) 869 self.menuActs["SpellCheckSelection"] = self.menu.addAction( 870 UI.PixmapCache.getIcon("spellchecking"), 871 self.tr('Check spelling of selection...'), 872 self.__checkSpellingSelection) 873 self.menuActs["SpellCheckRemove"] = self.menu.addAction( 874 self.tr("Remove from dictionary"), 875 self.__removeFromSpellingDictionary) 876 self.menuActs["SpellCheckLanguages"] = self.menu.addMenu( 877 self.spellLanguagesMenu) 878 self.menu.addSeparator() 879 self.menu.addAction( 880 self.tr('Shorten empty lines'), self.shortenEmptyLines) 881 self.menu.addSeparator() 882 self.menuActs["Languages"] = self.menu.addMenu(self.languagesMenu) 883 self.menuActs["Encodings"] = self.menu.addMenu(self.encodingsMenu) 884 self.menuActs["Eol"] = self.menu.addMenu(self.eolMenu) 885 self.menu.addSeparator() 886 self.menuActs["MonospacedFont"] = self.menu.addAction( 887 self.tr("Use Monospaced Font"), 888 self.handleMonospacedEnable) 889 self.menuActs["MonospacedFont"].setCheckable(True) 890 self.menuActs["MonospacedFont"].setChecked(self.useMonospaced) 891 self.menuActs["AutosaveEnable"] = self.menu.addAction( 892 self.tr("Autosave enabled"), self.__autosaveEnable) 893 self.menuActs["AutosaveEnable"].setCheckable(True) 894 self.menuActs["AutosaveEnable"].setChecked(self.autosaveEnabled) 895 self.menuActs["TypingAidsEnabled"] = self.menu.addAction( 896 self.tr("Typing aids enabled"), self.__toggleTypingAids) 897 self.menuActs["TypingAidsEnabled"].setCheckable(True) 898 self.menuActs["TypingAidsEnabled"].setEnabled( 899 self.completer is not None) 900 self.menuActs["TypingAidsEnabled"].setChecked( 901 self.completer is not None and self.completer.isEnabled()) 902 self.menuActs["AutoCompletionEnable"] = self.menu.addAction( 903 self.tr("Automatic Completion enabled"), 904 self.__toggleAutoCompletionEnable) 905 self.menuActs["AutoCompletionEnable"].setCheckable(True) 906 self.menuActs["AutoCompletionEnable"].setChecked( 907 self.autoCompletionThreshold() != -1) 908 if not self.isResourcesFile: 909 self.menu.addMenu(self.autocompletionMenu) 910 self.menuActs["calltip"] = self.menu.addAction( 911 self.tr('Calltip'), self.callTip) 912 self.menuActs["codeInfo"] = self.menu.addAction( 913 self.tr('Code Info'), self.__showCodeInfo) 914 self.menu.addSeparator() 915 if self.isResourcesFile: 916 self.menu.addMenu(self.resourcesMenu) 917 else: 918 self.menuActs["Check"] = self.menu.addMenu(self.checksMenu) 919 self.menu.addSeparator() 920 self.menuActs["Show"] = self.menu.addMenu(self.menuShow) 921 self.menu.addSeparator() 922 self.menuActs["Diagrams"] = self.menu.addMenu(self.graphicsMenu) 923 self.menu.addSeparator() 924 self.menuActs["Tools"] = self.menu.addMenu(self.toolsMenu) 925 self.menu.addSeparator() 926 self.menu.addAction( 927 UI.PixmapCache.getIcon("documentNewView"), 928 self.tr('New Document View'), self.__newView) 929 self.menuActs["NewSplit"] = self.menu.addAction( 930 UI.PixmapCache.getIcon("splitVertical"), 931 self.tr('New Document View (with new split)'), 932 self.__newViewNewSplit) 933 self.menuActs["NewSplit"].setEnabled(self.vm.canSplit()) 934 self.menu.addAction( 935 UI.PixmapCache.getIcon("close"), 936 self.tr('Close'), self.__contextClose) 937 self.menu.addSeparator() 938 self.reopenEncodingMenu = self.__initContextMenuReopenWithEncoding() 939 self.menuActs["Reopen"] = self.menu.addMenu(self.reopenEncodingMenu) 940 self.menuActs["Save"] = self.menu.addAction( 941 UI.PixmapCache.getIcon("fileSave"), 942 self.tr('Save'), self.__contextSave) 943 self.menu.addAction( 944 UI.PixmapCache.getIcon("fileSaveAs"), 945 self.tr('Save As...'), self.__contextSaveAs) 946 self.menu.addAction( 947 UI.PixmapCache.getIcon("fileSaveCopy"), 948 self.tr('Save Copy...'), self.__contextSaveCopy) 949 if not self.miniMenu: 950 self.menu.addMenu(self.exportersMenu) 951 self.menu.addSeparator() 952 self.menuActs["OpenRejections"] = self.menu.addAction( 953 self.tr("Open 'rejection' file"), 954 self.__contextOpenRejections) 955 self.menu.addSeparator() 956 self.menu.addAction( 957 UI.PixmapCache.getIcon("printPreview"), 958 self.tr("Print Preview"), self.printPreviewFile) 959 self.menu.addAction( 960 UI.PixmapCache.getIcon("print"), 961 self.tr('Print'), self.printFile) 962 else: 963 self.menuActs["OpenRejections"] = None 964 965 self.menu.aboutToShow.connect(self.__showContextMenu) 966 967 self.spellingMenu = QMenu() 968 self.__menus["Spelling"] = self.spellingMenu 969 970 self.spellingMenu.aboutToShow.connect(self.__showContextMenuSpelling) 971 self.spellingMenu.triggered.connect( 972 self.__contextMenuSpellingTriggered) 973 974 def __initContextMenuAutocompletion(self): 975 """ 976 Private method used to setup the Checks context sub menu. 977 978 @return reference to the generated menu (QMenu) 979 """ 980 menu = QMenu(self.tr('Complete')) 981 982 self.menuActs["acDynamic"] = menu.addAction( 983 self.tr('Complete'), self.autoComplete) 984 menu.addSeparator() 985 self.menuActs["acClearCache"] = menu.addAction( 986 self.tr("Clear Completions Cache"), self.__clearCompletionsCache) 987 menu.addSeparator() 988 menu.addAction( 989 self.tr('Complete from Document'), self.autoCompleteFromDocument) 990 self.menuActs["acAPI"] = menu.addAction( 991 self.tr('Complete from APIs'), self.autoCompleteFromAPIs) 992 self.menuActs["acAPIDocument"] = menu.addAction( 993 self.tr('Complete from Document and APIs'), 994 self.autoCompleteFromAll) 995 996 menu.aboutToShow.connect(self.__showContextMenuAutocompletion) 997 998 return menu 999 1000 def __initContextMenuChecks(self): 1001 """ 1002 Private method used to setup the Checks context sub menu. 1003 1004 @return reference to the generated menu (QMenu) 1005 """ 1006 menu = QMenu(self.tr('Check')) 1007 menu.aboutToShow.connect(self.__showContextMenuChecks) 1008 return menu 1009 1010 def __initContextMenuTools(self): 1011 """ 1012 Private method used to setup the Tools context sub menu. 1013 1014 @return reference to the generated menu (QMenu) 1015 """ 1016 menu = QMenu(self.tr('Tools')) 1017 menu.aboutToShow.connect(self.__showContextMenuTools) 1018 return menu 1019 1020 def __initContextMenuShow(self): 1021 """ 1022 Private method used to setup the Show context sub menu. 1023 1024 @return reference to the generated menu (QMenu) 1025 """ 1026 menu = QMenu(self.tr('Show')) 1027 1028 menu.addAction(self.tr('Code metrics...'), self.__showCodeMetrics) 1029 self.coverageMenuAct = menu.addAction( 1030 self.tr('Code coverage...'), self.__showCodeCoverage) 1031 self.coverageShowAnnotationMenuAct = menu.addAction( 1032 self.tr('Show code coverage annotations'), 1033 self.codeCoverageShowAnnotations) 1034 self.coverageHideAnnotationMenuAct = menu.addAction( 1035 self.tr('Hide code coverage annotations'), 1036 self.__codeCoverageHideAnnotations) 1037 self.profileMenuAct = menu.addAction( 1038 self.tr('Profile data...'), self.__showProfileData) 1039 1040 menu.aboutToShow.connect(self.__showContextMenuShow) 1041 1042 return menu 1043 1044 def __initContextMenuGraphics(self): 1045 """ 1046 Private method used to setup the diagrams context sub menu. 1047 1048 @return reference to the generated menu (QMenu) 1049 """ 1050 menu = QMenu(self.tr('Diagrams')) 1051 1052 menu.addAction( 1053 self.tr('Class Diagram...'), self.__showClassDiagram) 1054 menu.addAction( 1055 self.tr('Package Diagram...'), self.__showPackageDiagram) 1056 menu.addAction( 1057 self.tr('Imports Diagram...'), self.__showImportsDiagram) 1058 self.applicationDiagramMenuAct = menu.addAction( 1059 self.tr('Application Diagram...'), 1060 self.__showApplicationDiagram) 1061 menu.addSeparator() 1062 menu.addAction( 1063 UI.PixmapCache.getIcon("open"), 1064 self.tr("Load Diagram..."), self.__loadDiagram) 1065 1066 menu.aboutToShow.connect(self.__showContextMenuGraphics) 1067 1068 return menu 1069 1070 def __initContextMenuLanguages(self): 1071 """ 1072 Private method used to setup the Languages context sub menu. 1073 1074 @return reference to the generated menu (QMenu) 1075 """ 1076 menu = QMenu(self.tr("Languages")) 1077 1078 self.languagesActGrp = QActionGroup(self) 1079 self.noLanguageAct = menu.addAction( 1080 UI.PixmapCache.getIcon("fileText"), 1081 self.tr("No Language")) 1082 self.noLanguageAct.setCheckable(True) 1083 self.noLanguageAct.setData("None") 1084 self.languagesActGrp.addAction(self.noLanguageAct) 1085 menu.addSeparator() 1086 1087 from . import Lexers 1088 self.supportedLanguages = {} 1089 supportedLanguages = Lexers.getSupportedLanguages() 1090 languages = sorted(list(supportedLanguages.keys())) 1091 for language in languages: 1092 if language != "Guessed": 1093 self.supportedLanguages[language] = ( 1094 supportedLanguages[language][:2] 1095 ) 1096 act = menu.addAction( 1097 UI.PixmapCache.getIcon(supportedLanguages[language][2]), 1098 self.supportedLanguages[language][0]) 1099 act.setCheckable(True) 1100 act.setData(language) 1101 self.supportedLanguages[language].append(act) 1102 self.languagesActGrp.addAction(act) 1103 1104 menu.addSeparator() 1105 self.pygmentsAct = menu.addAction(self.tr("Guessed")) 1106 self.pygmentsAct.setCheckable(True) 1107 self.pygmentsAct.setData("Guessed") 1108 self.languagesActGrp.addAction(self.pygmentsAct) 1109 self.pygmentsSelAct = menu.addAction(self.tr("Alternatives")) 1110 self.pygmentsSelAct.setData("Alternatives") 1111 1112 menu.triggered.connect(self.__languageMenuTriggered) 1113 menu.aboutToShow.connect(self.__showContextMenuLanguages) 1114 1115 return menu 1116 1117 def __initContextMenuEncodings(self): 1118 """ 1119 Private method used to setup the Encodings context sub menu. 1120 1121 @return reference to the generated menu (QMenu) 1122 """ 1123 self.supportedEncodings = {} 1124 1125 menu = QMenu(self.tr("Encodings")) 1126 1127 self.encodingsActGrp = QActionGroup(self) 1128 1129 for encoding in sorted(Utilities.supportedCodecs): 1130 act = menu.addAction(encoding) 1131 act.setCheckable(True) 1132 act.setData(encoding) 1133 self.supportedEncodings[encoding] = act 1134 self.encodingsActGrp.addAction(act) 1135 1136 menu.triggered.connect(self.__encodingsMenuTriggered) 1137 menu.aboutToShow.connect(self.__showContextMenuEncodings) 1138 1139 return menu 1140 1141 def __initContextMenuReopenWithEncoding(self): 1142 """ 1143 Private method used to setup the Reopen With Encoding context sub menu. 1144 1145 @return reference to the generated menu (QMenu) 1146 """ 1147 menu = QMenu(self.tr("Re-Open With Encoding")) 1148 menu.setIcon(UI.PixmapCache.getIcon("open")) 1149 1150 for encoding in sorted(Utilities.supportedCodecs): 1151 act = menu.addAction(encoding) 1152 act.setData(encoding) 1153 1154 menu.triggered.connect(self.__reopenWithEncodingMenuTriggered) 1155 1156 return menu 1157 1158 def __initContextMenuEol(self): 1159 """ 1160 Private method to setup the eol context sub menu. 1161 1162 @return reference to the generated menu (QMenu) 1163 """ 1164 self.supportedEols = {} 1165 1166 menu = QMenu(self.tr("End-of-Line Type")) 1167 1168 self.eolActGrp = QActionGroup(self) 1169 1170 act = menu.addAction(UI.PixmapCache.getIcon("eolLinux"), 1171 self.tr("Unix")) 1172 act.setCheckable(True) 1173 act.setData('\n') 1174 self.supportedEols['\n'] = act 1175 self.eolActGrp.addAction(act) 1176 1177 act = menu.addAction(UI.PixmapCache.getIcon("eolWindows"), 1178 self.tr("Windows")) 1179 act.setCheckable(True) 1180 act.setData('\r\n') 1181 self.supportedEols['\r\n'] = act 1182 self.eolActGrp.addAction(act) 1183 1184 act = menu.addAction(UI.PixmapCache.getIcon("eolMac"), 1185 self.tr("Macintosh")) 1186 act.setCheckable(True) 1187 act.setData('\r') 1188 self.supportedEols['\r'] = act 1189 self.eolActGrp.addAction(act) 1190 1191 menu.triggered.connect(self.__eolMenuTriggered) 1192 menu.aboutToShow.connect(self.__showContextMenuEol) 1193 1194 return menu 1195 1196 def __initContextMenuSpellLanguages(self): 1197 """ 1198 Private method to setup the spell checking languages context sub menu. 1199 1200 @return reference to the generated menu 1201 @rtype QMenu 1202 """ 1203 self.supportedSpellLanguages = {} 1204 1205 menu = QMenu(self.tr("Spell Check Languages")) 1206 1207 self.spellLanguagesActGrp = QActionGroup(self) 1208 1209 self.noSpellLanguageAct = menu.addAction( 1210 self.tr("No Language")) 1211 self.noSpellLanguageAct.setCheckable(True) 1212 self.noSpellLanguageAct.setData("") 1213 self.spellLanguagesActGrp.addAction(self.noSpellLanguageAct) 1214 menu.addSeparator() 1215 1216 for language in sorted(SpellChecker.getAvailableLanguages()): 1217 act = menu.addAction(language) 1218 act.setCheckable(True) 1219 act.setData(language) 1220 self.supportedSpellLanguages[language] = act 1221 self.spellLanguagesActGrp.addAction(act) 1222 1223 menu.triggered.connect(self.__spellLanguagesMenuTriggered) 1224 menu.aboutToShow.connect(self.__showContextMenuSpellLanguages) 1225 1226 return menu 1227 1228 def __initContextMenuExporters(self): 1229 """ 1230 Private method used to setup the Exporters context sub menu. 1231 1232 @return reference to the generated menu (QMenu) 1233 """ 1234 menu = QMenu(self.tr("Export as")) 1235 1236 from . import Exporters 1237 supportedExporters = Exporters.getSupportedFormats() 1238 exporters = sorted(list(supportedExporters.keys())) 1239 for exporter in exporters: 1240 act = menu.addAction(supportedExporters[exporter]) 1241 act.setData(exporter) 1242 1243 menu.triggered.connect(self.__exportMenuTriggered) 1244 1245 return menu 1246 1247 def __initContextMenuMargins(self): 1248 """ 1249 Private method used to setup the context menu for the margins. 1250 """ 1251 self.marginMenuActs = {} 1252 1253 # bookmark margin 1254 self.bmMarginMenu = QMenu() 1255 1256 self.bmMarginMenu.addAction( 1257 self.tr('Toggle bookmark'), self.menuToggleBookmark) 1258 self.marginMenuActs["NextBookmark"] = self.bmMarginMenu.addAction( 1259 self.tr('Next bookmark'), self.nextBookmark) 1260 self.marginMenuActs["PreviousBookmark"] = self.bmMarginMenu.addAction( 1261 self.tr('Previous bookmark'), self.previousBookmark) 1262 self.marginMenuActs["ClearBookmark"] = self.bmMarginMenu.addAction( 1263 self.tr('Clear all bookmarks'), self.clearBookmarks) 1264 1265 self.bmMarginMenu.aboutToShow.connect( 1266 lambda: self.__showContextMenuMargin(self.bmMarginMenu)) 1267 1268 # breakpoint margin 1269 self.bpMarginMenu = QMenu() 1270 1271 self.marginMenuActs["Breakpoint"] = self.bpMarginMenu.addAction( 1272 self.tr('Toggle breakpoint'), self.menuToggleBreakpoint) 1273 self.marginMenuActs["TempBreakpoint"] = self.bpMarginMenu.addAction( 1274 self.tr('Toggle temporary breakpoint'), 1275 self.__menuToggleTemporaryBreakpoint) 1276 self.marginMenuActs["EditBreakpoint"] = self.bpMarginMenu.addAction( 1277 self.tr('Edit breakpoint...'), self.menuEditBreakpoint) 1278 self.marginMenuActs["EnableBreakpoint"] = self.bpMarginMenu.addAction( 1279 self.tr('Enable breakpoint'), 1280 self.__menuToggleBreakpointEnabled) 1281 self.marginMenuActs["NextBreakpoint"] = self.bpMarginMenu.addAction( 1282 self.tr('Next breakpoint'), self.menuNextBreakpoint) 1283 self.marginMenuActs["PreviousBreakpoint"] = ( 1284 self.bpMarginMenu.addAction( 1285 self.tr('Previous breakpoint'), 1286 self.menuPreviousBreakpoint) 1287 ) 1288 self.marginMenuActs["ClearBreakpoint"] = self.bpMarginMenu.addAction( 1289 self.tr('Clear all breakpoints'), self.__menuClearBreakpoints) 1290 1291 self.bpMarginMenu.aboutToShow.connect( 1292 lambda: self.__showContextMenuMargin(self.bpMarginMenu)) 1293 1294 # fold margin 1295 self.foldMarginMenu = QMenu() 1296 1297 self.marginMenuActs["ToggleAllFolds"] = ( 1298 self.foldMarginMenu.addAction( 1299 self.tr("Toggle all folds"), 1300 self.foldAll) 1301 ) 1302 self.marginMenuActs["ToggleAllFoldsAndChildren"] = ( 1303 self.foldMarginMenu.addAction( 1304 self.tr("Toggle all folds (including children)"), 1305 lambda: self.foldAll(True)) 1306 ) 1307 self.marginMenuActs["ToggleCurrentFold"] = ( 1308 self.foldMarginMenu.addAction( 1309 self.tr("Toggle current fold"), 1310 self.toggleCurrentFold) 1311 ) 1312 self.foldMarginMenu.addSeparator() 1313 self.marginMenuActs["ExpandChildren"] = ( 1314 self.foldMarginMenu.addAction( 1315 self.tr("Expand (including children)"), 1316 self.__contextMenuExpandFoldWithChildren) 1317 ) 1318 self.marginMenuActs["CollapseChildren"] = ( 1319 self.foldMarginMenu.addAction( 1320 self.tr("Collapse (including children)"), 1321 self.__contextMenuCollapseFoldWithChildren) 1322 ) 1323 self.foldMarginMenu.addSeparator() 1324 self.marginMenuActs["ClearAllFolds"] = ( 1325 self.foldMarginMenu.addAction( 1326 self.tr("Clear all folds"), 1327 self.clearFolds) 1328 ) 1329 1330 self.foldMarginMenu.aboutToShow.connect( 1331 lambda: self.__showContextMenuMargin(self.foldMarginMenu)) 1332 1333 # indicator margin 1334 self.indicMarginMenu = QMenu() 1335 1336 self.marginMenuActs["GotoSyntaxError"] = ( 1337 self.indicMarginMenu.addAction( 1338 self.tr('Goto syntax error'), self.gotoSyntaxError) 1339 ) 1340 self.marginMenuActs["ShowSyntaxError"] = ( 1341 self.indicMarginMenu.addAction( 1342 self.tr('Show syntax error message'), 1343 self.__showSyntaxError) 1344 ) 1345 self.marginMenuActs["ClearSyntaxError"] = ( 1346 self.indicMarginMenu.addAction( 1347 self.tr('Clear syntax error'), self.clearSyntaxError) 1348 ) 1349 self.indicMarginMenu.addSeparator() 1350 self.marginMenuActs["NextWarningMarker"] = ( 1351 self.indicMarginMenu.addAction( 1352 self.tr("Next warning"), self.nextWarning) 1353 ) 1354 self.marginMenuActs["PreviousWarningMarker"] = ( 1355 self.indicMarginMenu.addAction( 1356 self.tr("Previous warning"), self.previousWarning) 1357 ) 1358 self.marginMenuActs["ShowWarning"] = ( 1359 self.indicMarginMenu.addAction( 1360 self.tr('Show warning message'), self.__showWarning) 1361 ) 1362 self.marginMenuActs["ClearWarnings"] = ( 1363 self.indicMarginMenu.addAction( 1364 self.tr('Clear warnings'), self.clearWarnings) 1365 ) 1366 self.indicMarginMenu.addSeparator() 1367 self.marginMenuActs["NextCoverageMarker"] = ( 1368 self.indicMarginMenu.addAction( 1369 self.tr('Next uncovered line'), self.nextUncovered) 1370 ) 1371 self.marginMenuActs["PreviousCoverageMarker"] = ( 1372 self.indicMarginMenu.addAction( 1373 self.tr('Previous uncovered line'), self.previousUncovered) 1374 ) 1375 self.indicMarginMenu.addSeparator() 1376 self.marginMenuActs["NextTaskMarker"] = ( 1377 self.indicMarginMenu.addAction( 1378 self.tr('Next task'), self.nextTask) 1379 ) 1380 self.marginMenuActs["PreviousTaskMarker"] = ( 1381 self.indicMarginMenu.addAction( 1382 self.tr('Previous task'), self.previousTask) 1383 ) 1384 self.indicMarginMenu.addSeparator() 1385 self.marginMenuActs["NextChangeMarker"] = ( 1386 self.indicMarginMenu.addAction( 1387 self.tr('Next change'), self.nextChange) 1388 ) 1389 self.marginMenuActs["PreviousChangeMarker"] = ( 1390 self.indicMarginMenu.addAction( 1391 self.tr('Previous change'), self.previousChange) 1392 ) 1393 self.marginMenuActs["ClearChangeMarkers"] = ( 1394 self.indicMarginMenu.addAction( 1395 self.tr('Clear changes'), self.__reinitOnlineChangeTrace) 1396 ) 1397 1398 self.indicMarginMenu.aboutToShow.connect( 1399 lambda: self.__showContextMenuMargin(self.indicMarginMenu)) 1400 1401 def __exportMenuTriggered(self, act): 1402 """ 1403 Private method to handle the selection of an export format. 1404 1405 @param act reference to the action that was triggered (QAction) 1406 """ 1407 exporterFormat = act.data() 1408 self.exportFile(exporterFormat) 1409 1410 def exportFile(self, exporterFormat): 1411 """ 1412 Public method to export the file. 1413 1414 @param exporterFormat format the file should be exported into (string) 1415 """ 1416 if exporterFormat: 1417 from . import Exporters 1418 exporter = Exporters.getExporter(exporterFormat, self) 1419 if exporter: 1420 exporter.exportSource() 1421 else: 1422 E5MessageBox.critical( 1423 self, 1424 self.tr("Export source"), 1425 self.tr( 1426 """<p>No exporter available for the """ 1427 """export format <b>{0}</b>. Aborting...</p>""") 1428 .format(exporterFormat)) 1429 else: 1430 E5MessageBox.critical( 1431 self, 1432 self.tr("Export source"), 1433 self.tr("""No export format given. Aborting...""")) 1434 1435 def __showContextMenuLanguages(self): 1436 """ 1437 Private slot handling the aboutToShow signal of the languages context 1438 menu. 1439 """ 1440 if self.apiLanguage.startswith("Pygments|"): 1441 self.pygmentsSelAct.setText( 1442 self.tr("Alternatives ({0})").format( 1443 self.getLanguage(normalized=False))) 1444 else: 1445 self.pygmentsSelAct.setText(self.tr("Alternatives")) 1446 self.showMenu.emit("Languages", self.languagesMenu, self) 1447 1448 def __selectPygmentsLexer(self): 1449 """ 1450 Private method to select a specific pygments lexer. 1451 1452 @return name of the selected pygments lexer (string) 1453 """ 1454 from pygments.lexers import get_all_lexers 1455 lexerList = sorted(lex[0] for lex in get_all_lexers()) 1456 try: 1457 lexerSel = lexerList.index( 1458 self.getLanguage(normalized=False, forPygments=True)) 1459 except ValueError: 1460 lexerSel = 0 1461 lexerName, ok = QInputDialog.getItem( 1462 self, 1463 self.tr("Pygments Lexer"), 1464 self.tr("Select the Pygments lexer to apply."), 1465 lexerList, 1466 lexerSel, 1467 False) 1468 if ok and lexerName: 1469 return lexerName 1470 else: 1471 return "" 1472 1473 def __languageMenuTriggered(self, act): 1474 """ 1475 Private method to handle the selection of a lexer language. 1476 1477 @param act reference to the action that was triggered (QAction) 1478 """ 1479 if act == self.noLanguageAct: 1480 self.__resetLanguage() 1481 elif act == self.pygmentsAct: 1482 self.setLanguage("dummy.pygments") 1483 elif act == self.pygmentsSelAct: 1484 language = self.__selectPygmentsLexer() 1485 if language: 1486 self.setLanguage("dummy.pygments", pyname=language) 1487 else: 1488 language = act.data() 1489 if language: 1490 self.filetype = language 1491 self.setLanguage(self.supportedLanguages[language][1]) 1492 self.checkSyntax() 1493 1494 self.__docstringGenerator = None 1495 1496 def __languageChanged(self, language, propagate=True): 1497 """ 1498 Private slot handling a change of a connected editor's language. 1499 1500 @param language language to be set (string) 1501 @param propagate flag indicating to propagate the change (boolean) 1502 """ 1503 if language == '': 1504 self.__resetLanguage(propagate=propagate) 1505 elif language == "Guessed": 1506 self.setLanguage("dummy.pygments", 1507 propagate=propagate) 1508 elif language.startswith("Pygments|"): 1509 pyname = language.split("|", 1)[1] 1510 self.setLanguage("dummy.pygments", pyname=pyname, 1511 propagate=propagate) 1512 else: 1513 self.filetype = language 1514 self.setLanguage(self.supportedLanguages[language][1], 1515 propagate=propagate) 1516 self.checkSyntax() 1517 1518 self.__docstringGenerator = None 1519 1520 def __resetLanguage(self, propagate=True): 1521 """ 1522 Private method used to reset the language selection. 1523 1524 @param propagate flag indicating to propagate the change (boolean) 1525 """ 1526 if ( 1527 self.lexer_ is not None and 1528 (self.lexer_.lexer() == "container" or 1529 self.lexer_.lexer() is None) 1530 ): 1531 self.SCN_STYLENEEDED.disconnect(self.__styleNeeded) 1532 1533 self.apiLanguage = "" 1534 self.lexer_ = None 1535 self.__lexerReset = True 1536 self.setLexer() 1537 if self.completer is not None: 1538 self.completer.setEnabled(False) 1539 self.completer = None 1540 useMonospaced = self.useMonospaced 1541 self.__setTextDisplay() 1542 self.__setMarginsDisplay() 1543 self.setMonospaced(useMonospaced) 1544 self.menuActs["MonospacedFont"].setChecked(self.useMonospaced) 1545 1546 self.__docstringGenerator = None 1547 1548 if not self.inLanguageChanged and propagate: 1549 self.inLanguageChanged = True 1550 self.languageChanged.emit(self.apiLanguage) 1551 self.inLanguageChanged = False 1552 1553 def setLanguage(self, filename, initTextDisplay=True, propagate=True, 1554 pyname=""): 1555 """ 1556 Public method to set a lexer language. 1557 1558 @param filename filename used to determine the associated lexer 1559 language (string) 1560 @param initTextDisplay flag indicating an initialization of the text 1561 display is required as well (boolean) 1562 @param propagate flag indicating to propagate the change (boolean) 1563 @param pyname name of the pygments lexer to use (string) 1564 """ 1565 # clear all warning and syntax error markers 1566 self.clearSyntaxError() 1567 self.clearWarnings() 1568 1569 self.menuActs["MonospacedFont"].setChecked(False) 1570 1571 self.__lexerReset = False 1572 self.__bindLexer(filename, pyname=pyname) 1573 self.__bindCompleter(filename) 1574 self.recolor() 1575 self.__checkLanguage() 1576 1577 self.__docstringGenerator = None 1578 1579 # set the text display 1580 if initTextDisplay: 1581 self.__setTextDisplay() 1582 1583 # set the auto-completion and call-tips function 1584 self.__setAutoCompletion() 1585 self.__setCallTips() 1586 1587 if not self.inLanguageChanged and propagate: 1588 self.inLanguageChanged = True 1589 self.languageChanged.emit(self.apiLanguage) 1590 self.inLanguageChanged = False 1591 1592 def __checkLanguage(self): 1593 """ 1594 Private method to check the selected language of the language submenu. 1595 """ 1596 if self.apiLanguage == "": 1597 self.noLanguageAct.setChecked(True) 1598 elif self.apiLanguage == "Guessed": 1599 self.pygmentsAct.setChecked(True) 1600 elif self.apiLanguage.startswith("Pygments|"): 1601 act = self.languagesActGrp.checkedAction() 1602 if act: 1603 act.setChecked(False) 1604 else: 1605 self.supportedLanguages[self.apiLanguage][2].setChecked(True) 1606 1607 def projectLexerAssociationsChanged(self): 1608 """ 1609 Public slot to handle changes of the project lexer associations. 1610 """ 1611 self.setLanguage(self.fileName) 1612 1613 def __showContextMenuEncodings(self): 1614 """ 1615 Private slot handling the aboutToShow signal of the encodings context 1616 menu. 1617 """ 1618 self.showMenu.emit("Encodings", self.encodingsMenu, self) 1619 1620 def __encodingsMenuTriggered(self, act): 1621 """ 1622 Private method to handle the selection of an encoding. 1623 1624 @param act reference to the action that was triggered (QAction) 1625 """ 1626 encoding = act.data() 1627 self.setModified(True) 1628 self.__encodingChanged("{0}-selected".format(encoding)) 1629 1630 def __checkEncoding(self): 1631 """ 1632 Private method to check the selected encoding of the encodings submenu. 1633 """ 1634 with contextlib.suppress(AttributeError, KeyError): 1635 (self.supportedEncodings[self.__normalizedEncoding()] 1636 .setChecked(True)) 1637 1638 def __encodingChanged(self, encoding, propagate=True): 1639 """ 1640 Private slot to handle a change of the encoding. 1641 1642 @param encoding changed encoding (string) 1643 @param propagate flag indicating to propagate the change (boolean) 1644 """ 1645 self.encoding = encoding 1646 self.__checkEncoding() 1647 1648 if not self.inEncodingChanged and propagate: 1649 self.inEncodingChanged = True 1650 self.encodingChanged.emit(self.encoding) 1651 self.inEncodingChanged = False 1652 1653 def __normalizedEncoding(self, encoding=""): 1654 """ 1655 Private method to calculate the normalized encoding string. 1656 1657 @param encoding encoding to be normalized (string) 1658 @return normalized encoding (string) 1659 """ 1660 if not encoding: 1661 encoding = self.encoding 1662 return ( 1663 encoding 1664 .replace("-default", "") 1665 .replace("-guessed", "") 1666 .replace("-selected", "") 1667 ) 1668 1669 def __showContextMenuEol(self): 1670 """ 1671 Private slot handling the aboutToShow signal of the eol context menu. 1672 """ 1673 self.showMenu.emit("Eol", self.eolMenu, self) 1674 1675 def __eolMenuTriggered(self, act): 1676 """ 1677 Private method to handle the selection of an eol type. 1678 1679 @param act reference to the action that was triggered (QAction) 1680 """ 1681 eol = act.data() 1682 self.setEolModeByEolString(eol) 1683 self.convertEols(self.eolMode()) 1684 1685 def __checkEol(self): 1686 """ 1687 Private method to check the selected eol type of the eol submenu. 1688 """ 1689 with contextlib.suppress(AttributeError, TypeError): 1690 self.supportedEols[self.getLineSeparator()].setChecked(True) 1691 1692 def __eolChanged(self): 1693 """ 1694 Private slot to handle a change of the eol mode. 1695 """ 1696 self.__checkEol() 1697 1698 if not self.inEolChanged: 1699 self.inEolChanged = True 1700 eol = self.getLineSeparator() 1701 self.eolChanged.emit(eol) 1702 self.inEolChanged = False 1703 1704 def __showContextMenuSpellLanguages(self): 1705 """ 1706 Private slot handling the aboutToShow signal of the spell check 1707 languages context menu. 1708 """ 1709 self.showMenu.emit("SpellLanguage", self.spellLanguagesMenu, self) 1710 1711 def __spellLanguagesMenuTriggered(self, act): 1712 """ 1713 Private method to handle the selection of a spell check language. 1714 1715 @param act reference to the action that was triggered 1716 @type QAction 1717 """ 1718 language = act.data() 1719 self.__setSpellingLanguage(language) 1720 self.spellLanguageChanged.emit(language) 1721 1722 def __checkSpellLanguage(self): 1723 """ 1724 Private slot to check the selected spell check language action. 1725 """ 1726 language = self.getSpellingLanguage() 1727 with contextlib.suppress(AttributeError, KeyError): 1728 self.supportedSpellLanguages[language].setChecked(True) 1729 1730 def __spellLanguageChanged(self, language, propagate=True): 1731 """ 1732 Private slot to handle a change of the spell check language. 1733 1734 @param language new spell check language 1735 @type str 1736 @param propagate flag indicating to propagate the change 1737 @type bool 1738 """ 1739 self.__setSpellingLanguage(language) 1740 self.__checkSpellLanguage() 1741 1742 if not self.__inSpellLanguageChanged and propagate: 1743 self.__inSpellLanguageChanged = True 1744 self.spellLanguageChanged.emit(language) 1745 self.__inSpellLanguageChanged = False 1746 1747 def __bindLexer(self, filename, pyname=""): 1748 """ 1749 Private slot to set the correct lexer depending on language. 1750 1751 @param filename filename used to determine the associated lexer 1752 language (string) 1753 @param pyname name of the pygments lexer to use (string) 1754 """ 1755 if ( 1756 self.lexer_ is not None and 1757 (self.lexer_.lexer() == "container" or 1758 self.lexer_.lexer() is None) 1759 ): 1760 self.SCN_STYLENEEDED.disconnect(self.__styleNeeded) 1761 1762 language = "" 1763 if not self.filetype: 1764 if filename: 1765 basename = os.path.basename(filename) 1766 if ( 1767 self.project.isOpen() and 1768 self.project.isProjectFile(filename) 1769 ): 1770 language = self.project.getEditorLexerAssoc(basename) 1771 if not language: 1772 language = Preferences.getEditorLexerAssoc(basename) 1773 if not language: 1774 bindName = self.__bindName(self.text(0)) 1775 if bindName: 1776 language = Preferences.getEditorLexerAssoc(bindName) 1777 if language == "Python": 1778 # correction for Python 1779 pyVer = Utilities.determinePythonVersion( 1780 filename, self.text(0), self) 1781 language = "Python{0}".format(pyVer) 1782 if language in ['Python3', 'MicroPython', 'Cython', 'Ruby', 1783 'JavaScript', 'YAML', 'JSON']: 1784 self.filetype = language 1785 else: 1786 self.filetype = "" 1787 else: 1788 language = self.filetype 1789 1790 if language.startswith("Pygments|"): 1791 pyname = language 1792 self.filetype = language.split("|")[-1] 1793 language = "" 1794 1795 from . import Lexers 1796 self.lexer_ = Lexers.getLexer(language, self, pyname=pyname) 1797 if self.lexer_ is None: 1798 self.setLexer() 1799 self.apiLanguage = "" 1800 return 1801 1802 if pyname: 1803 if pyname.startswith("Pygments|"): 1804 self.apiLanguage = pyname 1805 else: 1806 self.apiLanguage = "Pygments|{0}".format(pyname) 1807 else: 1808 if language == "Protocol": 1809 self.apiLanguage = language 1810 else: 1811 # Change API language for lexer where QScintilla reports 1812 # an abbreviated name. 1813 self.apiLanguage = self.lexer_.language() 1814 if self.apiLanguage == "POV": 1815 self.apiLanguage = "Povray" 1816 elif self.apiLanguage == "PO": 1817 self.apiLanguage = "Gettext" 1818 self.setLexer(self.lexer_) 1819 self.__setMarginsDisplay() 1820 if self.lexer_.lexer() == "container" or self.lexer_.lexer() is None: 1821 self.SCN_STYLENEEDED.connect(self.__styleNeeded) 1822 1823 # get the font for style 0 and set it as the default font 1824 key = ( 1825 'Scintilla/Guessed/style0/font' 1826 if pyname and pyname.startswith("Pygments|") else 1827 'Scintilla/{0}/style0/font'.format(self.lexer_.language()) 1828 ) 1829 fdesc = Preferences.Prefs.settings.value(key) 1830 if fdesc is not None: 1831 font = QFont(fdesc[0], int(fdesc[1])) 1832 self.lexer_.setDefaultFont(font) 1833 self.lexer_.readSettings(Preferences.Prefs.settings, "Scintilla") 1834 if self.lexer_.hasSubstyles(): 1835 self.lexer_.readSubstyles(self) 1836 1837 # now set the lexer properties 1838 self.lexer_.initProperties() 1839 1840 # initialize the lexer APIs settings 1841 projectType = ( 1842 self.project.getProjectType() 1843 if self.project.isOpen() and self.project.isProjectFile(filename) 1844 else "" 1845 ) 1846 api = self.vm.getAPIsManager().getAPIs(self.apiLanguage, 1847 projectType=projectType) 1848 if api is not None and not api.isEmpty(): 1849 self.lexer_.setAPIs(api.getQsciAPIs()) 1850 self.acAPI = True 1851 else: 1852 self.acAPI = False 1853 self.autoCompletionAPIsAvailable.emit(self.acAPI) 1854 1855 self.__setAnnotationStyles() 1856 1857 self.lexer_.setDefaultColor(self.lexer_.color(0)) 1858 self.lexer_.setDefaultPaper(self.lexer_.paper(0)) 1859 1860 def __styleNeeded(self, position): 1861 """ 1862 Private slot to handle the need for more styling. 1863 1864 @param position end position, that needs styling (integer) 1865 """ 1866 self.lexer_.styleText(self.getEndStyled(), position) 1867 1868 def getLexer(self): 1869 """ 1870 Public method to retrieve a reference to the lexer object. 1871 1872 @return the lexer object (Lexer) 1873 """ 1874 return self.lexer_ 1875 1876 def getLanguage(self, normalized=True, forPygments=False): 1877 """ 1878 Public method to retrieve the language of the editor. 1879 1880 @param normalized flag indicating to normalize some Pygments 1881 lexer names (boolean) 1882 @param forPygments flag indicating to normalize some lexer 1883 names for Pygments (boolean) 1884 @return language of the editor (string) 1885 """ 1886 if ( 1887 self.apiLanguage == "Guessed" or 1888 self.apiLanguage.startswith("Pygments|") 1889 ): 1890 lang = self.lexer_.name() 1891 if normalized: 1892 # adjust some Pygments lexer names 1893 if lang in ("Python 2.x", "Python"): 1894 lang = "Python3" 1895 elif lang == "Protocol Buffer": 1896 lang = "Protocol" 1897 1898 else: 1899 lang = self.apiLanguage 1900 if forPygments: 1901 # adjust some names to Pygments lexer names 1902 if lang == "Python3": 1903 lang = "Python" 1904 elif lang == "Protocol": 1905 lang = "Protocol Buffer" 1906 return lang 1907 1908 def getApiLanguage(self): 1909 """ 1910 Public method to get the API language of the editor. 1911 1912 @return API language 1913 @rtype str 1914 """ 1915 return self.apiLanguage 1916 1917 def __bindCompleter(self, filename): 1918 """ 1919 Private slot to set the correct typing completer depending on language. 1920 1921 @param filename filename used to determine the associated typing 1922 completer language (string) 1923 """ 1924 if self.completer is not None: 1925 self.completer.setEnabled(False) 1926 self.completer = None 1927 1928 filename = os.path.basename(filename) 1929 apiLanguage = Preferences.getEditorLexerAssoc(filename) 1930 if apiLanguage == "": 1931 pyVer = self.__getPyVersion() 1932 if pyVer: 1933 apiLanguage = "Python{0}".format(pyVer) 1934 elif self.isRubyFile(): 1935 apiLanguage = "Ruby" 1936 1937 from . import TypingCompleters 1938 self.completer = TypingCompleters.getCompleter(apiLanguage, self) 1939 1940 def getCompleter(self): 1941 """ 1942 Public method to retrieve a reference to the completer object. 1943 1944 @return the completer object (CompleterBase) 1945 """ 1946 return self.completer 1947 1948 def __modificationChanged(self, m): 1949 """ 1950 Private slot to handle the modificationChanged signal. 1951 1952 It emits the signal modificationStatusChanged with parameters 1953 m and self. 1954 1955 @param m modification status 1956 """ 1957 if not m and bool(self.fileName): 1958 self.lastModified = QFileInfo(self.fileName).lastModified() 1959 self.modificationStatusChanged.emit(m, self) 1960 self.undoAvailable.emit(self.isUndoAvailable()) 1961 self.redoAvailable.emit(self.isRedoAvailable()) 1962 1963 def __cursorPositionChanged(self, line, index): 1964 """ 1965 Private slot to handle the cursorPositionChanged signal. 1966 1967 It emits the signal cursorChanged with parameters fileName, 1968 line and pos. 1969 1970 @param line line number of the cursor 1971 @param index position in line of the cursor 1972 """ 1973 self.cursorChanged.emit(self.fileName, line + 1, index) 1974 1975 if Preferences.getEditor("MarkOccurrencesEnabled"): 1976 self.__markOccurrencesTimer.stop() 1977 self.__markOccurrencesTimer.start() 1978 1979 if self.lastLine != line: 1980 self.cursorLineChanged.emit(line) 1981 1982 if self.spell is not None: 1983 # do spell checking 1984 doSpelling = True 1985 if self.lastLine == line: 1986 start, end = self.getWordBoundaries( 1987 line, index, useWordChars=False) 1988 if start <= self.lastIndex and self.lastIndex <= end: 1989 doSpelling = False 1990 if doSpelling: 1991 pos = self.positionFromLineIndex(self.lastLine, self.lastIndex) 1992 self.spell.checkWord(pos) 1993 1994 if self.lastLine != line: 1995 self.__markerMap.update() 1996 1997 self.lastLine = line 1998 self.lastIndex = index 1999 2000 def __modificationReadOnly(self): 2001 """ 2002 Private slot to handle the modificationAttempted signal. 2003 """ 2004 E5MessageBox.warning( 2005 self, 2006 self.tr("Modification of Read Only file"), 2007 self.tr("""You are attempting to change a read only file. """ 2008 """Please save to a different file first.""")) 2009 2010 def setNoName(self, noName): 2011 """ 2012 Public method to set the display string for an unnamed editor. 2013 2014 @param noName display string for this unnamed editor (string) 2015 """ 2016 self.noName = noName 2017 2018 def getNoName(self): 2019 """ 2020 Public method to get the display string for an unnamed editor. 2021 2022 @return display string for this unnamed editor (string) 2023 """ 2024 return self.noName 2025 2026 def getFileName(self): 2027 """ 2028 Public method to return the name of the file being displayed. 2029 2030 @return filename of the displayed file (string) 2031 """ 2032 return self.fileName 2033 2034 def getFileType(self): 2035 """ 2036 Public method to return the type of the file being displayed. 2037 2038 @return type of the displayed file (string) 2039 """ 2040 return self.filetype 2041 2042 def getFileTypeByFlag(self): 2043 """ 2044 Public method to return the type of the file, if it was set by an 2045 eflag: marker. 2046 2047 @return type of the displayed file, if set by an eflag: marker or an 2048 empty string (string) 2049 """ 2050 if self.filetypeByFlag: 2051 return self.filetype 2052 else: 2053 return "" 2054 2055 def determineFileType(self): 2056 """ 2057 Public method to determine the file type using various tests. 2058 2059 @return type of the displayed file or an empty string (string) 2060 """ 2061 ftype = self.filetype 2062 if not ftype: 2063 pyVer = self.__getPyVersion() 2064 if pyVer: 2065 ftype = "Python{0}".format(pyVer) 2066 elif self.isRubyFile(): 2067 ftype = "Ruby" 2068 else: 2069 ftype = "" 2070 2071 return ftype 2072 2073 def getEncoding(self): 2074 """ 2075 Public method to return the current encoding. 2076 2077 @return current encoding (string) 2078 """ 2079 return self.encoding 2080 2081 def __getPyVersion(self): 2082 """ 2083 Private method to return the Python main version or 0 if it's 2084 not a Python file at all. 2085 2086 @return Python version or 0 if it's not a Python file (int) 2087 """ 2088 return Utilities.determinePythonVersion( 2089 self.fileName, self.text(0), self) 2090 2091 def isPyFile(self): 2092 """ 2093 Public method to return a flag indicating a Python (2 or 3) file. 2094 2095 @return flag indicating a Python3 file (boolean) 2096 """ 2097 return self.__getPyVersion() == 3 2098 2099 def isPy2File(self): 2100 """ 2101 Public method to return a flag indicating a Python2 file. 2102 2103 @return flag reporting always False 2104 @rtype bool 2105 """ 2106 # kept to keep the API compatible for plugins 2107 return False 2108 2109 def isPy3File(self): 2110 """ 2111 Public method to return a flag indicating a Python3 file. 2112 2113 @return flag indicating a Python3 file (boolean) 2114 """ 2115 return self.__getPyVersion() == 3 2116 2117 def isMicroPythonFile(self): 2118 """ 2119 Public method to return a flag indicating a MicroPython file. 2120 2121 @return flag indicating a MicroPython file 2122 @rtype bool 2123 """ 2124 if self.filetype == "MicroPython": 2125 return True 2126 2127 return False 2128 2129 def isCythonFile(self): 2130 """ 2131 Public method to return a flag indicating a Cython file. 2132 2133 @return flag indicating a Cython file 2134 @rtype bool 2135 """ 2136 if self.filetype == "Cython": 2137 return True 2138 2139 return False 2140 2141 def isRubyFile(self): 2142 """ 2143 Public method to return a flag indicating a Ruby file. 2144 2145 @return flag indicating a Ruby file (boolean) 2146 """ 2147 if self.filetype == "Ruby": 2148 return True 2149 2150 if self.filetype == "": 2151 line0 = self.text(0) 2152 if ( 2153 line0.startswith("#!") and 2154 "ruby" in line0 2155 ): 2156 self.filetype = "Ruby" 2157 return True 2158 2159 if ( 2160 bool(self.fileName) and 2161 os.path.splitext(self.fileName)[1] in 2162 self.dbs.getExtensions('Ruby') 2163 ): 2164 self.filetype = "Ruby" 2165 return True 2166 2167 return False 2168 2169 def isJavascriptFile(self): 2170 """ 2171 Public method to return a flag indicating a Javascript file. 2172 2173 @return flag indicating a Javascript file (boolean) 2174 """ 2175 if self.filetype == "JavaScript": 2176 return True 2177 2178 if ( 2179 self.filetype == "" and 2180 self.fileName and 2181 os.path.splitext(self.fileName)[1] == ".js" 2182 ): 2183 self.filetype = "JavaScript" 2184 return True 2185 2186 return False 2187 2188 def highlightVisible(self): 2189 """ 2190 Public method to make sure that the highlight is visible. 2191 """ 2192 if self.lastHighlight is not None: 2193 lineno = self.markerLine(self.lastHighlight) 2194 self.ensureVisible(lineno + 1) 2195 2196 def highlight(self, line=None, error=False, syntaxError=False): 2197 """ 2198 Public method to highlight [or de-highlight] a particular line. 2199 2200 @param line line number to highlight (integer) 2201 @param error flag indicating whether the error highlight should be 2202 used (boolean) 2203 @param syntaxError flag indicating a syntax error (boolean) 2204 """ 2205 if line is None: 2206 self.lastHighlight = None 2207 if self.lastErrorMarker is not None: 2208 self.markerDeleteHandle(self.lastErrorMarker) 2209 self.lastErrorMarker = None 2210 if self.lastCurrMarker is not None: 2211 self.markerDeleteHandle(self.lastCurrMarker) 2212 self.lastCurrMarker = None 2213 else: 2214 if error: 2215 if self.lastErrorMarker is not None: 2216 self.markerDeleteHandle(self.lastErrorMarker) 2217 self.lastErrorMarker = self.markerAdd(line - 1, self.errorline) 2218 self.lastHighlight = self.lastErrorMarker 2219 else: 2220 if self.lastCurrMarker is not None: 2221 self.markerDeleteHandle(self.lastCurrMarker) 2222 self.lastCurrMarker = self.markerAdd(line - 1, 2223 self.currentline) 2224 self.lastHighlight = self.lastCurrMarker 2225 self.setCursorPosition(line - 1, 0) 2226 2227 def getHighlightPosition(self): 2228 """ 2229 Public method to return the position of the highlight bar. 2230 2231 @return line number of the highlight bar (integer) 2232 """ 2233 if self.lastHighlight is not None: 2234 return self.markerLine(self.lastHighlight) 2235 else: 2236 return 1 2237 2238 ########################################################################### 2239 ## Breakpoint handling methods below 2240 ########################################################################### 2241 2242 def __modified(self, pos, mtype, text, length, linesAdded, line, foldNow, 2243 foldPrev, token, annotationLinesAdded): 2244 """ 2245 Private method to handle changes of the number of lines. 2246 2247 @param pos start position of change (integer) 2248 @param mtype flags identifying the change (integer) 2249 @param text text that is given to the Undo system (string) 2250 @param length length of the change (integer) 2251 @param linesAdded number of added/deleted lines (integer) 2252 @param line line number of a fold level or marker change (integer) 2253 @param foldNow new fold level (integer) 2254 @param foldPrev previous fold level (integer) 2255 @param token ??? 2256 @param annotationLinesAdded number of added/deleted annotation lines 2257 (integer) 2258 """ 2259 if ( 2260 mtype & (self.SC_MOD_INSERTTEXT | self.SC_MOD_DELETETEXT) and 2261 linesAdded != 0 and 2262 self.breaks 2263 ): 2264 bps = [] # list of breakpoints 2265 for handle, (ln, cond, temp, enabled, ignorecount) in ( 2266 self.breaks.items() 2267 ): 2268 line = self.markerLine(handle) + 1 2269 if ln != line: 2270 bps.append((ln, line)) 2271 self.breaks[handle] = (line, cond, temp, enabled, 2272 ignorecount) 2273 self.inLinesChanged = True 2274 for ln, line in sorted(bps, reverse=linesAdded > 0): 2275 index1 = self.breakpointModel.getBreakPointIndex( 2276 self.fileName, ln) 2277 index2 = self.breakpointModel.index(index1.row(), 1) 2278 self.breakpointModel.setData(index2, line) 2279 self.inLinesChanged = False 2280 2281 def __restoreBreakpoints(self): 2282 """ 2283 Private method to restore the breakpoints. 2284 """ 2285 for handle in list(self.breaks.keys()): 2286 self.markerDeleteHandle(handle) 2287 self.__addBreakPoints( 2288 QModelIndex(), 0, self.breakpointModel.rowCount() - 1) 2289 self.__markerMap.update() 2290 2291 def __deleteBreakPoints(self, parentIndex, start, end): 2292 """ 2293 Private slot to delete breakpoints. 2294 2295 @param parentIndex index of parent item (QModelIndex) 2296 @param start start row (integer) 2297 @param end end row (integer) 2298 """ 2299 for row in range(start, end + 1): 2300 index = self.breakpointModel.index(row, 0, parentIndex) 2301 fn, lineno = self.breakpointModel.getBreakPointByIndex(index)[0:2] 2302 if fn == self.fileName: 2303 self.clearBreakpoint(lineno) 2304 2305 def __changeBreakPoints(self, startIndex, endIndex): 2306 """ 2307 Private slot to set changed breakpoints. 2308 2309 @param startIndex start index of the breakpoints being changed 2310 (QModelIndex) 2311 @param endIndex end index of the breakpoints being changed 2312 (QModelIndex) 2313 """ 2314 if not self.inLinesChanged: 2315 self.__addBreakPoints(QModelIndex(), startIndex.row(), 2316 endIndex.row()) 2317 2318 def __breakPointDataAboutToBeChanged(self, startIndex, endIndex): 2319 """ 2320 Private slot to handle the dataAboutToBeChanged signal of the 2321 breakpoint model. 2322 2323 @param startIndex start index of the rows to be changed (QModelIndex) 2324 @param endIndex end index of the rows to be changed (QModelIndex) 2325 """ 2326 self.__deleteBreakPoints(QModelIndex(), startIndex.row(), 2327 endIndex.row()) 2328 2329 def __addBreakPoints(self, parentIndex, start, end): 2330 """ 2331 Private slot to add breakpoints. 2332 2333 @param parentIndex index of parent item (QModelIndex) 2334 @param start start row (integer) 2335 @param end end row (integer) 2336 """ 2337 for row in range(start, end + 1): 2338 index = self.breakpointModel.index(row, 0, parentIndex) 2339 fn, line, cond, temp, enabled, ignorecount = ( 2340 self.breakpointModel.getBreakPointByIndex(index)[:6] 2341 ) 2342 if fn == self.fileName: 2343 self.newBreakpointWithProperties( 2344 line, (cond, temp, enabled, ignorecount)) 2345 2346 def clearBreakpoint(self, line): 2347 """ 2348 Public method to clear a breakpoint. 2349 2350 Note: This doesn't clear the breakpoint in the debugger, 2351 it just deletes it from the editor internal list of breakpoints. 2352 2353 @param line line number of the breakpoint (integer) 2354 """ 2355 if self.inLinesChanged: 2356 return 2357 2358 for handle in self.breaks: 2359 if self.markerLine(handle) == line - 1: 2360 break 2361 else: 2362 # not found, simply ignore it 2363 return 2364 2365 del self.breaks[handle] 2366 self.markerDeleteHandle(handle) 2367 self.__markerMap.update() 2368 2369 def newBreakpointWithProperties(self, line, properties): 2370 """ 2371 Public method to set a new breakpoint and its properties. 2372 2373 @param line line number of the breakpoint (integer) 2374 @param properties properties for the breakpoint (tuple) 2375 (condition, temporary flag, enabled flag, ignore count) 2376 """ 2377 if not properties[2]: 2378 marker = self.dbreakpoint 2379 elif properties[0]: 2380 marker = properties[1] and self.tcbreakpoint or self.cbreakpoint 2381 else: 2382 marker = properties[1] and self.tbreakpoint or self.breakpoint 2383 2384 if self.markersAtLine(line - 1) & self.breakpointMask == 0: 2385 handle = self.markerAdd(line - 1, marker) 2386 self.breaks[handle] = (line,) + properties 2387 self.breakpointToggled.emit(self) 2388 self.__markerMap.update() 2389 2390 def __toggleBreakpoint(self, line, temporary=False): 2391 """ 2392 Private method to toggle a breakpoint. 2393 2394 @param line line number of the breakpoint (integer) 2395 @param temporary flag indicating a temporary breakpoint (boolean) 2396 """ 2397 for handle in self.breaks: 2398 if self.markerLine(handle) == line - 1: 2399 # delete breakpoint or toggle it to the next state 2400 index = self.breakpointModel.getBreakPointIndex( 2401 self.fileName, line) 2402 if ( 2403 Preferences.getDebugger("ThreeStateBreakPoints") and 2404 not self.breakpointModel.isBreakPointTemporaryByIndex( 2405 index) 2406 ): 2407 self.breakpointModel.deleteBreakPointByIndex(index) 2408 self.__addBreakPoint(line, True) 2409 else: 2410 self.breakpointModel.deleteBreakPointByIndex(index) 2411 self.breakpointToggled.emit(self) 2412 break 2413 else: 2414 self.__addBreakPoint(line, temporary) 2415 2416 def __addBreakPoint(self, line, temporary): 2417 """ 2418 Private method to add a new breakpoint. 2419 2420 @param line line number of the breakpoint (integer) 2421 @param temporary flag indicating a temporary breakpoint (boolean) 2422 """ 2423 if self.fileName and self.isPyFile(): 2424 self.breakpointModel.addBreakPoint( 2425 self.fileName, line, ('', temporary, True, 0)) 2426 self.breakpointToggled.emit(self) 2427 2428 def __toggleBreakpointEnabled(self, line): 2429 """ 2430 Private method to toggle a breakpoints enabled status. 2431 2432 @param line line number of the breakpoint (integer) 2433 """ 2434 for handle in self.breaks: 2435 if self.markerLine(handle) == line - 1: 2436 index = self.breakpointModel.getBreakPointIndex( 2437 self.fileName, line) 2438 self.breakpointModel.setBreakPointEnabledByIndex( 2439 index, not self.breaks[handle][3]) 2440 break 2441 2442 def curLineHasBreakpoint(self): 2443 """ 2444 Public method to check for the presence of a breakpoint at the current 2445 line. 2446 2447 @return flag indicating the presence of a breakpoint (boolean) 2448 """ 2449 line, _ = self.getCursorPosition() 2450 return self.markersAtLine(line) & self.breakpointMask != 0 2451 2452 def getBreakpointLines(self): 2453 """ 2454 Public method to get the lines containing a breakpoint. 2455 2456 @return list of lines containing a breakpoint (list of integer) 2457 """ 2458 lines = [] 2459 line = -1 2460 while True: 2461 line = self.markerFindNext(line + 1, self.breakpointMask) 2462 if line < 0: 2463 break 2464 else: 2465 lines.append(line) 2466 return lines 2467 2468 def hasBreakpoints(self): 2469 """ 2470 Public method to check for the presence of breakpoints. 2471 2472 @return flag indicating the presence of breakpoints (boolean) 2473 """ 2474 return len(self.breaks) > 0 2475 2476 def __menuToggleTemporaryBreakpoint(self): 2477 """ 2478 Private slot to handle the 'Toggle temporary breakpoint' context menu 2479 action. 2480 """ 2481 if self.line < 0: 2482 self.line, index = self.getCursorPosition() 2483 self.line += 1 2484 self.__toggleBreakpoint(self.line, 1) 2485 self.line = -1 2486 2487 def menuToggleBreakpoint(self): 2488 """ 2489 Public slot to handle the 'Toggle breakpoint' context menu action. 2490 """ 2491 if self.line < 0: 2492 self.line, index = self.getCursorPosition() 2493 self.line += 1 2494 self.__toggleBreakpoint(self.line) 2495 self.line = -1 2496 2497 def __menuToggleBreakpointEnabled(self): 2498 """ 2499 Private slot to handle the 'Enable/Disable breakpoint' context menu 2500 action. 2501 """ 2502 if self.line < 0: 2503 self.line, index = self.getCursorPosition() 2504 self.line += 1 2505 self.__toggleBreakpointEnabled(self.line) 2506 self.line = -1 2507 2508 def menuEditBreakpoint(self, line=None): 2509 """ 2510 Public slot to handle the 'Edit breakpoint' context menu action. 2511 2512 @param line linenumber of the breakpoint to edit 2513 """ 2514 if line is not None: 2515 self.line = line - 1 2516 if self.line < 0: 2517 self.line, index = self.getCursorPosition() 2518 2519 for handle in self.breaks: 2520 if self.markerLine(handle) == self.line: 2521 ln, cond, temp, enabled, ignorecount = self.breaks[handle] 2522 index = self.breakpointModel.getBreakPointIndex(self.fileName, 2523 ln) 2524 if not index.isValid(): 2525 return 2526 2527 from Debugger.EditBreakpointDialog import EditBreakpointDialog 2528 dlg = EditBreakpointDialog( 2529 (self.fileName, ln), 2530 (cond, temp, enabled, ignorecount), 2531 self.condHistory, self, modal=True) 2532 if dlg.exec() == QDialog.DialogCode.Accepted: 2533 cond, temp, enabled, ignorecount = dlg.getData() 2534 self.breakpointModel.setBreakPointByIndex( 2535 index, self.fileName, ln, 2536 (cond, temp, enabled, ignorecount)) 2537 break 2538 2539 self.line = -1 2540 2541 def menuNextBreakpoint(self): 2542 """ 2543 Public slot to handle the 'Next breakpoint' context menu action. 2544 """ 2545 line, index = self.getCursorPosition() 2546 if line == self.lines() - 1: 2547 line = 0 2548 else: 2549 line += 1 2550 bpline = self.markerFindNext(line, self.breakpointMask) 2551 if bpline < 0: 2552 # wrap around 2553 bpline = self.markerFindNext(0, self.breakpointMask) 2554 if bpline >= 0: 2555 self.setCursorPosition(bpline, 0) 2556 self.ensureLineVisible(bpline) 2557 2558 def menuPreviousBreakpoint(self): 2559 """ 2560 Public slot to handle the 'Previous breakpoint' context menu action. 2561 """ 2562 line, index = self.getCursorPosition() 2563 if line == 0: 2564 line = self.lines() - 1 2565 else: 2566 line -= 1 2567 bpline = self.markerFindPrevious(line, self.breakpointMask) 2568 if bpline < 0: 2569 # wrap around 2570 bpline = self.markerFindPrevious( 2571 self.lines() - 1, self.breakpointMask) 2572 if bpline >= 0: 2573 self.setCursorPosition(bpline, 0) 2574 self.ensureLineVisible(bpline) 2575 2576 def __menuClearBreakpoints(self): 2577 """ 2578 Private slot to handle the 'Clear all breakpoints' context menu action. 2579 """ 2580 self.__clearBreakpoints(self.fileName) 2581 2582 def __clearBreakpoints(self, fileName): 2583 """ 2584 Private slot to clear all breakpoints. 2585 2586 @param fileName name of the file (string) 2587 """ 2588 idxList = [] 2589 for (ln, _, _, _, _) in self.breaks.values(): 2590 index = self.breakpointModel.getBreakPointIndex(fileName, ln) 2591 if index.isValid(): 2592 idxList.append(index) 2593 if idxList: 2594 self.breakpointModel.deleteBreakPoints(idxList) 2595 2596 ########################################################################### 2597 ## Bookmark handling methods below 2598 ########################################################################### 2599 2600 def toggleBookmark(self, line): 2601 """ 2602 Public method to toggle a bookmark. 2603 2604 @param line line number of the bookmark (integer) 2605 """ 2606 for handle in self.bookmarks: 2607 if self.markerLine(handle) == line - 1: 2608 self.bookmarks.remove(handle) 2609 self.markerDeleteHandle(handle) 2610 break 2611 else: 2612 # set a new bookmark 2613 handle = self.markerAdd(line - 1, self.bookmark) 2614 self.bookmarks.append(handle) 2615 self.bookmarkToggled.emit(self) 2616 self.__markerMap.update() 2617 2618 def getBookmarks(self): 2619 """ 2620 Public method to retrieve the bookmarks. 2621 2622 @return sorted list of all lines containing a bookmark 2623 (list of integer) 2624 """ 2625 bmlist = [] 2626 for handle in self.bookmarks: 2627 bmlist.append(self.markerLine(handle) + 1) 2628 2629 bmlist.sort() 2630 return bmlist 2631 2632 def getBookmarkLines(self): 2633 """ 2634 Public method to get the lines containing a bookmark. 2635 2636 @return list of lines containing a bookmark (list of integer) 2637 """ 2638 lines = [] 2639 line = -1 2640 while True: 2641 line = self.markerFindNext(line + 1, 1 << self.bookmark) 2642 if line < 0: 2643 break 2644 else: 2645 lines.append(line) 2646 return lines 2647 2648 def hasBookmarks(self): 2649 """ 2650 Public method to check for the presence of bookmarks. 2651 2652 @return flag indicating the presence of bookmarks (boolean) 2653 """ 2654 return len(self.bookmarks) > 0 2655 2656 def menuToggleBookmark(self): 2657 """ 2658 Public slot to handle the 'Toggle bookmark' context menu action. 2659 """ 2660 if self.line < 0: 2661 self.line, index = self.getCursorPosition() 2662 self.line += 1 2663 self.toggleBookmark(self.line) 2664 self.line = -1 2665 2666 def nextBookmark(self): 2667 """ 2668 Public slot to handle the 'Next bookmark' context menu action. 2669 """ 2670 line, index = self.getCursorPosition() 2671 if line == self.lines() - 1: 2672 line = 0 2673 else: 2674 line += 1 2675 bmline = self.markerFindNext(line, 1 << self.bookmark) 2676 if bmline < 0: 2677 # wrap around 2678 bmline = self.markerFindNext(0, 1 << self.bookmark) 2679 if bmline >= 0: 2680 self.setCursorPosition(bmline, 0) 2681 self.ensureLineVisible(bmline) 2682 2683 def previousBookmark(self): 2684 """ 2685 Public slot to handle the 'Previous bookmark' context menu action. 2686 """ 2687 line, index = self.getCursorPosition() 2688 if line == 0: 2689 line = self.lines() - 1 2690 else: 2691 line -= 1 2692 bmline = self.markerFindPrevious(line, 1 << self.bookmark) 2693 if bmline < 0: 2694 # wrap around 2695 bmline = self.markerFindPrevious( 2696 self.lines() - 1, 1 << self.bookmark) 2697 if bmline >= 0: 2698 self.setCursorPosition(bmline, 0) 2699 self.ensureLineVisible(bmline) 2700 2701 def clearBookmarks(self): 2702 """ 2703 Public slot to handle the 'Clear all bookmarks' context menu action. 2704 """ 2705 for handle in self.bookmarks: 2706 self.markerDeleteHandle(handle) 2707 self.bookmarks.clear() 2708 self.bookmarkToggled.emit(self) 2709 self.__markerMap.update() 2710 2711 ########################################################################### 2712 ## Printing methods below 2713 ########################################################################### 2714 2715 def printFile(self): 2716 """ 2717 Public slot to print the text. 2718 """ 2719 from .Printer import Printer 2720 printer = Printer(mode=QPrinter.PrinterMode.HighResolution) 2721 sb = e5App().getObject("UserInterface").statusBar() 2722 printDialog = QPrintDialog(printer, self) 2723 if self.hasSelectedText(): 2724 printDialog.setOption( 2725 QAbstractPrintDialog.PrintDialogOption.PrintSelection, 2726 True) 2727 if printDialog.exec() == QDialog.DialogCode.Accepted: 2728 sb.showMessage(self.tr('Printing...')) 2729 QApplication.processEvents() 2730 fn = self.getFileName() 2731 if fn is not None: 2732 printer.setDocName(os.path.basename(fn)) 2733 else: 2734 printer.setDocName(self.noName) 2735 if ( 2736 printDialog.printRange() == 2737 QAbstractPrintDialog.PrintRange.Selection 2738 ): 2739 # get the selection 2740 fromLine, fromIndex, toLine, toIndex = self.getSelection() 2741 if toIndex == 0: 2742 toLine -= 1 2743 # QScintilla seems to print one line more than told 2744 res = printer.printRange(self, fromLine, toLine - 1) 2745 else: 2746 res = printer.printRange(self) 2747 if res: 2748 sb.showMessage(self.tr('Printing completed'), 2000) 2749 else: 2750 sb.showMessage(self.tr('Error while printing'), 2000) 2751 QApplication.processEvents() 2752 else: 2753 sb.showMessage(self.tr('Printing aborted'), 2000) 2754 QApplication.processEvents() 2755 2756 def printPreviewFile(self): 2757 """ 2758 Public slot to show a print preview of the text. 2759 """ 2760 from PyQt5.QtPrintSupport import QPrintPreviewDialog 2761 from .Printer import Printer 2762 2763 printer = Printer(mode=QPrinter.PrinterMode.HighResolution) 2764 fn = self.getFileName() 2765 if fn is not None: 2766 printer.setDocName(os.path.basename(fn)) 2767 else: 2768 printer.setDocName(self.noName) 2769 preview = QPrintPreviewDialog(printer, self) 2770 preview.paintRequested.connect(self.__printPreview) 2771 preview.exec() 2772 2773 def __printPreview(self, printer): 2774 """ 2775 Private slot to generate a print preview. 2776 2777 @param printer reference to the printer object 2778 (QScintilla.Printer.Printer) 2779 """ 2780 printer.printRange(self) 2781 2782 ########################################################################### 2783 ## Task handling methods below 2784 ########################################################################### 2785 2786 def getTaskLines(self): 2787 """ 2788 Public method to get the lines containing a task. 2789 2790 @return list of lines containing a task (list of integer) 2791 """ 2792 lines = [] 2793 line = -1 2794 while True: 2795 line = self.markerFindNext(line + 1, 1 << self.taskmarker) 2796 if line < 0: 2797 break 2798 else: 2799 lines.append(line) 2800 return lines 2801 2802 def hasTaskMarkers(self): 2803 """ 2804 Public method to determine, if this editor contains any task markers. 2805 2806 @return flag indicating the presence of task markers (boolean) 2807 """ 2808 return self.__hasTaskMarkers 2809 2810 def nextTask(self): 2811 """ 2812 Public slot to handle the 'Next task' context menu action. 2813 """ 2814 line, index = self.getCursorPosition() 2815 if line == self.lines() - 1: 2816 line = 0 2817 else: 2818 line += 1 2819 taskline = self.markerFindNext(line, 1 << self.taskmarker) 2820 if taskline < 0: 2821 # wrap around 2822 taskline = self.markerFindNext(0, 1 << self.taskmarker) 2823 if taskline >= 0: 2824 self.setCursorPosition(taskline, 0) 2825 self.ensureLineVisible(taskline) 2826 2827 def previousTask(self): 2828 """ 2829 Public slot to handle the 'Previous task' context menu action. 2830 """ 2831 line, index = self.getCursorPosition() 2832 if line == 0: 2833 line = self.lines() - 1 2834 else: 2835 line -= 1 2836 taskline = self.markerFindPrevious(line, 1 << self.taskmarker) 2837 if taskline < 0: 2838 # wrap around 2839 taskline = self.markerFindPrevious( 2840 self.lines() - 1, 1 << self.taskmarker) 2841 if taskline >= 0: 2842 self.setCursorPosition(taskline, 0) 2843 self.ensureLineVisible(taskline) 2844 2845 def extractTasks(self): 2846 """ 2847 Public slot to extract all tasks. 2848 """ 2849 from Tasks.Task import Task 2850 markers = { 2851 taskType: Preferences.getTasks(markersName).split() 2852 for taskType, markersName in Task.TaskType2MarkersName.items() 2853 } 2854 txtList = self.text().split(self.getLineSeparator()) 2855 2856 # clear all task markers and tasks 2857 self.markerDeleteAll(self.taskmarker) 2858 self.taskViewer.clearFileTasks(self.fileName) 2859 self.__hasTaskMarkers = False 2860 2861 # now search tasks and record them 2862 for lineIndex, line in enumerate(txtList): 2863 shouldBreak = False 2864 2865 if line.endswith("__NO-TASK__"): 2866 # ignore potential task marker 2867 continue 2868 2869 for taskType, taskMarkers in markers.items(): 2870 for taskMarker in taskMarkers: 2871 index = line.find(taskMarker) 2872 if index > -1: 2873 task = line[index:] 2874 self.markerAdd(lineIndex, self.taskmarker) 2875 self.taskViewer.addFileTask( 2876 task, self.fileName, lineIndex + 1, taskType) 2877 self.__hasTaskMarkers = True 2878 shouldBreak = True 2879 break 2880 if shouldBreak: 2881 break 2882 self.taskMarkersUpdated.emit(self) 2883 self.__markerMap.update() 2884 2885 ########################################################################### 2886 ## Change tracing methods below 2887 ########################################################################### 2888 2889 def __createChangeMarkerPixmap(self, key, size=16, width=4): 2890 """ 2891 Private method to create a pixmap for the change markers. 2892 2893 @param key key of the color to use (string) 2894 @param size size of the pixmap (integer) 2895 @param width width of the marker line (integer) 2896 @return create pixmap (QPixmap) 2897 """ 2898 pixmap = QPixmap(size, size) 2899 pixmap.fill(Qt.GlobalColor.transparent) 2900 painter = QPainter(pixmap) 2901 painter.fillRect(size - 4, 0, 4, size, 2902 Preferences.getEditorColour(key)) 2903 painter.end() 2904 return pixmap 2905 2906 def __initOnlineChangeTrace(self): 2907 """ 2908 Private slot to initialize the online change trace. 2909 """ 2910 self.__hasChangeMarkers = False 2911 self.__oldText = self.text() 2912 self.__lastSavedText = self.text() 2913 self.__onlineChangeTraceTimer = QTimer(self) 2914 self.__onlineChangeTraceTimer.setSingleShot(True) 2915 self.__onlineChangeTraceTimer.setInterval( 2916 Preferences.getEditor("OnlineChangeTraceInterval")) 2917 self.__onlineChangeTraceTimer.timeout.connect( 2918 self.__onlineChangeTraceTimerTimeout) 2919 self.textChanged.connect(self.__resetOnlineChangeTraceTimer) 2920 2921 def __reinitOnlineChangeTrace(self): 2922 """ 2923 Private slot to re-initialize the online change trace. 2924 """ 2925 self.__oldText = self.text() 2926 self.__lastSavedText = self.text() 2927 self.__deleteAllChangeMarkers() 2928 2929 def __resetOnlineChangeTraceTimer(self): 2930 """ 2931 Private method to reset the online syntax check timer. 2932 """ 2933 if Preferences.getEditor("OnlineChangeTrace"): 2934 self.__onlineChangeTraceTimer.stop() 2935 self.__onlineChangeTraceTimer.start() 2936 2937 def __onlineChangeTraceTimerTimeout(self): 2938 """ 2939 Private slot to mark added and changed lines. 2940 """ 2941 self.__deleteAllChangeMarkers() 2942 2943 # step 1: mark saved changes 2944 oldL = self.__oldText.splitlines() 2945 newL = self.__lastSavedText.splitlines() 2946 matcher = difflib.SequenceMatcher(None, oldL, newL) 2947 2948 for token, _, _, j1, j2 in matcher.get_opcodes(): 2949 if token in ["insert", "replace"]: 2950 for lineNo in range(j1, j2): 2951 self.markerAdd(lineNo, self.__changeMarkerSaved) 2952 self.__hasChangeMarkers = True 2953 2954 # step 2: mark unsaved changes 2955 oldL = self.__lastSavedText.splitlines() 2956 newL = self.text().splitlines() 2957 matcher = difflib.SequenceMatcher(None, oldL, newL) 2958 2959 for token, _, _, j1, j2 in matcher.get_opcodes(): 2960 if token in ["insert", "replace"]: 2961 for lineNo in range(j1, j2): 2962 self.markerAdd(lineNo, self.__changeMarkerUnsaved) 2963 self.__hasChangeMarkers = True 2964 2965 if self.__hasChangeMarkers: 2966 self.changeMarkersUpdated.emit(self) 2967 self.__markerMap.update() 2968 2969 def __resetOnlineChangeTraceInfo(self): 2970 """ 2971 Private slot to reset the online change trace info. 2972 """ 2973 self.__lastSavedText = self.text() 2974 self.__deleteAllChangeMarkers() 2975 2976 # mark saved changes 2977 oldL = self.__oldText.splitlines() 2978 newL = self.__lastSavedText.splitlines() 2979 matcher = difflib.SequenceMatcher(None, oldL, newL) 2980 2981 for token, _, _, j1, j2 in matcher.get_opcodes(): 2982 if token in ["insert", "replace"]: 2983 for lineNo in range(j1, j2): 2984 self.markerAdd(lineNo, self.__changeMarkerSaved) 2985 self.__hasChangeMarkers = True 2986 2987 if self.__hasChangeMarkers: 2988 self.changeMarkersUpdated.emit(self) 2989 self.__markerMap.update() 2990 2991 def __deleteAllChangeMarkers(self): 2992 """ 2993 Private slot to delete all change markers. 2994 """ 2995 self.markerDeleteAll(self.__changeMarkerUnsaved) 2996 self.markerDeleteAll(self.__changeMarkerSaved) 2997 self.__hasChangeMarkers = False 2998 self.changeMarkersUpdated.emit(self) 2999 self.__markerMap.update() 3000 3001 def getChangeLines(self): 3002 """ 3003 Public method to get the lines containing a change. 3004 3005 @return list of lines containing a change (list of integer) 3006 """ 3007 lines = [] 3008 line = -1 3009 while True: 3010 line = self.markerFindNext(line + 1, self.changeMarkersMask) 3011 if line < 0: 3012 break 3013 else: 3014 lines.append(line) 3015 return lines 3016 3017 def hasChangeMarkers(self): 3018 """ 3019 Public method to determine, if this editor contains any change markers. 3020 3021 @return flag indicating the presence of change markers (boolean) 3022 """ 3023 return self.__hasChangeMarkers 3024 3025 def nextChange(self): 3026 """ 3027 Public slot to handle the 'Next change' context menu action. 3028 """ 3029 line, index = self.getCursorPosition() 3030 if line == self.lines() - 1: 3031 line = 0 3032 else: 3033 line += 1 3034 changeline = self.markerFindNext(line, self.changeMarkersMask) 3035 if changeline < 0: 3036 # wrap around 3037 changeline = self.markerFindNext(0, self.changeMarkersMask) 3038 if changeline >= 0: 3039 self.setCursorPosition(changeline, 0) 3040 self.ensureLineVisible(changeline) 3041 3042 def previousChange(self): 3043 """ 3044 Public slot to handle the 'Previous change' context menu action. 3045 """ 3046 line, index = self.getCursorPosition() 3047 if line == 0: 3048 line = self.lines() - 1 3049 else: 3050 line -= 1 3051 changeline = self.markerFindPrevious(line, self.changeMarkersMask) 3052 if changeline < 0: 3053 # wrap around 3054 changeline = self.markerFindPrevious( 3055 self.lines() - 1, self.changeMarkersMask) 3056 if changeline >= 0: 3057 self.setCursorPosition(changeline, 0) 3058 self.ensureLineVisible(changeline) 3059 3060 ########################################################################### 3061 ## Flags handling methods below 3062 ########################################################################### 3063 3064 def __processFlags(self): 3065 """ 3066 Private method to extract flags and process them. 3067 3068 @return list of change flags (list of string) 3069 """ 3070 txt = self.text() 3071 flags = Utilities.extractFlags(txt) 3072 3073 changedFlags = [] 3074 3075 # Flag 1: FileType 3076 if "FileType" in flags: 3077 oldFiletype = self.filetype 3078 if isinstance(flags["FileType"], str): 3079 self.filetype = flags["FileType"] 3080 self.filetypeByFlag = True 3081 if oldFiletype != self.filetype: 3082 changedFlags.append("FileType") 3083 else: 3084 if self.filetype != "" and self.filetypeByFlag: 3085 self.filetype = "" 3086 self.filetypeByFlag = False 3087 self.__bindName(txt.splitlines()[0]) 3088 changedFlags.append("FileType") 3089 3090 return changedFlags 3091 3092 ########################################################################### 3093 ## File handling methods below 3094 ########################################################################### 3095 3096 def checkDirty(self): 3097 """ 3098 Public method to check dirty status and open a message window. 3099 3100 @return flag indicating successful reset of the dirty flag (boolean) 3101 """ 3102 if self.isModified(): 3103 fn = self.fileName 3104 if fn is None: 3105 fn = self.noName 3106 res = E5MessageBox.okToClearData( 3107 self, 3108 self.tr("File Modified"), 3109 self.tr("<p>The file <b>{0}</b> has unsaved changes.</p>") 3110 .format(fn), 3111 self.saveFile) 3112 if res: 3113 self.vm.setEditorName(self, self.fileName) 3114 return res 3115 3116 return True 3117 3118 def revertToUnmodified(self): 3119 """ 3120 Public method to revert back to the last saved state. 3121 """ 3122 undo_ = True 3123 while self.isModified(): 3124 if undo_: 3125 # try undo first 3126 if self.isUndoAvailable(): 3127 self.undo() 3128 else: 3129 undo_ = False 3130 else: 3131 # try redo next 3132 if self.isRedoAvailable(): 3133 self.redo() 3134 else: 3135 break 3136 # Couldn't find the unmodified state 3137 3138 def readFile(self, fn, createIt=False, encoding=""): 3139 """ 3140 Public slot to read the text from a file. 3141 3142 @param fn filename to read from (string) 3143 @param createIt flag indicating the creation of a new file, if the 3144 given one doesn't exist (boolean) 3145 @param encoding encoding to be used to read the file (string) 3146 (Note: this parameter overrides encoding detection) 3147 """ 3148 self.__loadEditorConfig(fileName=fn) 3149 3150 try: 3151 with E5OverrideCursor(): 3152 if createIt and not os.path.exists(fn): 3153 with open(fn, "w"): 3154 pass 3155 if encoding == "": 3156 encoding = self.__getEditorConfig("DefaultEncoding", 3157 nodefault=True) 3158 if encoding: 3159 txt, self.encoding = Utilities.readEncodedFileWithEncoding( 3160 fn, encoding) 3161 else: 3162 txt, self.encoding = Utilities.readEncodedFile(fn) 3163 except (UnicodeDecodeError, OSError) as why: 3164 E5MessageBox.critical( 3165 self.vm, 3166 self.tr('Open File'), 3167 self.tr('<p>The file <b>{0}</b> could not be opened.</p>' 3168 '<p>Reason: {1}</p>') 3169 .format(fn, str(why))) 3170 raise 3171 3172 with E5OverrideCursor(): 3173 modified = False 3174 3175 self.setText(txt) 3176 3177 # get eric specific flags 3178 self.__processFlags() 3179 3180 # perform automatic EOL conversion 3181 if ( 3182 self.__getEditorConfig("EOLMode", nodefault=True) or 3183 Preferences.getEditor("AutomaticEOLConversion") 3184 ): 3185 self.convertEols(self.eolMode()) 3186 else: 3187 fileEol = self.detectEolString(txt) 3188 self.setEolModeByEolString(fileEol) 3189 3190 self.extractTasks() 3191 3192 self.setModified(modified) 3193 self.lastModified = QFileInfo(self.fileName).lastModified() 3194 3195 def __convertTabs(self): 3196 """ 3197 Private slot to convert tabulators to spaces. 3198 """ 3199 if ( 3200 (not self.__getEditorConfig("TabForIndentation")) and 3201 Preferences.getEditor("ConvertTabsOnLoad") and 3202 not (self.lexer_ and 3203 self.lexer_.alwaysKeepTabs()) 3204 ): 3205 txt = self.text() 3206 txtExpanded = txt.expandtabs(self.__getEditorConfig("TabWidth")) 3207 if txtExpanded != txt: 3208 self.beginUndoAction() 3209 self.setText(txt) 3210 self.endUndoAction() 3211 3212 self.setModified(True) 3213 3214 def __removeTrailingWhitespace(self): 3215 """ 3216 Private method to remove trailing whitespace. 3217 """ 3218 searchRE = r"[ \t]+$" # whitespace at the end of a line 3219 3220 ok = self.findFirstTarget(searchRE, True, False, False, 0, 0) 3221 self.beginUndoAction() 3222 while ok: 3223 self.replaceTarget("") 3224 ok = self.findNextTarget() 3225 self.endUndoAction() 3226 3227 def writeFile(self, fn, backup=True): 3228 """ 3229 Public slot to write the text to a file. 3230 3231 @param fn filename to write to (string) 3232 @param backup flag indicating to save a backup (boolean) 3233 @return flag indicating success (boolean) 3234 """ 3235 config = self.__loadEditorConfigObject(fn) 3236 3237 eol = self.__getEditorConfig("EOLMode", nodefault=True, config=config) 3238 if eol is not None: 3239 self.convertEols(eol) 3240 3241 if self.__getEditorConfig("StripTrailingWhitespace", config=config): 3242 self.__removeTrailingWhitespace() 3243 3244 txt = self.text() 3245 3246 if self.__getEditorConfig("InsertFinalNewline", config=config): 3247 eol = self.getLineSeparator() 3248 if eol: 3249 if len(txt) >= len(eol): 3250 if txt[-len(eol):] != eol: 3251 txt += eol 3252 else: 3253 txt += eol 3254 3255 # create a backup file, if the option is set 3256 createBackup = backup and Preferences.getEditor("CreateBackupFile") 3257 if createBackup: 3258 if os.path.islink(fn): 3259 fn = os.path.realpath(fn) 3260 bfn = '{0}~'.format(fn) 3261 try: 3262 permissions = os.stat(fn).st_mode 3263 perms_valid = True 3264 except OSError: 3265 # if there was an error, ignore it 3266 perms_valid = False 3267 with contextlib.suppress(OSError): 3268 os.remove(bfn) 3269 with contextlib.suppress(OSError): 3270 os.rename(fn, bfn) 3271 3272 # now write text to the file fn 3273 try: 3274 editorConfigEncoding = self.__getEditorConfig( 3275 "DefaultEncoding", nodefault=True, config=config) 3276 self.encoding = Utilities.writeEncodedFile( 3277 fn, txt, self.encoding, forcedEncoding=editorConfigEncoding) 3278 if createBackup and perms_valid: 3279 os.chmod(fn, permissions) 3280 return True 3281 except (OSError, Utilities.CodingError, UnicodeError) as why: 3282 E5MessageBox.critical( 3283 self, 3284 self.tr('Save File'), 3285 self.tr('<p>The file <b>{0}</b> could not be saved.<br/>' 3286 'Reason: {1}</p>') 3287 .format(fn, str(why))) 3288 return False 3289 3290 def __getSaveFileName(self, path=None): 3291 """ 3292 Private method to get the name of the file to be saved. 3293 3294 @param path directory to save the file in (string) 3295 @return file name (string) 3296 """ 3297 # save to project, if a project is loaded 3298 if self.project.isOpen(): 3299 if ( 3300 self.fileName and 3301 self.project.startswithProjectPath(self.fileName) 3302 ): 3303 path = os.path.dirname(self.fileName) 3304 elif not self.fileName: 3305 path = self.project.getProjectPath() 3306 3307 if not path and self.fileName: 3308 path = os.path.dirname(self.fileName) 3309 if not path: 3310 path = ( 3311 Preferences.getMultiProject("Workspace") or 3312 Utilities.getHomeDir() 3313 ) 3314 3315 from . import Lexers 3316 if self.fileName: 3317 filterPattern = "(*{0})".format( 3318 os.path.splitext(self.fileName)[1]) 3319 for fileFilter in Lexers.getSaveFileFiltersList(True): 3320 if filterPattern in fileFilter: 3321 defaultFilter = fileFilter 3322 break 3323 else: 3324 defaultFilter = Preferences.getEditor("DefaultSaveFilter") 3325 else: 3326 defaultFilter = Preferences.getEditor("DefaultSaveFilter") 3327 fn, selectedFilter = E5FileDialog.getSaveFileNameAndFilter( 3328 self, 3329 self.tr("Save File"), 3330 path, 3331 Lexers.getSaveFileFiltersList(True, True), 3332 defaultFilter, 3333 E5FileDialog.Options(E5FileDialog.DontConfirmOverwrite)) 3334 3335 if fn: 3336 if fn.endswith("."): 3337 fn = fn[:-1] 3338 3339 ext = QFileInfo(fn).suffix() 3340 if not ext: 3341 ex = selectedFilter.split("(*")[1].split(")")[0] 3342 if ex: 3343 fn += ex 3344 if QFileInfo(fn).exists(): 3345 res = E5MessageBox.yesNo( 3346 self, 3347 self.tr("Save File"), 3348 self.tr("<p>The file <b>{0}</b> already exists." 3349 " Overwrite it?</p>").format(fn), 3350 icon=E5MessageBox.Warning) 3351 if not res: 3352 return "" 3353 fn = Utilities.toNativeSeparators(fn) 3354 3355 return fn 3356 3357 def saveFileCopy(self, path=None): 3358 """ 3359 Public method to save a copy of the file. 3360 3361 @param path directory to save the file in (string) 3362 @return flag indicating success (boolean) 3363 """ 3364 fn = self.__getSaveFileName(path) 3365 if not fn: 3366 return False 3367 3368 res = self.writeFile(fn) 3369 if ( 3370 res and 3371 self.project.isOpen() and 3372 self.project.startswithProjectPath(fn) 3373 ): 3374 # save to project, if a project is loaded 3375 self.project.appendFile(fn) 3376 3377 return res 3378 3379 def saveFile(self, saveas=False, path=None): 3380 """ 3381 Public method to save the text to a file. 3382 3383 @param saveas flag indicating a 'save as' action (boolean) 3384 @param path directory to save the file in (string) 3385 @return flag indicating success (boolean) 3386 """ 3387 if not saveas and not self.isModified(): 3388 return False # do nothing if text wasn't changed 3389 3390 newName = None 3391 if saveas or self.fileName == "": 3392 saveas = True 3393 3394 fn = self.__getSaveFileName(path) 3395 if not fn: 3396 return False 3397 3398 newName = fn 3399 3400 # save to project, if a project is loaded 3401 if ( 3402 self.project.isOpen() and 3403 self.project.startswithProjectPath(fn) 3404 ): 3405 editorConfigEol = self.__getEditorConfig( 3406 "EOLMode", nodefault=True, 3407 config=self.__loadEditorConfigObject(fn)) 3408 if editorConfigEol is not None: 3409 self.setEolMode(editorConfigEol) 3410 else: 3411 self.setEolModeByEolString(self.project.getEolString()) 3412 self.convertEols(self.eolMode()) 3413 else: 3414 fn = self.fileName 3415 3416 self.__loadEditorConfig(fn) 3417 self.editorAboutToBeSaved.emit(self.fileName) 3418 if self.writeFile(fn): 3419 if saveas: 3420 self.__clearBreakpoints(self.fileName) 3421 self.__setFileName(fn) 3422 self.setModified(False) 3423 self.setReadOnly(False) 3424 self.setWindowTitle(self.fileName) 3425 # get eric specific flags 3426 changedFlags = self.__processFlags() 3427 if not self.__lexerReset and "FileType" in changedFlags: 3428 self.setLanguage(self.fileName) 3429 3430 if saveas: 3431 self.isResourcesFile = self.fileName.endswith(".qrc") 3432 self.__initContextMenu() 3433 self.editorRenamed.emit(self.fileName) 3434 3435 # save to project, if a project is loaded 3436 if ( 3437 self.project.isOpen() and 3438 self.project.startswithProjectPath(fn) 3439 ): 3440 self.project.appendFile(fn) 3441 self.addedToProject() 3442 3443 self.setLanguage(self.fileName) 3444 3445 self.lastModified = QFileInfo(self.fileName).lastModified() 3446 if newName is not None: 3447 self.vm.addToRecentList(newName) 3448 self.editorSaved.emit(self.fileName) 3449 self.checkSyntax() 3450 self.extractTasks() 3451 self.__resetOnlineChangeTraceInfo() 3452 self.__checkEncoding() 3453 return True 3454 else: 3455 self.lastModified = QFileInfo(fn).lastModified() 3456 return False 3457 3458 def saveFileAs(self, path=None, toProject=False): 3459 """ 3460 Public slot to save a file with a new name. 3461 3462 @param path directory to save the file in (string) 3463 @param toProject flag indicating a save to project operation 3464 (boolean) 3465 @return tuple of two values (boolean, string) giving a success 3466 indicator and the name of the saved file 3467 """ 3468 return self.saveFile(True, path) 3469 3470 def handleRenamed(self, fn): 3471 """ 3472 Public slot to handle the editorRenamed signal. 3473 3474 @param fn filename to be set for the editor (string). 3475 """ 3476 self.__clearBreakpoints(fn) 3477 3478 self.__setFileName(fn) 3479 self.setWindowTitle(self.fileName) 3480 3481 self.__loadEditorConfig() 3482 3483 if self.lexer_ is None: 3484 self.setLanguage(self.fileName) 3485 3486 self.lastModified = QFileInfo(self.fileName).lastModified() 3487 self.vm.setEditorName(self, self.fileName) 3488 self.__updateReadOnly(True) 3489 3490 def fileRenamed(self, fn): 3491 """ 3492 Public slot to handle the editorRenamed signal. 3493 3494 @param fn filename to be set for the editor (string). 3495 """ 3496 self.handleRenamed(fn) 3497 if not self.inFileRenamed: 3498 self.inFileRenamed = True 3499 self.editorRenamed.emit(self.fileName) 3500 self.inFileRenamed = False 3501 3502 ########################################################################### 3503 ## Utility methods below 3504 ########################################################################### 3505 3506 def ensureVisible(self, line, expand=False): 3507 """ 3508 Public slot to ensure, that the specified line is visible. 3509 3510 @param line line number to make visible 3511 @type int 3512 @param expand flag indicating to expand all folds 3513 @type bool 3514 """ 3515 self.ensureLineVisible(line - 1) 3516 if expand: 3517 self.SendScintilla(QsciScintilla.SCI_FOLDCHILDREN, line - 1, 3518 QsciScintilla.SC_FOLDACTION_EXPAND) 3519 3520 def ensureVisibleTop(self, line, expand=False): 3521 """ 3522 Public slot to ensure, that the specified line is visible at the top 3523 of the editor. 3524 3525 @param line line number to make visible 3526 @type int 3527 @param expand flag indicating to expand all folds 3528 @type bool 3529 """ 3530 self.ensureVisible(line) 3531 self.setFirstVisibleLine(line - 1) 3532 self.ensureCursorVisible() 3533 if expand: 3534 self.SendScintilla(QsciScintilla.SCI_FOLDCHILDREN, line - 1, 3535 QsciScintilla.SC_FOLDACTION_EXPAND) 3536 3537 def __marginClicked(self, margin, line, modifiers): 3538 """ 3539 Private slot to handle the marginClicked signal. 3540 3541 @param margin id of the clicked margin (integer) 3542 @param line line number of the click (integer) 3543 @param modifiers keyboard modifiers (Qt.KeyboardModifiers) 3544 """ 3545 if margin == self.__bmMargin: 3546 self.toggleBookmark(line + 1) 3547 elif margin == self.__bpMargin: 3548 self.__toggleBreakpoint(line + 1) 3549 elif margin == self.__indicMargin: 3550 if self.markersAtLine(line) & (1 << self.syntaxerror): 3551 self.__showSyntaxError(line) 3552 elif self.markersAtLine(line) & (1 << self.warning): 3553 self.__showWarning(line) 3554 3555 def handleMonospacedEnable(self): 3556 """ 3557 Public slot to handle the Use Monospaced Font context menu entry. 3558 """ 3559 if self.menuActs["MonospacedFont"].isChecked(): 3560 if not self.lexer_: 3561 self.setMonospaced(True) 3562 else: 3563 if self.lexer_: 3564 self.lexer_.readSettings( 3565 Preferences.Prefs.settings, "Scintilla") 3566 if self.lexer_.hasSubstyles(): 3567 self.lexer_.readSubstyles(self) 3568 self.lexer_.initProperties() 3569 self.setMonospaced(False) 3570 self.__setMarginsDisplay() 3571 3572 def getWordBoundaries(self, line, index, useWordChars=True): 3573 """ 3574 Public method to get the word boundaries at a position. 3575 3576 @param line number of line to look at (int) 3577 @param index position to look at (int) 3578 @param useWordChars flag indicating to use the wordCharacters 3579 method (boolean) 3580 @return tuple with start and end indexes of the word at the position 3581 (integer, integer) 3582 """ 3583 wc = self.wordCharacters() 3584 if wc is None or not useWordChars: 3585 pattern = r"\b[\w_]+\b" 3586 else: 3587 wc = re.sub(r'\w', "", wc) 3588 pattern = r"\b[\w{0}]+\b".format(re.escape(wc)) 3589 rx = ( 3590 re.compile(pattern) 3591 if self.caseSensitive() else 3592 re.compile(pattern, re.IGNORECASE) 3593 ) 3594 3595 text = self.text(line) 3596 for match in rx.finditer(text): 3597 start, end = match.span() 3598 if start <= index <= end: 3599 return (start, end) 3600 3601 return (index, index) 3602 3603 def getWord(self, line, index, direction=0, useWordChars=True): 3604 """ 3605 Public method to get the word at a position. 3606 3607 @param line number of line to look at (int) 3608 @param index position to look at (int) 3609 @param direction direction to look in (0 = whole word, 1 = left, 3610 2 = right) 3611 @param useWordChars flag indicating to use the wordCharacters 3612 method (boolean) 3613 @return the word at that position (string) 3614 """ 3615 start, end = self.getWordBoundaries(line, index, useWordChars) 3616 if direction == 1: 3617 end = index 3618 elif direction == 2: 3619 start = index 3620 if end > start: 3621 text = self.text(line) 3622 word = text[start:end] 3623 else: 3624 word = '' 3625 return word 3626 3627 def getWordLeft(self, line, index): 3628 """ 3629 Public method to get the word to the left of a position. 3630 3631 @param line number of line to look at (int) 3632 @param index position to look at (int) 3633 @return the word to the left of that position (string) 3634 """ 3635 return self.getWord(line, index, 1) 3636 3637 def getWordRight(self, line, index): 3638 """ 3639 Public method to get the word to the right of a position. 3640 3641 @param line number of line to look at (int) 3642 @param index position to look at (int) 3643 @return the word to the right of that position (string) 3644 """ 3645 return self.getWord(line, index, 2) 3646 3647 def getCurrentWord(self): 3648 """ 3649 Public method to get the word at the current position. 3650 3651 @return the word at that current position (string) 3652 """ 3653 line, index = self.getCursorPosition() 3654 return self.getWord(line, index) 3655 3656 def getCurrentWordBoundaries(self): 3657 """ 3658 Public method to get the word boundaries at the current position. 3659 3660 @return tuple with start and end indexes of the current word 3661 (integer, integer) 3662 """ 3663 line, index = self.getCursorPosition() 3664 return self.getWordBoundaries(line, index) 3665 3666 def selectWord(self, line, index): 3667 """ 3668 Public method to select the word at a position. 3669 3670 @param line number of line to look at (int) 3671 @param index position to look at (int) 3672 """ 3673 start, end = self.getWordBoundaries(line, index) 3674 self.setSelection(line, start, line, end) 3675 3676 def selectCurrentWord(self): 3677 """ 3678 Public method to select the current word. 3679 """ 3680 line, index = self.getCursorPosition() 3681 self.selectWord(line, index) 3682 3683 def __getCharacter(self, pos): 3684 """ 3685 Private method to get the character to the left of the current position 3686 in the current line. 3687 3688 @param pos position to get character at (integer) 3689 @return requested character or "", if there are no more (string) and 3690 the next position (i.e. pos - 1) 3691 """ 3692 if pos <= 0: 3693 return "", pos 3694 3695 pos = self.positionBefore(pos) 3696 ch = self.charAt(pos) 3697 3698 # Don't go past the end of the previous line 3699 if ch in ('\n', '\r'): 3700 return "", pos 3701 3702 return ch, pos 3703 3704 def getSearchText(self, selectionOnly=False): 3705 """ 3706 Public method to determine the selection or the current word for the 3707 next search operation. 3708 3709 @param selectionOnly flag indicating that only selected text should be 3710 returned (boolean) 3711 @return selection or current word (string) 3712 """ 3713 if self.hasSelectedText(): 3714 text = self.selectedText() 3715 if '\r' in text or '\n' in text: 3716 # the selection contains at least a newline, it is 3717 # unlikely to be the expression to search for 3718 return '' 3719 3720 return text 3721 3722 if not selectionOnly: 3723 # no selected text, determine the word at the current position 3724 return self.getCurrentWord() 3725 3726 return '' 3727 3728 def setSearchIndicator(self, startPos, indicLength): 3729 """ 3730 Public method to set a search indicator for the given range. 3731 3732 @param startPos start position of the indicator (integer) 3733 @param indicLength length of the indicator (integer) 3734 """ 3735 self.setIndicatorRange(self.searchIndicator, startPos, indicLength) 3736 line = self.lineIndexFromPosition(startPos)[0] 3737 if line not in self.__searchIndicatorLines: 3738 self.__searchIndicatorLines.append(line) 3739 3740 def clearSearchIndicators(self): 3741 """ 3742 Public method to clear all search indicators. 3743 """ 3744 self.clearAllIndicators(self.searchIndicator) 3745 self.__markedText = "" 3746 self.__searchIndicatorLines = [] 3747 self.__markerMap.update() 3748 3749 def __markOccurrences(self): 3750 """ 3751 Private method to mark all occurrences of the current word. 3752 """ 3753 word = self.getCurrentWord() 3754 if not word: 3755 self.clearSearchIndicators() 3756 return 3757 3758 if self.__markedText == word: 3759 return 3760 3761 self.clearSearchIndicators() 3762 ok = self.findFirstTarget(word, False, self.caseSensitive(), True, 3763 0, 0) 3764 while ok: 3765 tgtPos, tgtLen = self.getFoundTarget() 3766 self.setSearchIndicator(tgtPos, tgtLen) 3767 ok = self.findNextTarget() 3768 self.__markedText = word 3769 self.__markerMap.update() 3770 3771 def getSearchIndicatorLines(self): 3772 """ 3773 Public method to get the lines containing a search indicator. 3774 3775 @return list of lines containing a search indicator (list of integer) 3776 """ 3777 return self.__searchIndicatorLines[:] 3778 3779 def updateMarkerMap(self): 3780 """ 3781 Public method to initiate an update of the marker map. 3782 """ 3783 self.__markerMap.update() 3784 3785 ########################################################################### 3786 ## Highlighting marker handling methods below 3787 ########################################################################### 3788 3789 def setHighlight(self, startLine, startIndex, endLine, endIndex): 3790 """ 3791 Public method to set a text highlight. 3792 3793 @param startLine line of the highlight start 3794 @type int 3795 @param startIndex index of the highlight start 3796 @type int 3797 @param endLine line of the highlight end 3798 @type int 3799 @param endIndex index of the highlight end 3800 @type int 3801 """ 3802 self.setIndicator(self.highlightIndicator, startLine, startIndex, 3803 endLine, endIndex) 3804 3805 def clearAllHighlights(self): 3806 """ 3807 Public method to clear all highlights. 3808 """ 3809 self.clearAllIndicators(self.highlightIndicator) 3810 3811 def clearHighlight(self, startLine, startIndex, endLine, endIndex): 3812 """ 3813 Public method to clear a text highlight. 3814 3815 @param startLine line of the highlight start 3816 @type int 3817 @param startIndex index of the highlight start 3818 @type int 3819 @param endLine line of the highlight end 3820 @type int 3821 @param endIndex index of the highlight end 3822 @type int 3823 """ 3824 self.clearIndicator(self.highlightIndicator, startLine, startIndex, 3825 endLine, endIndex) 3826 3827 ########################################################################### 3828 ## Comment handling methods below 3829 ########################################################################### 3830 3831 def __isCommentedLine(self, line, commentStr): 3832 """ 3833 Private method to check, if the given line is a comment line as 3834 produced by the configured comment rules. 3835 3836 @param line text of the line to check (string) 3837 @param commentStr comment string to check against (string) 3838 @return flag indicating a commented line (boolean) 3839 """ 3840 if Preferences.getEditor("CommentColumn0"): 3841 return line.startswith(commentStr) 3842 else: 3843 return line.strip().startswith(commentStr) 3844 3845 def toggleCommentBlock(self): 3846 """ 3847 Public slot to toggle the comment of a block. 3848 3849 If the line of the cursor or the selection is not commented, it will 3850 be commented. If it is commented, the comment block will be removed. 3851 The later works independent of the current selection. 3852 """ 3853 if self.lexer_ is None or not self.lexer_.canBlockComment(): 3854 return 3855 3856 commentStr = self.lexer_.commentStr() 3857 line, index = self.getCursorPosition() 3858 3859 # check if line starts with our comment string (i.e. was commented 3860 # by our comment...() slots 3861 if ( 3862 self.hasSelectedText() and 3863 self.__isCommentedLine(self.text(self.getSelection()[0]), 3864 commentStr) 3865 ): 3866 self.uncommentLineOrSelection() 3867 elif not self.__isCommentedLine(self.text(line), commentStr): 3868 # it doesn't, so comment the line or selection 3869 self.commentLineOrSelection() 3870 else: 3871 # determine the start of the comment block 3872 begline = line 3873 while ( 3874 begline > 0 and 3875 self.__isCommentedLine(self.text(begline - 1), commentStr) 3876 ): 3877 begline -= 1 3878 # determine the end of the comment block 3879 endline = line 3880 lines = self.lines() 3881 while ( 3882 endline < lines and 3883 self.__isCommentedLine(self.text(endline + 1), commentStr) 3884 ): 3885 endline += 1 3886 3887 self.setSelection(begline, 0, endline, self.lineLength(endline)) 3888 self.uncommentLineOrSelection() 3889 3890 # reset the cursor 3891 self.setCursorPosition(line, index - len(commentStr)) 3892 3893 def commentLine(self): 3894 """ 3895 Public slot to comment the current line. 3896 """ 3897 if self.lexer_ is None or not self.lexer_.canBlockComment(): 3898 return 3899 3900 line, index = self.getCursorPosition() 3901 self.beginUndoAction() 3902 if Preferences.getEditor("CommentColumn0"): 3903 self.insertAt(self.lexer_.commentStr(), line, 0) 3904 else: 3905 lineText = self.text(line) 3906 pos = len(lineText.replace(lineText.lstrip(" \t"), "")) 3907 self.insertAt(self.lexer_.commentStr(), line, pos) 3908 self.endUndoAction() 3909 3910 def uncommentLine(self): 3911 """ 3912 Public slot to uncomment the current line. 3913 """ 3914 if self.lexer_ is None or not self.lexer_.canBlockComment(): 3915 return 3916 3917 commentStr = self.lexer_.commentStr() 3918 line, index = self.getCursorPosition() 3919 3920 # check if line starts with our comment string (i.e. was commented 3921 # by our comment...() slots 3922 if not self.__isCommentedLine(self.text(line), commentStr): 3923 return 3924 3925 # now remove the comment string 3926 self.beginUndoAction() 3927 if Preferences.getEditor("CommentColumn0"): 3928 self.setSelection(line, 0, line, len(commentStr)) 3929 else: 3930 lineText = self.text(line) 3931 pos = len(lineText.replace(lineText.lstrip(" \t"), "")) 3932 self.setSelection(line, pos, line, pos + len(commentStr)) 3933 self.removeSelectedText() 3934 self.endUndoAction() 3935 3936 def commentSelection(self): 3937 """ 3938 Public slot to comment the current selection. 3939 """ 3940 if self.lexer_ is None or not self.lexer_.canBlockComment(): 3941 return 3942 3943 if not self.hasSelectedText(): 3944 return 3945 3946 commentStr = self.lexer_.commentStr() 3947 3948 # get the selection boundaries 3949 lineFrom, indexFrom, lineTo, indexTo = self.getSelection() 3950 endLine = lineTo if indexTo else lineTo - 1 3951 3952 self.beginUndoAction() 3953 # iterate over the lines 3954 for line in range(lineFrom, endLine + 1): 3955 if Preferences.getEditor("CommentColumn0"): 3956 self.insertAt(commentStr, line, 0) 3957 else: 3958 lineText = self.text(line) 3959 pos = len(lineText.replace(lineText.lstrip(" \t"), "")) 3960 self.insertAt(commentStr, line, pos) 3961 3962 # change the selection accordingly 3963 self.setSelection(lineFrom, 0, endLine + 1, 0) 3964 self.endUndoAction() 3965 3966 def uncommentSelection(self): 3967 """ 3968 Public slot to uncomment the current selection. 3969 """ 3970 if self.lexer_ is None or not self.lexer_.canBlockComment(): 3971 return 3972 3973 if not self.hasSelectedText(): 3974 return 3975 3976 commentStr = self.lexer_.commentStr() 3977 3978 # get the selection boundaries 3979 lineFrom, indexFrom, lineTo, indexTo = self.getSelection() 3980 endLine = lineTo if indexTo else lineTo - 1 3981 3982 self.beginUndoAction() 3983 # iterate over the lines 3984 for line in range(lineFrom, endLine + 1): 3985 # check if line starts with our comment string (i.e. was commented 3986 # by our comment...() slots 3987 if not self.__isCommentedLine(self.text(line), commentStr): 3988 continue 3989 3990 if Preferences.getEditor("CommentColumn0"): 3991 self.setSelection(line, 0, line, len(commentStr)) 3992 else: 3993 lineText = self.text(line) 3994 pos = len(lineText.replace(lineText.lstrip(" \t"), "")) 3995 self.setSelection(line, pos, line, pos + len(commentStr)) 3996 self.removeSelectedText() 3997 3998 # adjust selection start 3999 if line == lineFrom: 4000 indexFrom -= len(commentStr) 4001 if indexFrom < 0: 4002 indexFrom = 0 4003 4004 # adjust selection end 4005 if line == lineTo: 4006 indexTo -= len(commentStr) 4007 if indexTo < 0: 4008 indexTo = 0 4009 4010 # change the selection accordingly 4011 self.setSelection(lineFrom, indexFrom, lineTo, indexTo) 4012 self.endUndoAction() 4013 4014 def commentLineOrSelection(self): 4015 """ 4016 Public slot to comment the current line or current selection. 4017 """ 4018 if self.hasSelectedText(): 4019 self.commentSelection() 4020 else: 4021 self.commentLine() 4022 4023 def uncommentLineOrSelection(self): 4024 """ 4025 Public slot to uncomment the current line or current selection. 4026 """ 4027 if self.hasSelectedText(): 4028 self.uncommentSelection() 4029 else: 4030 self.uncommentLine() 4031 4032 def streamCommentLine(self): 4033 """ 4034 Public slot to stream comment the current line. 4035 """ 4036 if self.lexer_ is None or not self.lexer_.canStreamComment(): 4037 return 4038 4039 commentStr = self.lexer_.streamCommentStr() 4040 line, index = self.getCursorPosition() 4041 4042 self.beginUndoAction() 4043 self.insertAt(commentStr['end'], line, self.lineLength(line)) 4044 self.insertAt(commentStr['start'], line, 0) 4045 self.endUndoAction() 4046 4047 def streamCommentSelection(self): 4048 """ 4049 Public slot to comment the current selection. 4050 """ 4051 if self.lexer_ is None or not self.lexer_.canStreamComment(): 4052 return 4053 4054 if not self.hasSelectedText(): 4055 return 4056 4057 commentStr = self.lexer_.streamCommentStr() 4058 4059 # get the selection boundaries 4060 lineFrom, indexFrom, lineTo, indexTo = self.getSelection() 4061 if indexTo == 0: 4062 endLine = lineTo - 1 4063 endIndex = self.lineLength(endLine) 4064 else: 4065 endLine = lineTo 4066 endIndex = indexTo 4067 4068 self.beginUndoAction() 4069 self.insertAt(commentStr['end'], endLine, endIndex) 4070 self.insertAt(commentStr['start'], lineFrom, indexFrom) 4071 4072 # change the selection accordingly 4073 if indexTo > 0: 4074 indexTo += len(commentStr['end']) 4075 if lineFrom == endLine: 4076 indexTo += len(commentStr['start']) 4077 self.setSelection(lineFrom, indexFrom, lineTo, indexTo) 4078 self.endUndoAction() 4079 4080 def streamCommentLineOrSelection(self): 4081 """ 4082 Public slot to stream comment the current line or current selection. 4083 """ 4084 if self.hasSelectedText(): 4085 self.streamCommentSelection() 4086 else: 4087 self.streamCommentLine() 4088 4089 def boxCommentLine(self): 4090 """ 4091 Public slot to box comment the current line. 4092 """ 4093 if self.lexer_ is None or not self.lexer_.canBoxComment(): 4094 return 4095 4096 commentStr = self.lexer_.boxCommentStr() 4097 line, index = self.getCursorPosition() 4098 4099 eol = self.getLineSeparator() 4100 self.beginUndoAction() 4101 self.insertAt(eol, line, self.lineLength(line)) 4102 self.insertAt(commentStr['end'], line + 1, 0) 4103 self.insertAt(commentStr['middle'], line, 0) 4104 self.insertAt(eol, line, 0) 4105 self.insertAt(commentStr['start'], line, 0) 4106 self.endUndoAction() 4107 4108 def boxCommentSelection(self): 4109 """ 4110 Public slot to box comment the current selection. 4111 """ 4112 if self.lexer_ is None or not self.lexer_.canBoxComment(): 4113 return 4114 4115 if not self.hasSelectedText(): 4116 return 4117 4118 commentStr = self.lexer_.boxCommentStr() 4119 4120 # get the selection boundaries 4121 lineFrom, indexFrom, lineTo, indexTo = self.getSelection() 4122 endLine = lineTo if indexTo else lineTo - 1 4123 4124 self.beginUndoAction() 4125 # iterate over the lines 4126 for line in range(lineFrom, endLine + 1): 4127 self.insertAt(commentStr['middle'], line, 0) 4128 4129 # now do the comments before and after the selection 4130 eol = self.getLineSeparator() 4131 self.insertAt(eol, endLine, self.lineLength(endLine)) 4132 self.insertAt(commentStr['end'], endLine + 1, 0) 4133 self.insertAt(eol, lineFrom, 0) 4134 self.insertAt(commentStr['start'], lineFrom, 0) 4135 4136 # change the selection accordingly 4137 self.setSelection(lineFrom, 0, endLine + 3, 0) 4138 self.endUndoAction() 4139 4140 def boxCommentLineOrSelection(self): 4141 """ 4142 Public slot to box comment the current line or current selection. 4143 """ 4144 if self.hasSelectedText(): 4145 self.boxCommentSelection() 4146 else: 4147 self.boxCommentLine() 4148 4149 ########################################################################### 4150 ## Indentation handling methods below 4151 ########################################################################### 4152 4153 def __indentLine(self, indent=True): 4154 """ 4155 Private method to indent or unindent the current line. 4156 4157 @param indent flag indicating an indent operation (boolean) 4158 <br />If the flag is true, an indent operation is performed. 4159 Otherwise the current line is unindented. 4160 """ 4161 line, index = self.getCursorPosition() 4162 self.beginUndoAction() 4163 if indent: 4164 self.indent(line) 4165 else: 4166 self.unindent(line) 4167 self.endUndoAction() 4168 if indent: 4169 self.setCursorPosition(line, index + self.indentationWidth()) 4170 else: 4171 self.setCursorPosition(line, index - self.indentationWidth()) 4172 4173 def __indentSelection(self, indent=True): 4174 """ 4175 Private method to indent or unindent the current selection. 4176 4177 @param indent flag indicating an indent operation (boolean) 4178 <br />If the flag is true, an indent operation is performed. 4179 Otherwise the current line is unindented. 4180 """ 4181 if not self.hasSelectedText(): 4182 return 4183 4184 # get the selection 4185 lineFrom, indexFrom, lineTo, indexTo = self.getSelection() 4186 endLine = lineTo if indexTo else lineTo - 1 4187 4188 self.beginUndoAction() 4189 # iterate over the lines 4190 for line in range(lineFrom, endLine + 1): 4191 if indent: 4192 self.indent(line) 4193 else: 4194 self.unindent(line) 4195 self.endUndoAction() 4196 if indent: 4197 if indexTo == 0: 4198 self.setSelection( 4199 lineFrom, indexFrom + self.indentationWidth(), 4200 lineTo, 0) 4201 else: 4202 self.setSelection( 4203 lineFrom, indexFrom + self.indentationWidth(), 4204 lineTo, indexTo + self.indentationWidth()) 4205 else: 4206 indexStart = indexFrom - self.indentationWidth() 4207 if indexStart < 0: 4208 indexStart = 0 4209 indexEnd = indexTo - self.indentationWidth() 4210 if indexEnd < 0: 4211 indexEnd = 0 4212 self.setSelection(lineFrom, indexStart, lineTo, indexEnd) 4213 4214 def indentLineOrSelection(self): 4215 """ 4216 Public slot to indent the current line or current selection. 4217 """ 4218 if self.hasSelectedText(): 4219 self.__indentSelection(True) 4220 else: 4221 self.__indentLine(True) 4222 4223 def unindentLineOrSelection(self): 4224 """ 4225 Public slot to unindent the current line or current selection. 4226 """ 4227 if self.hasSelectedText(): 4228 self.__indentSelection(False) 4229 else: 4230 self.__indentLine(False) 4231 4232 def smartIndentLineOrSelection(self): 4233 """ 4234 Public slot to indent current line smartly. 4235 """ 4236 if self.hasSelectedText(): 4237 if self.lexer_ and self.lexer_.hasSmartIndent(): 4238 self.lexer_.smartIndentSelection(self) 4239 else: 4240 self.__indentSelection(True) 4241 else: 4242 if self.lexer_ and self.lexer_.hasSmartIndent(): 4243 self.lexer_.smartIndentLine(self) 4244 else: 4245 self.__indentLine(True) 4246 4247 def gotoLine(self, line, pos=1, firstVisible=False, expand=False): 4248 """ 4249 Public slot to jump to the beginning of a line. 4250 4251 @param line line number to go to 4252 @type int 4253 @param pos position in line to go to 4254 @type int 4255 @param firstVisible flag indicating to make the line the first 4256 visible line 4257 @type bool 4258 @param expand flag indicating to expand all folds 4259 @type bool 4260 """ 4261 self.setCursorPosition(line - 1, pos - 1) 4262 if firstVisible: 4263 self.ensureVisibleTop(line, expand) 4264 else: 4265 self.ensureVisible(line, expand) 4266 4267 def __textChanged(self): 4268 """ 4269 Private slot to handle a change of the editor text. 4270 4271 This slot defers the handling to the next time the event loop 4272 is run in order to ensure, that cursor position has been updated 4273 by the underlying Scintilla editor. 4274 """ 4275 QTimer.singleShot(0, self.__saveLastEditPosition) 4276 4277 def __saveLastEditPosition(self): 4278 """ 4279 Private slot to record the last edit position. 4280 """ 4281 self.__lastEditPosition = self.getCursorPosition() 4282 self.lastEditPositionAvailable.emit() 4283 4284 def isLastEditPositionAvailable(self): 4285 """ 4286 Public method to check, if a last edit position is available. 4287 4288 @return flag indicating availability (boolean) 4289 """ 4290 return self.__lastEditPosition is not None 4291 4292 def gotoLastEditPosition(self): 4293 """ 4294 Public method to move the cursor to the last edit position. 4295 """ 4296 self.setCursorPosition(*self.__lastEditPosition) 4297 self.ensureVisible(self.__lastEditPosition[0]) 4298 4299 def gotoMethodClass(self, goUp=False): 4300 """ 4301 Public method to go to the next Python method or class definition. 4302 4303 @param goUp flag indicating the move direction (boolean) 4304 """ 4305 if self.isPyFile() or self.isRubyFile(): 4306 lineNo = self.getCursorPosition()[0] 4307 line = self.text(lineNo) 4308 if line.strip().startswith(("class ", "def ", "module ")): 4309 if goUp: 4310 lineNo -= 1 4311 else: 4312 lineNo += 1 4313 while True: 4314 if goUp and lineNo < 0: 4315 self.setCursorPosition(0, 0) 4316 self.ensureVisible(0) 4317 return 4318 elif not goUp and lineNo == self.lines(): 4319 lineNo = self.lines() - 1 4320 self.setCursorPosition(lineNo, self.lineLength(lineNo)) 4321 self.ensureVisible(lineNo) 4322 return 4323 4324 line = self.text(lineNo) 4325 if line.strip().startswith(("class ", "def ", "module ")): 4326 # try 'def ' first because it occurs more often 4327 first = line.find("def ") 4328 if first > -1: 4329 first += 4 4330 else: 4331 first = line.find("class ") 4332 if first > -1: 4333 first += 6 4334 else: 4335 first = line.find("module ") + 7 4336 match = re.search("[:(]", line) 4337 if match: 4338 end = match.start() 4339 else: 4340 end = self.lineLength(lineNo) - 1 4341 self.setSelection(lineNo, first, lineNo, end) 4342 self.ensureVisible(lineNo) 4343 return 4344 4345 if goUp: 4346 lineNo -= 1 4347 else: 4348 lineNo += 1 4349 4350 ########################################################################### 4351 ## Setup methods below 4352 ########################################################################### 4353 4354 def readSettings(self): 4355 """ 4356 Public slot to read the settings into our lexer. 4357 """ 4358 # read the lexer settings and reinit the properties 4359 if self.lexer_ is not None: 4360 self.lexer_.readSettings(Preferences.Prefs.settings, "Scintilla") 4361 if self.lexer_.hasSubstyles(): 4362 self.lexer_.readSubstyles(self) 4363 self.lexer_.initProperties() 4364 4365 self.lexer_.setDefaultColor(self.lexer_.color(0)) 4366 self.lexer_.setDefaultPaper(self.lexer_.paper(0)) 4367 4368 self.__bindLexer(self.fileName) 4369 self.recolor() 4370 4371 # read the typing completer settings 4372 if self.completer is not None: 4373 self.completer.readSettings() 4374 4375 # set the line marker colours or pixmap 4376 if Preferences.getEditor("LineMarkersBackground"): 4377 self.markerDefine(QsciScintilla.MarkerSymbol.Background, 4378 self.currentline) 4379 self.markerDefine(QsciScintilla.MarkerSymbol.Background, 4380 self.errorline) 4381 self.__setLineMarkerColours() 4382 else: 4383 self.markerDefine( 4384 UI.PixmapCache.getPixmap("currentLineMarker"), 4385 self.currentline) 4386 self.markerDefine( 4387 UI.PixmapCache.getPixmap("errorLineMarker"), 4388 self.errorline) 4389 4390 # set the text display 4391 self.__setTextDisplay() 4392 4393 # set margin 0 and 2 configuration 4394 self.__setMarginsDisplay() 4395 4396 # set the auto-completion function 4397 self.__acCache.setSize( 4398 Preferences.getEditor("AutoCompletionCacheSize")) 4399 self.__acCache.setMaximumCacheTime( 4400 Preferences.getEditor("AutoCompletionCacheTime")) 4401 self.__acCacheEnabled = Preferences.getEditor( 4402 "AutoCompletionCacheEnabled") 4403 acTimeout = Preferences.getEditor("AutoCompletionTimeout") 4404 if acTimeout != self.__acTimer.interval: 4405 self.__acTimer.setInterval(acTimeout) 4406 self.__setAutoCompletion() 4407 4408 # set the calltips function 4409 self.__setCallTips() 4410 4411 # set the autosave flags 4412 self.autosaveEnabled = Preferences.getEditor("AutosaveInterval") > 0 4413 4414 if Preferences.getEditor("MiniContextMenu") != self.miniMenu: 4415 # regenerate context menu 4416 self.__initContextMenu() 4417 else: 4418 # set checked context menu items 4419 self.menuActs["AutoCompletionEnable"].setChecked( 4420 self.autoCompletionThreshold() != -1) 4421 self.menuActs["MonospacedFont"].setChecked( 4422 self.useMonospaced) 4423 self.menuActs["AutosaveEnable"].setChecked( 4424 self.autosaveEnabled and not self.autosaveManuallyDisabled) 4425 4426 # regenerate the margins context menu(s) 4427 self.__initContextMenuMargins() 4428 4429 if Preferences.getEditor("MarkOccurrencesEnabled"): 4430 self.__markOccurrencesTimer.setInterval( 4431 Preferences.getEditor("MarkOccurrencesTimeout")) 4432 else: 4433 self.__markOccurrencesTimer.stop() 4434 self.clearSearchIndicators() 4435 4436 if Preferences.getEditor("OnlineSyntaxCheck"): 4437 self.__onlineSyntaxCheckTimer.setInterval( 4438 Preferences.getEditor("OnlineSyntaxCheckInterval") * 1000) 4439 else: 4440 self.__onlineSyntaxCheckTimer.stop() 4441 4442 if Preferences.getEditor("OnlineChangeTrace"): 4443 self.__onlineChangeTraceTimer.setInterval( 4444 Preferences.getEditor("OnlineChangeTraceInterval")) 4445 else: 4446 self.__onlineChangeTraceTimer.stop() 4447 self.__deleteAllChangeMarkers() 4448 self.markerDefine(self.__createChangeMarkerPixmap( 4449 "OnlineChangeTraceMarkerUnsaved"), self.__changeMarkerUnsaved) 4450 self.markerDefine(self.__createChangeMarkerPixmap( 4451 "OnlineChangeTraceMarkerSaved"), self.__changeMarkerSaved) 4452 4453 # refresh the annotations display 4454 self.__refreshAnnotations() 4455 4456 self.__markerMap.setMapPosition( 4457 Preferences.getEditor("ShowMarkerMapOnRight")) 4458 self.__markerMap.initColors() 4459 4460 self.setLanguage(self.fileName, propagate=False) 4461 4462 self.settingsRead.emit() 4463 4464 def __setLineMarkerColours(self): 4465 """ 4466 Private method to set the line marker colours. 4467 """ 4468 self.setMarkerForegroundColor( 4469 Preferences.getEditorColour("CurrentMarker"), self.currentline) 4470 self.setMarkerBackgroundColor( 4471 Preferences.getEditorColour("CurrentMarker"), self.currentline) 4472 self.setMarkerForegroundColor( 4473 Preferences.getEditorColour("ErrorMarker"), self.errorline) 4474 self.setMarkerBackgroundColor( 4475 Preferences.getEditorColour("ErrorMarker"), self.errorline) 4476 4477 def __setMarginsDisplay(self): 4478 """ 4479 Private method to configure margins 0 and 2. 4480 """ 4481 # set the settings for all margins 4482 self.setMarginsFont(Preferences.getEditorOtherFonts("MarginsFont")) 4483 self.setMarginsForegroundColor( 4484 Preferences.getEditorColour("MarginsForeground")) 4485 self.setMarginsBackgroundColor( 4486 Preferences.getEditorColour("MarginsBackground")) 4487 4488 # reset standard margins settings 4489 for margin in range(5): 4490 self.setMarginLineNumbers(margin, False) 4491 self.setMarginMarkerMask(margin, 0) 4492 self.setMarginWidth(margin, 0) 4493 self.setMarginSensitivity(margin, False) 4494 4495 # set marker margin(s) settings 4496 self.__bmMargin = 0 4497 self.__linenoMargin = 1 4498 self.__bpMargin = 2 4499 self.__foldMargin = 3 4500 self.__indicMargin = 4 4501 4502 marginBmMask = (1 << self.bookmark) 4503 self.setMarginWidth(self.__bmMargin, 16) 4504 self.setMarginSensitivity(self.__bmMargin, True) 4505 self.setMarginMarkerMask(self.__bmMargin, marginBmMask) 4506 4507 marginBpMask = ( 4508 (1 << self.breakpoint) | 4509 (1 << self.cbreakpoint) | 4510 (1 << self.tbreakpoint) | 4511 (1 << self.tcbreakpoint) | 4512 (1 << self.dbreakpoint) 4513 ) 4514 self.setMarginWidth(self.__bpMargin, 16) 4515 self.setMarginSensitivity(self.__bpMargin, True) 4516 self.setMarginMarkerMask(self.__bpMargin, marginBpMask) 4517 4518 marginIndicMask = ( 4519 (1 << self.syntaxerror) | 4520 (1 << self.notcovered) | 4521 (1 << self.taskmarker) | 4522 (1 << self.warning) | 4523 (1 << self.__changeMarkerUnsaved) | 4524 (1 << self.__changeMarkerSaved) | 4525 (1 << self.currentline) | 4526 (1 << self.errorline) 4527 ) 4528 self.setMarginWidth(self.__indicMargin, 16) 4529 self.setMarginSensitivity(self.__indicMargin, True) 4530 self.setMarginMarkerMask(self.__indicMargin, marginIndicMask) 4531 4532 # set linenumber margin settings 4533 linenoMargin = Preferences.getEditor("LinenoMargin") 4534 self.setMarginLineNumbers(self.__linenoMargin, linenoMargin) 4535 if linenoMargin: 4536 self.__resizeLinenoMargin() 4537 else: 4538 self.setMarginWidth(self.__linenoMargin, 0) 4539 4540 # set folding margin settings 4541 if Preferences.getEditor("FoldingMargin"): 4542 self.setMarginWidth(self.__foldMargin, 16) 4543 folding = Preferences.getEditor("FoldingStyle") 4544 with contextlib.suppress(AttributeError): 4545 folding = QsciScintilla.FoldStyle(folding) 4546 self.setFolding(folding, self.__foldMargin) 4547 self.setFoldMarginColors( 4548 Preferences.getEditorColour("FoldmarginBackground"), 4549 Preferences.getEditorColour("FoldmarginBackground")) 4550 self.setFoldMarkersColors( 4551 Preferences.getEditorColour("FoldMarkersForeground"), 4552 Preferences.getEditorColour("FoldMarkersBackground")) 4553 else: 4554 self.setMarginWidth(self.__foldMargin, 0) 4555 self.setFolding(QsciScintilla.FoldStyle.NoFoldStyle, 4556 self.__foldMargin) 4557 4558 def __resizeLinenoMargin(self): 4559 """ 4560 Private slot to resize the line numbers margin. 4561 """ 4562 linenoMargin = Preferences.getEditor("LinenoMargin") 4563 if linenoMargin: 4564 self.setMarginWidth( 4565 self.__linenoMargin, '8' * (len(str(self.lines())) + 1)) 4566 4567 def __setTabAndIndent(self): 4568 """ 4569 Private method to set indentation size and style and tab width. 4570 """ 4571 self.setTabWidth(self.__getEditorConfig("TabWidth")) 4572 self.setIndentationWidth(self.__getEditorConfig("IndentWidth")) 4573 if self.lexer_ and self.lexer_.alwaysKeepTabs(): 4574 self.setIndentationsUseTabs(True) 4575 else: 4576 self.setIndentationsUseTabs( 4577 self.__getEditorConfig("TabForIndentation")) 4578 4579 def __setTextDisplay(self): 4580 """ 4581 Private method to configure the text display. 4582 """ 4583 self.__setTabAndIndent() 4584 4585 self.setTabIndents(Preferences.getEditor("TabIndents")) 4586 self.setBackspaceUnindents(Preferences.getEditor("TabIndents")) 4587 self.setIndentationGuides(Preferences.getEditor("IndentationGuides")) 4588 self.setIndentationGuidesBackgroundColor( 4589 Preferences.getEditorColour("IndentationGuidesBackground")) 4590 self.setIndentationGuidesForegroundColor( 4591 Preferences.getEditorColour("IndentationGuidesForeground")) 4592 if Preferences.getEditor("ShowWhitespace"): 4593 self.setWhitespaceVisibility( 4594 QsciScintilla.WhitespaceVisibility.WsVisible) 4595 with contextlib.suppress(AttributeError): 4596 self.setWhitespaceForegroundColor( 4597 Preferences.getEditorColour("WhitespaceForeground")) 4598 self.setWhitespaceBackgroundColor( 4599 Preferences.getEditorColour("WhitespaceBackground")) 4600 self.setWhitespaceSize( 4601 Preferences.getEditor("WhitespaceSize")) 4602 else: 4603 self.setWhitespaceVisibility( 4604 QsciScintilla.WhitespaceVisibility.WsInvisible) 4605 self.setEolVisibility(Preferences.getEditor("ShowEOL")) 4606 self.setAutoIndent(Preferences.getEditor("AutoIndentation")) 4607 if Preferences.getEditor("BraceHighlighting"): 4608 self.setBraceMatching(QsciScintilla.BraceMatch.SloppyBraceMatch) 4609 else: 4610 self.setBraceMatching(QsciScintilla.BraceMatch.NoBraceMatch) 4611 self.setMatchedBraceForegroundColor( 4612 Preferences.getEditorColour("MatchingBrace")) 4613 self.setMatchedBraceBackgroundColor( 4614 Preferences.getEditorColour("MatchingBraceBack")) 4615 self.setUnmatchedBraceForegroundColor( 4616 Preferences.getEditorColour("NonmatchingBrace")) 4617 self.setUnmatchedBraceBackgroundColor( 4618 Preferences.getEditorColour("NonmatchingBraceBack")) 4619 if Preferences.getEditor("CustomSelectionColours"): 4620 self.setSelectionBackgroundColor( 4621 Preferences.getEditorColour("SelectionBackground")) 4622 else: 4623 self.setSelectionBackgroundColor( 4624 QApplication.palette().color(QPalette.ColorRole.Highlight)) 4625 if Preferences.getEditor("ColourizeSelText"): 4626 self.resetSelectionForegroundColor() 4627 elif Preferences.getEditor("CustomSelectionColours"): 4628 self.setSelectionForegroundColor( 4629 Preferences.getEditorColour("SelectionForeground")) 4630 else: 4631 self.setSelectionForegroundColor( 4632 QApplication.palette().color( 4633 QPalette.ColorRole.HighlightedText)) 4634 self.setSelectionToEol(Preferences.getEditor("ExtendSelectionToEol")) 4635 self.setCaretForegroundColor( 4636 Preferences.getEditorColour("CaretForeground")) 4637 self.setCaretLineBackgroundColor( 4638 Preferences.getEditorColour("CaretLineBackground")) 4639 self.setCaretLineVisible(Preferences.getEditor("CaretLineVisible")) 4640 self.setCaretLineAlwaysVisible( 4641 Preferences.getEditor("CaretLineAlwaysVisible")) 4642 self.caretWidth = Preferences.getEditor("CaretWidth") 4643 self.setCaretWidth(self.caretWidth) 4644 self.caretLineFrameWidth = Preferences.getEditor("CaretLineFrameWidth") 4645 self.setCaretLineFrameWidth(self.caretLineFrameWidth) 4646 self.useMonospaced = Preferences.getEditor("UseMonospacedFont") 4647 self.setMonospaced(self.useMonospaced) 4648 edgeMode = Preferences.getEditor("EdgeMode") 4649 edge = QsciScintilla.EdgeMode(edgeMode) 4650 self.setEdgeMode(edge) 4651 if edgeMode: 4652 self.setEdgeColumn(Preferences.getEditor("EdgeColumn")) 4653 self.setEdgeColor(Preferences.getEditorColour("Edge")) 4654 4655 wrapVisualFlag = Preferences.getEditor("WrapVisualFlag") 4656 self.setWrapMode(Preferences.getEditor("WrapLongLinesMode")) 4657 self.setWrapVisualFlags(wrapVisualFlag, wrapVisualFlag) 4658 self.setWrapIndentMode(Preferences.getEditor("WrapIndentMode")) 4659 self.setWrapStartIndent(Preferences.getEditor("WrapStartIndent")) 4660 4661 self.zoomTo(Preferences.getEditor("ZoomFactor")) 4662 4663 self.searchIndicator = QsciScintilla.INDIC_CONTAINER 4664 self.indicatorDefine( 4665 self.searchIndicator, QsciScintilla.INDIC_BOX, 4666 Preferences.getEditorColour("SearchMarkers")) 4667 if ( 4668 not Preferences.getEditor("SearchMarkersEnabled") and 4669 not Preferences.getEditor("QuickSearchMarkersEnabled") and 4670 not Preferences.getEditor("MarkOccurrencesEnabled") 4671 ): 4672 self.clearAllIndicators(self.searchIndicator) 4673 4674 self.spellingIndicator = QsciScintilla.INDIC_CONTAINER + 1 4675 self.indicatorDefine( 4676 self.spellingIndicator, QsciScintilla.INDIC_SQUIGGLE, 4677 Preferences.getEditorColour("SpellingMarkers")) 4678 self.__setSpelling() 4679 4680 self.highlightIndicator = QsciScintilla.INDIC_CONTAINER + 2 4681 self.indicatorDefine( 4682 self.highlightIndicator, QsciScintilla.INDIC_FULLBOX, 4683 Preferences.getEditorColour("HighlightMarker")) 4684 4685 self.setCursorFlashTime(QApplication.cursorFlashTime()) 4686 4687 with contextlib.suppress(AttributeError): 4688 if Preferences.getEditor("AnnotationsEnabled"): 4689 self.setAnnotationDisplay( 4690 QsciScintilla.AnnotationDisplay.AnnotationBoxed) 4691 else: 4692 self.setAnnotationDisplay( 4693 QsciScintilla.AnnotationDisplay.AnnotationHidden) 4694 self.__setAnnotationStyles() 4695 4696 if Preferences.getEditor("OverrideEditAreaColours"): 4697 self.setColor(Preferences.getEditorColour("EditAreaForeground")) 4698 self.setPaper(Preferences.getEditorColour("EditAreaBackground")) 4699 4700 self.setVirtualSpaceOptions( 4701 Preferences.getEditor("VirtualSpaceOptions")) 4702 4703 # to avoid errors due to line endings by pasting 4704 self.SendScintilla(QsciScintilla.SCI_SETPASTECONVERTENDINGS, True) 4705 4706 self.__markerMap.setEnabled(True) 4707 4708 def __setEolMode(self): 4709 """ 4710 Private method to configure the eol mode of the editor. 4711 """ 4712 if ( 4713 self.fileName and 4714 self.project.isOpen() and 4715 self.project.isProjectFile(self.fileName) 4716 ): 4717 eolMode = self.__getEditorConfig("EOLMode", nodefault=True) 4718 if eolMode is None: 4719 eolStr = self.project.getEolString() 4720 self.setEolModeByEolString(eolStr) 4721 else: 4722 self.setEolMode(eolMode) 4723 else: 4724 eolMode = self.__getEditorConfig("EOLMode") 4725 eolMode = QsciScintilla.EolMode(eolMode) 4726 self.setEolMode(eolMode) 4727 self.__eolChanged() 4728 4729 def __setAutoCompletion(self): 4730 """ 4731 Private method to configure the autocompletion function. 4732 """ 4733 if self.lexer_: 4734 self.setAutoCompletionFillupsEnabled( 4735 Preferences.getEditor("AutoCompletionFillups")) 4736 self.setAutoCompletionCaseSensitivity( 4737 Preferences.getEditor("AutoCompletionCaseSensitivity")) 4738 self.setAutoCompletionReplaceWord( 4739 Preferences.getEditor("AutoCompletionReplaceWord")) 4740 self.setAutoCompletionThreshold(0) 4741 try: 4742 self.setAutoCompletionUseSingle( 4743 Preferences.getEditor("AutoCompletionShowSingle")) 4744 except AttributeError: 4745 self.setAutoCompletionShowSingle( 4746 Preferences.getEditor("AutoCompletionShowSingle")) 4747 autoCompletionSource = Preferences.getEditor("AutoCompletionSource") 4748 if ( 4749 autoCompletionSource == 4750 QsciScintilla.AutoCompletionSource.AcsDocument 4751 ): 4752 self.setAutoCompletionSource( 4753 QsciScintilla.AutoCompletionSource.AcsDocument) 4754 elif ( 4755 autoCompletionSource == QsciScintilla.AutoCompletionSource.AcsAPIs 4756 ): 4757 self.setAutoCompletionSource( 4758 QsciScintilla.AutoCompletionSource.AcsAPIs) 4759 else: 4760 self.setAutoCompletionSource( 4761 QsciScintilla.AutoCompletionSource.AcsAll) 4762 4763 self.setAutoCompletionWidgetSize( 4764 Preferences.getEditor("AutoCompletionMaxChars"), 4765 Preferences.getEditor("AutoCompletionMaxLines") 4766 ) 4767 4768 def __setCallTips(self): 4769 """ 4770 Private method to configure the calltips function. 4771 """ 4772 self.setCallTipsBackgroundColor( 4773 Preferences.getEditorColour("CallTipsBackground")) 4774 self.setCallTipsForegroundColor( 4775 Preferences.getEditorColour("CallTipsForeground")) 4776 self.setCallTipsHighlightColor( 4777 Preferences.getEditorColour("CallTipsHighlight")) 4778 self.setCallTipsVisible(Preferences.getEditor("CallTipsVisible")) 4779 calltipsStyle = Preferences.getEditor("CallTipsStyle") 4780 with contextlib.suppress(AttributeError): 4781 self.setCallTipsPosition( 4782 Preferences.getEditor("CallTipsPosition")) 4783 4784 if Preferences.getEditor("CallTipsEnabled"): 4785 if calltipsStyle == QsciScintilla.CallTipsStyle.CallTipsNoContext: 4786 self.setCallTipsStyle( 4787 QsciScintilla.CallTipsStyle.CallTipsNoContext) 4788 elif ( 4789 calltipsStyle == 4790 QsciScintilla.CallTipsStyle.CallTipsNoAutoCompletionContext 4791 ): 4792 self.setCallTipsStyle( 4793 QsciScintilla.CallTipsStyle 4794 .CallTipsNoAutoCompletionContext) 4795 else: 4796 self.setCallTipsStyle( 4797 QsciScintilla.CallTipsStyle.CallTipsContext) 4798 else: 4799 self.setCallTipsStyle(QsciScintilla.CallTipsStyle.CallTipsNone) 4800 4801 ########################################################################### 4802 ## Autocompletion handling methods below 4803 ########################################################################### 4804 4805 def canAutoCompleteFromAPIs(self): 4806 """ 4807 Public method to check for API availablity. 4808 4809 @return flag indicating autocompletion from APIs is available (boolean) 4810 """ 4811 return self.acAPI 4812 4813 def autoCompleteQScintilla(self): 4814 """ 4815 Public method to perform an autocompletion using QScintilla methods. 4816 """ 4817 self.__acText = ' ' # Prevent long running ACs to add results 4818 self.__acWatchdog.stop() 4819 if self.__acCompletions: 4820 return 4821 4822 acs = Preferences.getEditor("AutoCompletionSource") 4823 if acs == QsciScintilla.AutoCompletionSource.AcsDocument: 4824 self.autoCompleteFromDocument() 4825 elif acs == QsciScintilla.AutoCompletionSource.AcsAPIs: 4826 self.autoCompleteFromAPIs() 4827 elif acs == QsciScintilla.AutoCompletionSource.AcsAll: 4828 self.autoCompleteFromAll() 4829 else: 4830 E5MessageBox.information( 4831 self, 4832 self.tr("Autocompletion"), 4833 self.tr( 4834 """Autocompletion is not available because""" 4835 """ there is no autocompletion source set.""")) 4836 4837 def setAutoCompletionEnabled(self, enable): 4838 """ 4839 Public method to enable/disable autocompletion. 4840 4841 @param enable flag indicating the desired autocompletion status 4842 (boolean) 4843 """ 4844 if enable: 4845 autoCompletionSource = Preferences.getEditor( 4846 "AutoCompletionSource") 4847 if ( 4848 autoCompletionSource == 4849 QsciScintilla.AutoCompletionSource.AcsDocument 4850 ): 4851 self.setAutoCompletionSource( 4852 QsciScintilla.AutoCompletionSource.AcsDocument) 4853 elif ( 4854 autoCompletionSource == 4855 QsciScintilla.AutoCompletionSource.AcsAPIs 4856 ): 4857 self.setAutoCompletionSource( 4858 QsciScintilla.AutoCompletionSource.AcsAPIs) 4859 else: 4860 self.setAutoCompletionSource( 4861 QsciScintilla.AutoCompletionSource.AcsAll) 4862 4863 def __toggleAutoCompletionEnable(self): 4864 """ 4865 Private slot to handle the Enable Autocompletion context menu entry. 4866 """ 4867 if self.menuActs["AutoCompletionEnable"].isChecked(): 4868 self.setAutoCompletionEnabled(True) 4869 else: 4870 self.setAutoCompletionEnabled(False) 4871 4872 ################################################################# 4873 ## Support for autocompletion hook methods 4874 ################################################################# 4875 4876 def __charAdded(self, charNumber): 4877 """ 4878 Private slot called to handle the user entering a character. 4879 4880 @param charNumber value of the character entered (integer) 4881 """ 4882 char = chr(charNumber) 4883 # update code documentation viewer 4884 if ( 4885 char == "(" and 4886 Preferences.getDocuViewer("ShowInfoOnOpenParenthesis") 4887 ): 4888 self.vm.showEditorInfo(self) 4889 4890 self.__delayedDocstringMenuPopup(self.getCursorPosition()) 4891 4892 if self.isListActive(): 4893 if self.__isStartChar(char): 4894 self.cancelList() 4895 self.autoComplete(auto=True, context=True) 4896 return 4897 elif char == '(': 4898 self.cancelList() 4899 else: 4900 self.__acTimer.stop() 4901 4902 if ( 4903 self.callTipsStyle() != 4904 QsciScintilla.CallTipsStyle.CallTipsNone and 4905 self.lexer_ is not None and chr(charNumber) in '()' 4906 ): 4907 self.callTip() 4908 4909 if not self.isCallTipActive(): 4910 char = chr(charNumber) 4911 if self.__isStartChar(char): 4912 self.autoComplete(auto=True, context=True) 4913 return 4914 4915 line, col = self.getCursorPosition() 4916 txt = self.getWordLeft(line, col) 4917 if len(txt) >= Preferences.getEditor("AutoCompletionThreshold"): 4918 self.autoComplete(auto=True, context=False) 4919 return 4920 4921 def __isStartChar(self, ch): 4922 """ 4923 Private method to check, if a character is an autocompletion start 4924 character. 4925 4926 @param ch character to be checked (one character string) 4927 @return flag indicating the result (boolean) 4928 """ 4929 if self.lexer_ is None: 4930 return False 4931 4932 wseps = self.lexer_.autoCompletionWordSeparators() 4933 return any(wsep.endswith(ch) for wsep in wseps) 4934 4935 def __autocompletionCancelled(self): 4936 """ 4937 Private slot to handle the cancellation of an auto-completion list. 4938 """ 4939 self.__acWatchdog.stop() 4940 4941 self.__acText = "" 4942 4943 ################################################################# 4944 ## auto-completion hook interfaces 4945 ################################################################# 4946 4947 def addCompletionListHook(self, key, func, asynchroneous=False): 4948 """ 4949 Public method to set an auto-completion list provider. 4950 4951 @param key name of the provider 4952 @type str 4953 @param func function providing completion list. func 4954 should be a function taking a reference to the editor and 4955 a boolean indicating to complete a context. It should return 4956 the possible completions as a list of strings. 4957 @type function(editor, bool) -> list of str in case async is False 4958 and function(editor, bool, str) returning nothing in case async 4959 is True 4960 @param asynchroneous flag indicating an asynchroneous function 4961 @type bool 4962 """ 4963 if ( 4964 key in self.__completionListHookFunctions or 4965 key in self.__completionListAsyncHookFunctions 4966 ): 4967 # it was already registered 4968 E5MessageBox.warning( 4969 self, 4970 self.tr("Auto-Completion Provider"), 4971 self.tr("""The completion list provider '{0}' was already""" 4972 """ registered. Ignoring duplicate request.""") 4973 .format(key)) 4974 return 4975 4976 if asynchroneous: 4977 self.__completionListAsyncHookFunctions[key] = func 4978 else: 4979 self.__completionListHookFunctions[key] = func 4980 4981 def removeCompletionListHook(self, key): 4982 """ 4983 Public method to remove a previously registered completion list 4984 provider. 4985 4986 @param key name of the provider 4987 @type str 4988 """ 4989 if key in self.__completionListHookFunctions: 4990 del self.__completionListHookFunctions[key] 4991 elif key in self.__completionListAsyncHookFunctions: 4992 del self.__completionListAsyncHookFunctions[key] 4993 4994 def getCompletionListHook(self, key): 4995 """ 4996 Public method to get the registered completion list provider. 4997 4998 @param key name of the provider 4999 @type str 5000 @return function providing completion list 5001 @rtype function or None 5002 """ 5003 return (self.__completionListHookFunctions.get(key) or 5004 self.__completionListAsyncHookFunctions.get(key)) 5005 5006 def autoComplete(self, auto=False, context=True): 5007 """ 5008 Public method to start auto-completion. 5009 5010 @param auto flag indicating a call from the __charAdded method 5011 (boolean) 5012 @param context flag indicating to complete a context (boolean) 5013 """ 5014 if auto and not Preferences.getEditor("AutoCompletionEnabled"): 5015 # auto-completion is disabled 5016 return 5017 5018 if self.isListActive(): 5019 self.cancelList() 5020 5021 if ( 5022 self.__completionListHookFunctions or 5023 self.__completionListAsyncHookFunctions 5024 ): 5025 # Avoid delayed auto-completion after cursor repositioning 5026 self.__acText = self.__getAcText() 5027 if auto and Preferences.getEditor("AutoCompletionTimeout"): 5028 self.__acTimer.stop() 5029 self.__acContext = context 5030 self.__acTimer.start() 5031 else: 5032 self.__autoComplete(auto, context) 5033 elif ( 5034 not auto or 5035 (self.autoCompletionSource() != 5036 QsciScintilla.AutoCompletionSource.AcsNone) 5037 ): 5038 self.autoCompleteQScintilla() 5039 5040 def __getAcText(self): 5041 """ 5042 Private method to get the text from cursor position for autocompleting. 5043 5044 @return text left of cursor position 5045 @rtype str 5046 """ 5047 line, col = self.getCursorPosition() 5048 text = self.text(line) 5049 try: 5050 acText = ( 5051 self.getWordLeft(line, col - 1) + text[col - 1] 5052 if self.__isStartChar(text[col - 1]) else 5053 self.getWordLeft(line, col) 5054 ) 5055 except IndexError: 5056 acText = "" 5057 5058 return acText 5059 5060 def __autoComplete(self, auto=True, context=None): 5061 """ 5062 Private method to start auto-completion via plug-ins. 5063 5064 @param auto flag indicating a call from the __charAdded method 5065 (boolean) 5066 @param context flag indicating to complete a context 5067 @type bool or None 5068 """ 5069 self.__acCompletions.clear() 5070 self.__acCompletionsFinished = 0 5071 5072 # Suppress empty completions 5073 if auto and self.__acText == '': 5074 return 5075 5076 completions = ( 5077 self.__acCache.get(self.__acText) 5078 if self.__acCacheEnabled else 5079 None 5080 ) 5081 if completions is not None: 5082 # show list with cached entries 5083 if self.isListActive(): 5084 self.cancelList() 5085 5086 self.__showCompletionsList(completions) 5087 else: 5088 if context is None: 5089 context = self.__acContext 5090 5091 for key in self.__completionListAsyncHookFunctions: 5092 self.__completionListAsyncHookFunctions[key]( 5093 self, context, self.__acText) 5094 5095 for key in self.__completionListHookFunctions: 5096 completions = self.__completionListHookFunctions[key]( 5097 self, context) 5098 self.completionsListReady(completions, self.__acText) 5099 5100 if Preferences.getEditor("AutoCompletionScintillaOnFail"): 5101 self.__acWatchdog.start() 5102 5103 def completionsListReady(self, completions, acText): 5104 """ 5105 Public method to show the completions determined by a completions 5106 provider. 5107 5108 @param completions list of possible completions 5109 @type list of str or set of str 5110 @param acText text to be completed 5111 @type str 5112 """ 5113 currentWord = self.__getAcText() or ' ' 5114 # process the list only, if not already obsolete ... 5115 if acText != self.__acText or not self.__acText.endswith(currentWord): 5116 # Suppress auto-completion done by QScintilla as fallback 5117 self.__acWatchdog.stop() 5118 return 5119 5120 self.__acCompletions.update(set(completions)) 5121 5122 self.__acCompletionsFinished += 1 5123 # Got all results from auto completer? 5124 if self.__acCompletionsFinished >= ( 5125 len(self.__completionListAsyncHookFunctions) + 5126 len(self.__completionListHookFunctions) 5127 ): 5128 self.__acWatchdog.stop() 5129 5130 # Autocomplete with QScintilla if no results present 5131 if ( 5132 Preferences.getEditor("AutoCompletionScintillaOnFail") and 5133 not self.__acCompletions 5134 ): 5135 self.autoCompleteQScintilla() 5136 return 5137 5138 # ... or completions are not empty 5139 if not bool(completions): 5140 return 5141 5142 if self.isListActive(): 5143 self.cancelList() 5144 5145 if self.__acCompletions: 5146 if self.__acCacheEnabled: 5147 self.__acCache.add(acText, set(self.__acCompletions)) 5148 self.__showCompletionsList(self.__acCompletions) 5149 5150 def __showCompletionsList(self, completions): 5151 """ 5152 Private method to show the completions list. 5153 5154 @param completions completions to be shown 5155 @type list of str or set of str 5156 """ 5157 acCompletions = ( 5158 sorted( 5159 list(completions), 5160 key=self.__replaceLeadingUnderscores) 5161 if Preferences.getEditor("AutoCompletionReversedList") else 5162 sorted(list(completions)) 5163 ) 5164 self.showUserList(EditorAutoCompletionListID, acCompletions) 5165 5166 def __replaceLeadingUnderscores(self, txt): 5167 """ 5168 Private method to replace the first two underlines for invers sorting. 5169 5170 @param txt completion text 5171 @type str 5172 @return modified completion text 5173 @rtype str 5174 """ 5175 if txt.startswith('_'): 5176 return txt[:2].replace('_', '~') + txt[2:] 5177 else: 5178 return txt 5179 5180 def __clearCompletionsCache(self): 5181 """ 5182 Private method to clear the auto-completions cache. 5183 """ 5184 self.__acCache.clear() 5185 5186 def __completionListSelected(self, listId, txt): 5187 """ 5188 Private slot to handle the selection from the completion list. 5189 5190 @param listId the ID of the user list (should be 1 or 2) (integer) 5191 @param txt the selected text (string) 5192 """ 5193 if listId == EditorAutoCompletionListID: 5194 lst = txt.split() 5195 if len(lst) > 1: 5196 txt = lst[0] 5197 5198 self.beginUndoAction() 5199 if Preferences.getEditor("AutoCompletionReplaceWord"): 5200 self.selectCurrentWord() 5201 self.removeSelectedText() 5202 line, col = self.getCursorPosition() 5203 else: 5204 line, col = self.getCursorPosition() 5205 wLeft = self.getWordLeft(line, col) 5206 if not txt.startswith(wLeft): 5207 self.selectCurrentWord() 5208 self.removeSelectedText() 5209 line, col = self.getCursorPosition() 5210 elif wLeft: 5211 txt = txt[len(wLeft):] 5212 5213 if txt and txt[0] in "'\"": 5214 # New in jedi 0.16: AC of dict keys 5215 txt = txt[1:] 5216 self.insert(txt) 5217 self.endUndoAction() 5218 self.setCursorPosition(line, col + len(txt)) 5219 elif listId == TemplateCompletionListID: 5220 self.__applyTemplate(txt, self.getLanguage()) 5221 5222 def canProvideDynamicAutoCompletion(self): 5223 """ 5224 Public method to test the dynamic auto-completion availability. 5225 5226 @return flag indicating the availability of dynamic auto-completion 5227 (boolean) 5228 """ 5229 return (self.acAPI or 5230 bool(self.__completionListHookFunctions) or 5231 bool(self.__completionListAsyncHookFunctions)) 5232 5233 ################################################################# 5234 ## call-tip hook interfaces 5235 ################################################################# 5236 5237 def addCallTipHook(self, key, func): 5238 """ 5239 Public method to set a calltip provider. 5240 5241 @param key name of the provider 5242 @type str 5243 @param func function providing calltips. func 5244 should be a function taking a reference to the editor, 5245 a position into the text and the amount of commas to the 5246 left of the cursor. It should return the possible 5247 calltips as a list of strings. 5248 @type function(editor, int, int) -> list of str 5249 """ 5250 if key in self.__ctHookFunctions: 5251 # it was already registered 5252 E5MessageBox.warning( 5253 self, 5254 self.tr("Call-Tips Provider"), 5255 self.tr("""The call-tips provider '{0}' was already""" 5256 """ registered. Ignoring duplicate request.""") 5257 .format(key)) 5258 return 5259 5260 self.__ctHookFunctions[key] = func 5261 5262 def removeCallTipHook(self, key): 5263 """ 5264 Public method to remove a previously registered calltip provider. 5265 5266 @param key name of the provider 5267 @type str 5268 """ 5269 if key in self.__ctHookFunctions: 5270 del self.__ctHookFunctions[key] 5271 5272 def getCallTipHook(self, key): 5273 """ 5274 Public method to get the registered calltip provider. 5275 5276 @param key name of the provider 5277 @type str 5278 @return function providing calltips 5279 @rtype function or None 5280 """ 5281 if key in self.__ctHookFunctions: 5282 return self.__ctHookFunctions[key] 5283 else: 5284 return None 5285 5286 def canProvideCallTipps(self): 5287 """ 5288 Public method to test the calltips availability. 5289 5290 @return flag indicating the availability of calltips (boolean) 5291 """ 5292 return (self.acAPI or 5293 bool(self.__ctHookFunctions)) 5294 5295 def callTip(self): 5296 """ 5297 Public method to show calltips. 5298 """ 5299 if bool(self.__ctHookFunctions): 5300 self.__callTip() 5301 else: 5302 super().callTip() 5303 5304 def __callTip(self): 5305 """ 5306 Private method to show call tips provided by a plugin. 5307 """ 5308 pos = self.currentPosition() 5309 5310 # move backward to the start of the current calltip working out 5311 # which argument to highlight 5312 commas = 0 5313 found = False 5314 ch, pos = self.__getCharacter(pos) 5315 while ch: 5316 if ch == ',': 5317 commas += 1 5318 elif ch == ')': 5319 depth = 1 5320 5321 # ignore everything back to the start of the corresponding 5322 # parenthesis 5323 ch, pos = self.__getCharacter(pos) 5324 while ch: 5325 if ch == ')': 5326 depth += 1 5327 elif ch == '(': 5328 depth -= 1 5329 if depth == 0: 5330 break 5331 ch, pos = self.__getCharacter(pos) 5332 elif ch == '(': 5333 found = True 5334 break 5335 5336 ch, pos = self.__getCharacter(pos) 5337 5338 self.SendScintilla(QsciScintilla.SCI_CALLTIPCANCEL) 5339 5340 if not found: 5341 return 5342 5343 callTips = [] 5344 if self.__ctHookFunctions: 5345 for key in self.__ctHookFunctions: 5346 callTips.extend(self.__ctHookFunctions[key](self, pos, commas)) 5347 callTips = list(set(callTips)) 5348 callTips.sort() 5349 else: 5350 # try QScintilla calltips 5351 super().callTip() 5352 return 5353 if len(callTips) == 0: 5354 if Preferences.getEditor("CallTipsScintillaOnFail"): 5355 # try QScintilla calltips 5356 super().callTip() 5357 return 5358 5359 ctshift = 0 5360 for ct in callTips: 5361 shift = ct.index("(") 5362 if ctshift < shift: 5363 ctshift = shift 5364 5365 cv = self.callTipsVisible() 5366 ct = ( 5367 # this is just a safe guard 5368 self._encodeString("\n".join(callTips[:cv])) 5369 if cv > 0 else 5370 # until here and unindent below 5371 self._encodeString("\n".join(callTips)) 5372 ) 5373 5374 self.SendScintilla(QsciScintilla.SCI_CALLTIPSHOW, 5375 self.__adjustedCallTipPosition(ctshift, pos), ct) 5376 if b'\n' in ct: 5377 return 5378 5379 # Highlight the current argument 5380 if commas == 0: 5381 astart = ct.find(b'(') 5382 else: 5383 astart = ct.find(b',') 5384 commas -= 1 5385 while astart != -1 and commas > 0: 5386 astart = ct.find(b',', astart + 1) 5387 commas -= 1 5388 5389 if astart == -1: 5390 return 5391 5392 depth = 0 5393 for aend in range(astart + 1, len(ct)): 5394 ch = ct[aend:aend + 1] 5395 5396 if ch == b',' and depth == 0: 5397 break 5398 elif ch == b'(': 5399 depth += 1 5400 elif ch == b')': 5401 if depth == 0: 5402 break 5403 5404 depth -= 1 5405 5406 if astart != aend: 5407 self.SendScintilla(QsciScintilla.SCI_CALLTIPSETHLT, 5408 astart + 1, aend) 5409 5410 def __adjustedCallTipPosition(self, ctshift, pos): 5411 """ 5412 Private method to calculate an adjusted position for showing calltips. 5413 5414 @param ctshift amount the calltip shall be shifted (integer) 5415 @param pos position into the text (integer) 5416 @return new position for the calltip (integer) 5417 """ 5418 ct = pos 5419 if ctshift: 5420 ctmin = self.SendScintilla( 5421 QsciScintilla.SCI_POSITIONFROMLINE, 5422 self.SendScintilla(QsciScintilla.SCI_LINEFROMPOSITION, ct)) 5423 if ct - ctshift < ctmin: 5424 ct = ctmin 5425 else: 5426 ct -= ctshift 5427 return ct 5428 5429 ################################################################# 5430 ## Methods needed by the code documentation viewer 5431 ################################################################# 5432 5433 def __showCodeInfo(self): 5434 """ 5435 Private slot to handle the context menu action to show code info. 5436 """ 5437 self.vm.showEditorInfo(self) 5438 5439 ################################################################# 5440 ## Methods needed by the context menu 5441 ################################################################# 5442 5443 def __marginNumber(self, xPos): 5444 """ 5445 Private method to calculate the margin number based on a x position. 5446 5447 @param xPos x position (integer) 5448 @return margin number (integer, -1 for no margin) 5449 """ 5450 width = 0 5451 for margin in range(5): 5452 width += self.marginWidth(margin) 5453 if xPos <= width: 5454 return margin 5455 return -1 5456 5457 def contextMenuEvent(self, evt): 5458 """ 5459 Protected method implementing the context menu event. 5460 5461 @param evt the context menu event (QContextMenuEvent) 5462 """ 5463 evt.accept() 5464 if self.__marginNumber(evt.x()) == -1: 5465 self.spellingMenuPos = self.positionFromPoint(evt.pos()) 5466 if ( 5467 self.spellingMenuPos >= 0 and 5468 self.spell is not None and 5469 self.hasIndicator(self.spellingIndicator, 5470 self.spellingMenuPos) 5471 ): 5472 self.spellingMenu.popup(evt.globalPos()) 5473 else: 5474 self.menu.popup(evt.globalPos()) 5475 else: 5476 self.line = self.lineAt(evt.pos()) 5477 if self.__marginNumber(evt.x()) in [self.__bmMargin, 5478 self.__linenoMargin]: 5479 self.bmMarginMenu.popup(evt.globalPos()) 5480 elif self.__marginNumber(evt.x()) == self.__bpMargin: 5481 self.bpMarginMenu.popup(evt.globalPos()) 5482 elif self.__marginNumber(evt.x()) == self.__indicMargin: 5483 self.indicMarginMenu.popup(evt.globalPos()) 5484 elif self.__marginNumber(evt.x()) == self.__foldMargin: 5485 self.foldMarginMenu.popup(evt.globalPos()) 5486 5487 def __showContextMenu(self): 5488 """ 5489 Private slot handling the aboutToShow signal of the context menu. 5490 """ 5491 self.menuActs["Reopen"].setEnabled( 5492 not self.isModified() and bool(self.fileName)) 5493 self.menuActs["Save"].setEnabled(self.isModified()) 5494 self.menuActs["Undo"].setEnabled(self.isUndoAvailable()) 5495 self.menuActs["Redo"].setEnabled(self.isRedoAvailable()) 5496 self.menuActs["Revert"].setEnabled(self.isModified()) 5497 self.menuActs["Cut"].setEnabled(self.hasSelectedText()) 5498 self.menuActs["Copy"].setEnabled(self.hasSelectedText()) 5499 if self.menuActs["ExecuteSelection"] is not None: 5500 self.menuActs["ExecuteSelection"].setEnabled( 5501 self.hasSelectedText()) 5502 self.menuActs["Paste"].setEnabled(self.canPaste()) 5503 if not self.isResourcesFile: 5504 if self.fileName and self.isPyFile(): 5505 self.menuActs["Show"].setEnabled(True) 5506 else: 5507 self.menuActs["Show"].setEnabled(False) 5508 if ( 5509 self.fileName and 5510 (self.isPyFile() or self.isRubyFile()) 5511 ): 5512 self.menuActs["Diagrams"].setEnabled(True) 5513 else: 5514 self.menuActs["Diagrams"].setEnabled(False) 5515 if not self.miniMenu: 5516 if self.lexer_ is not None: 5517 self.menuActs["Comment"].setEnabled( 5518 self.lexer_.canBlockComment()) 5519 self.menuActs["Uncomment"].setEnabled( 5520 self.lexer_.canBlockComment()) 5521 self.menuActs["StreamComment"].setEnabled( 5522 self.lexer_.canStreamComment()) 5523 self.menuActs["BoxComment"].setEnabled( 5524 self.lexer_.canBoxComment()) 5525 else: 5526 self.menuActs["Comment"].setEnabled(False) 5527 self.menuActs["Uncomment"].setEnabled(False) 5528 self.menuActs["StreamComment"].setEnabled(False) 5529 self.menuActs["BoxComment"].setEnabled(False) 5530 5531 cline = self.getCursorPosition()[0] 5532 line = self.text(cline) 5533 self.menuActs["Docstring"].setEnabled( 5534 self.getDocstringGenerator().isFunctionStart(line)) 5535 5536 self.menuActs["TypingAidsEnabled"].setEnabled( 5537 self.completer is not None) 5538 self.menuActs["TypingAidsEnabled"].setChecked( 5539 self.completer is not None and self.completer.isEnabled()) 5540 5541 if not self.isResourcesFile: 5542 self.menuActs["calltip"].setEnabled(self.canProvideCallTipps()) 5543 self.menuActs["codeInfo"].setEnabled( 5544 self.vm.isEditorInfoSupported(self.getLanguage())) 5545 5546 spellingAvailable = SpellChecker.isAvailable() 5547 self.menuActs["SpellCheck"].setEnabled(spellingAvailable) 5548 self.menuActs["SpellCheckSelection"].setEnabled( 5549 spellingAvailable and self.hasSelectedText()) 5550 self.menuActs["SpellCheckRemove"].setEnabled( 5551 spellingAvailable and self.spellingMenuPos >= 0) 5552 self.menuActs["SpellCheckLanguages"].setEnabled(spellingAvailable) 5553 5554 if self.menuActs["OpenRejections"]: 5555 if self.fileName: 5556 rej = "{0}.rej".format(self.fileName) 5557 self.menuActs["OpenRejections"].setEnabled(os.path.exists(rej)) 5558 else: 5559 self.menuActs["OpenRejections"].setEnabled(False) 5560 5561 self.menuActs["MonospacedFont"].setEnabled(self.lexer_ is None) 5562 5563 splitOrientation = self.vm.getSplitOrientation() 5564 if splitOrientation == Qt.Orientation.Horizontal: 5565 self.menuActs["NewSplit"].setIcon( 5566 UI.PixmapCache.getIcon("splitHorizontal")) 5567 else: 5568 self.menuActs["NewSplit"].setIcon( 5569 UI.PixmapCache.getIcon("splitVertical")) 5570 5571 self.menuActs["Tools"].setEnabled(not self.toolsMenu.isEmpty()) 5572 5573 self.showMenu.emit("Main", self.menu, self) 5574 5575 def __showContextMenuAutocompletion(self): 5576 """ 5577 Private slot called before the autocompletion menu is shown. 5578 """ 5579 self.menuActs["acDynamic"].setEnabled( 5580 self.canProvideDynamicAutoCompletion()) 5581 self.menuActs["acClearCache"].setEnabled( 5582 self.canProvideDynamicAutoCompletion()) 5583 self.menuActs["acAPI"].setEnabled(self.acAPI) 5584 self.menuActs["acAPIDocument"].setEnabled(self.acAPI) 5585 5586 self.showMenu.emit("Autocompletion", self.autocompletionMenu, self) 5587 5588 def __showContextMenuShow(self): 5589 """ 5590 Private slot called before the show menu is shown. 5591 """ 5592 prEnable = False 5593 coEnable = False 5594 5595 # first check if the file belongs to a project 5596 if ( 5597 self.project.isOpen() and 5598 self.project.isProjectSource(self.fileName) 5599 ): 5600 fn = self.project.getMainScript(True) 5601 if fn is not None: 5602 tfn = Utilities.getTestFileName(fn) 5603 basename = os.path.splitext(fn)[0] 5604 tbasename = os.path.splitext(tfn)[0] 5605 prEnable = ( 5606 prEnable or 5607 os.path.isfile("{0}.profile".format(basename)) or 5608 os.path.isfile("{0}.profile".format(tbasename)) 5609 ) 5610 coEnable = ( 5611 (coEnable or 5612 os.path.isfile("{0}.coverage".format(basename)) or 5613 os.path.isfile("{0}.coverage".format(tbasename))) and 5614 self.project.isPy3Project() 5615 ) 5616 5617 # now check ourselves 5618 fn = self.getFileName() 5619 if fn is not None: 5620 tfn = Utilities.getTestFileName(fn) 5621 basename = os.path.splitext(fn)[0] 5622 tbasename = os.path.splitext(tfn)[0] 5623 prEnable = ( 5624 prEnable or 5625 os.path.isfile("{0}.profile".format(basename)) or 5626 os.path.isfile("{0}.profile".format(tbasename)) 5627 ) 5628 coEnable = ( 5629 (coEnable or 5630 os.path.isfile("{0}.coverage".format(basename)) or 5631 os.path.isfile("{0}.coverage".format(tbasename))) and 5632 self.isPyFile() 5633 ) 5634 5635 # now check for syntax errors 5636 if self.hasSyntaxErrors(): 5637 coEnable = False 5638 5639 self.profileMenuAct.setEnabled(prEnable) 5640 self.coverageMenuAct.setEnabled(coEnable) 5641 self.coverageShowAnnotationMenuAct.setEnabled( 5642 coEnable and len(self.notcoveredMarkers) == 0) 5643 self.coverageHideAnnotationMenuAct.setEnabled( 5644 len(self.notcoveredMarkers) > 0) 5645 5646 self.showMenu.emit("Show", self.menuShow, self) 5647 5648 def __showContextMenuGraphics(self): 5649 """ 5650 Private slot handling the aboutToShow signal of the diagrams context 5651 menu. 5652 """ 5653 if ( 5654 self.project.isOpen() and 5655 self.project.isProjectSource(self.fileName) 5656 ): 5657 self.applicationDiagramMenuAct.setEnabled(True) 5658 else: 5659 self.applicationDiagramMenuAct.setEnabled(False) 5660 5661 self.showMenu.emit("Graphics", self.graphicsMenu, self) 5662 5663 def __showContextMenuMargin(self, menu): 5664 """ 5665 Private slot handling the aboutToShow signal of the margins context 5666 menu. 5667 5668 @param menu reference to the menu to be shown 5669 @type QMenu 5670 """ 5671 if menu is self.bpMarginMenu: 5672 supportsDebugger = bool(self.fileName and self.isPyFile()) 5673 hasBreakpoints = bool(self.breaks) 5674 hasBreakpoint = bool( 5675 self.markersAtLine(self.line) & self.breakpointMask) 5676 5677 self.marginMenuActs["Breakpoint"].setEnabled(supportsDebugger) 5678 self.marginMenuActs["TempBreakpoint"].setEnabled(supportsDebugger) 5679 self.marginMenuActs["NextBreakpoint"].setEnabled( 5680 supportsDebugger and hasBreakpoints) 5681 self.marginMenuActs["PreviousBreakpoint"].setEnabled( 5682 supportsDebugger and hasBreakpoints) 5683 self.marginMenuActs["ClearBreakpoint"].setEnabled( 5684 supportsDebugger and hasBreakpoints) 5685 self.marginMenuActs["EditBreakpoint"].setEnabled( 5686 supportsDebugger and hasBreakpoint) 5687 self.marginMenuActs["EnableBreakpoint"].setEnabled( 5688 supportsDebugger and hasBreakpoint) 5689 if supportsDebugger: 5690 if self.markersAtLine(self.line) & (1 << self.dbreakpoint): 5691 self.marginMenuActs["EnableBreakpoint"].setText( 5692 self.tr('Enable breakpoint')) 5693 else: 5694 self.marginMenuActs["EnableBreakpoint"].setText( 5695 self.tr('Disable breakpoint')) 5696 5697 if menu is self.bmMarginMenu: 5698 hasBookmarks = bool(self.bookmarks) 5699 5700 self.marginMenuActs["NextBookmark"].setEnabled(hasBookmarks) 5701 self.marginMenuActs["PreviousBookmark"].setEnabled(hasBookmarks) 5702 self.marginMenuActs["ClearBookmark"].setEnabled(hasBookmarks) 5703 5704 if menu is self.foldMarginMenu: 5705 isFoldHeader = bool(self.SendScintilla( 5706 QsciScintilla.SCI_GETFOLDLEVEL, self.line) & 5707 QsciScintilla.SC_FOLDLEVELHEADERFLAG) 5708 5709 self.marginMenuActs["ExpandChildren"].setEnabled(isFoldHeader) 5710 self.marginMenuActs["CollapseChildren"].setEnabled(isFoldHeader) 5711 5712 if menu is self.indicMarginMenu: 5713 hasSyntaxErrors = bool(self.syntaxerrors) 5714 hasWarnings = bool(self.warnings) 5715 hasNotCoveredMarkers = bool(self.notcoveredMarkers) 5716 5717 self.marginMenuActs["GotoSyntaxError"].setEnabled(hasSyntaxErrors) 5718 self.marginMenuActs["ClearSyntaxError"].setEnabled(hasSyntaxErrors) 5719 if ( 5720 hasSyntaxErrors and 5721 self.markersAtLine(self.line) & (1 << self.syntaxerror) 5722 ): 5723 self.marginMenuActs["ShowSyntaxError"].setEnabled(True) 5724 else: 5725 self.marginMenuActs["ShowSyntaxError"].setEnabled(False) 5726 5727 self.marginMenuActs["NextWarningMarker"].setEnabled(hasWarnings) 5728 self.marginMenuActs["PreviousWarningMarker"].setEnabled( 5729 hasWarnings) 5730 self.marginMenuActs["ClearWarnings"].setEnabled(hasWarnings) 5731 if ( 5732 hasWarnings and 5733 self.markersAtLine(self.line) & (1 << self.warning) 5734 ): 5735 self.marginMenuActs["ShowWarning"].setEnabled(True) 5736 else: 5737 self.marginMenuActs["ShowWarning"].setEnabled(False) 5738 5739 self.marginMenuActs["NextCoverageMarker"].setEnabled( 5740 hasNotCoveredMarkers) 5741 self.marginMenuActs["PreviousCoverageMarker"].setEnabled( 5742 hasNotCoveredMarkers) 5743 5744 self.marginMenuActs["PreviousTaskMarker"].setEnabled( 5745 self.__hasTaskMarkers) 5746 self.marginMenuActs["NextTaskMarker"].setEnabled( 5747 self.__hasTaskMarkers) 5748 5749 self.marginMenuActs["PreviousChangeMarker"].setEnabled( 5750 self.__hasChangeMarkers) 5751 self.marginMenuActs["NextChangeMarker"].setEnabled( 5752 self.__hasChangeMarkers) 5753 self.marginMenuActs["ClearChangeMarkers"].setEnabled( 5754 self.__hasChangeMarkers) 5755 5756 self.showMenu.emit("Margin", menu, self) 5757 5758 def __showContextMenuChecks(self): 5759 """ 5760 Private slot handling the aboutToShow signal of the checks context 5761 menu. 5762 """ 5763 self.showMenu.emit("Checks", self.checksMenu, self) 5764 5765 def __showContextMenuTools(self): 5766 """ 5767 Private slot handling the aboutToShow signal of the tools context 5768 menu. 5769 """ 5770 self.showMenu.emit("Tools", self.toolsMenu, self) 5771 5772 def __reopenWithEncodingMenuTriggered(self, act): 5773 """ 5774 Private method to handle the rereading of the file with a selected 5775 encoding. 5776 5777 @param act reference to the action that was triggered (QAction) 5778 """ 5779 encoding = act.data() 5780 self.readFile(self.fileName, encoding=encoding) 5781 self.__convertTabs() 5782 self.__checkEncoding() 5783 5784 def __contextSave(self): 5785 """ 5786 Private slot handling the save context menu entry. 5787 """ 5788 ok = self.saveFile() 5789 if ok: 5790 self.vm.setEditorName(self, self.fileName) 5791 5792 def __contextSaveAs(self): 5793 """ 5794 Private slot handling the save as context menu entry. 5795 """ 5796 ok = self.saveFileAs() 5797 if ok: 5798 self.vm.setEditorName(self, self.fileName) 5799 5800 def __contextSaveCopy(self): 5801 """ 5802 Private slot handling the save copy context menu entry. 5803 """ 5804 self.saveFileCopy() 5805 5806 def __contextClose(self): 5807 """ 5808 Private slot handling the close context menu entry. 5809 """ 5810 self.vm.closeEditor(self) 5811 5812 def __contextOpenRejections(self): 5813 """ 5814 Private slot handling the open rejections file context menu entry. 5815 """ 5816 if self.fileName: 5817 rej = "{0}.rej".format(self.fileName) 5818 if os.path.exists(rej): 5819 self.vm.openSourceFile(rej) 5820 5821 def __newView(self): 5822 """ 5823 Private slot to create a new view to an open document. 5824 """ 5825 self.vm.newEditorView(self.fileName, self, self.filetype) 5826 5827 def __newViewNewSplit(self): 5828 """ 5829 Private slot to create a new view to an open document. 5830 """ 5831 self.vm.addSplit() 5832 self.vm.newEditorView(self.fileName, self, self.filetype) 5833 5834 def __selectAll(self): 5835 """ 5836 Private slot handling the select all context menu action. 5837 """ 5838 self.selectAll(True) 5839 5840 def __deselectAll(self): 5841 """ 5842 Private slot handling the deselect all context menu action. 5843 """ 5844 self.selectAll(False) 5845 5846 def joinLines(self): 5847 """ 5848 Public slot to join the current line with the next one. 5849 """ 5850 curLine = self.getCursorPosition()[0] 5851 if curLine == self.lines() - 1: 5852 return 5853 5854 line0Text = self.text(curLine) 5855 line1Text = self.text(curLine + 1) 5856 if line1Text in ["", "\r", "\n", "\r\n"]: 5857 return 5858 5859 if ( 5860 line0Text.rstrip("\r\n\\ \t").endswith(("'", '"')) and 5861 line1Text.lstrip().startswith(("'", '"')) 5862 ): 5863 # merging multi line strings 5864 startChars = "\r\n\\ \t'\"" 5865 endChars = " \t'\"" 5866 else: 5867 startChars = "\r\n\\ \t" 5868 endChars = " \t" 5869 5870 # determine start index 5871 startIndex = len(line0Text) 5872 while startIndex > 0 and line0Text[startIndex - 1] in startChars: 5873 startIndex -= 1 5874 if startIndex == 0: 5875 return 5876 5877 # determine end index 5878 endIndex = 0 5879 while line1Text[endIndex] in endChars: 5880 endIndex += 1 5881 5882 self.setSelection(curLine, startIndex, curLine + 1, endIndex) 5883 self.beginUndoAction() 5884 self.removeSelectedText() 5885 self.insertAt(" ", curLine, startIndex) 5886 self.endUndoAction() 5887 5888 def shortenEmptyLines(self): 5889 """ 5890 Public slot to compress lines consisting solely of whitespace 5891 characters. 5892 """ 5893 searchRE = r"^[ \t]+$" 5894 5895 ok = self.findFirstTarget(searchRE, True, False, False, 0, 0) 5896 self.beginUndoAction() 5897 while ok: 5898 self.replaceTarget("") 5899 ok = self.findNextTarget() 5900 self.endUndoAction() 5901 5902 def __autosaveEnable(self): 5903 """ 5904 Private slot handling the autosave enable context menu action. 5905 """ 5906 if self.menuActs["AutosaveEnable"].isChecked(): 5907 self.autosaveManuallyDisabled = False 5908 else: 5909 self.autosaveManuallyDisabled = True 5910 5911 def shouldAutosave(self): 5912 """ 5913 Public slot to check the autosave flags. 5914 5915 @return flag indicating this editor should be saved (boolean) 5916 """ 5917 return ( 5918 bool(self.fileName) and 5919 not self.autosaveManuallyDisabled and 5920 not self.isReadOnly() 5921 ) 5922 5923 def checkSyntax(self): 5924 """ 5925 Public method to perform an automatic syntax check of the file. 5926 """ 5927 fileType = self.filetype 5928 if fileType == "MicroPython": 5929 # adjustment for MicroPython 5930 fileType = "Python3" 5931 5932 if ( 5933 self.syntaxCheckService is None or 5934 fileType not in self.syntaxCheckService.getLanguages() 5935 ): 5936 return 5937 5938 if Preferences.getEditor("AutoCheckSyntax"): 5939 if Preferences.getEditor("OnlineSyntaxCheck"): 5940 self.__onlineSyntaxCheckTimer.stop() 5941 5942 self.syntaxCheckService.syntaxCheck( 5943 fileType, self.fileName or "(Unnamed)", self.text()) 5944 5945 def __processSyntaxCheckError(self, fn, msg): 5946 """ 5947 Private slot to report an error message of a syntax check. 5948 5949 @param fn filename of the file 5950 @type str 5951 @param msg error message 5952 @type str 5953 """ 5954 if fn != self.fileName and ( 5955 bool(self.fileName) or fn != "(Unnamed)"): 5956 return 5957 5958 self.clearSyntaxError() 5959 self.clearFlakesWarnings() 5960 5961 self.toggleWarning(0, 0, True, msg) 5962 5963 self.updateVerticalScrollBar() 5964 5965 def __processSyntaxCheckResult(self, fn, problems): 5966 """ 5967 Private slot to report the resulting messages of a syntax check. 5968 5969 @param fn filename of the checked file (str) 5970 @param problems dictionary with the keys 'error' and 'warnings' which 5971 hold a list containing details about the error/ warnings 5972 (file name, line number, column, codestring (only at syntax 5973 errors), the message) (dict) 5974 """ 5975 # Check if it's the requested file, otherwise ignore signal 5976 if fn != self.fileName and ( 5977 bool(self.fileName) or fn != "(Unnamed)"): 5978 return 5979 5980 self.clearSyntaxError() 5981 self.clearFlakesWarnings() 5982 5983 error = problems.get('error') 5984 if error: 5985 _fn, lineno, col, code, msg = error 5986 self.toggleSyntaxError(lineno, col, True, msg) 5987 5988 warnings = problems.get('warnings', []) 5989 for _fn, lineno, col, _code, msg in warnings: 5990 self.toggleWarning(lineno, col, True, msg) 5991 5992 self.updateVerticalScrollBar() 5993 5994 def __initOnlineSyntaxCheck(self): 5995 """ 5996 Private slot to initialize the online syntax check. 5997 """ 5998 self.__onlineSyntaxCheckTimer = QTimer(self) 5999 self.__onlineSyntaxCheckTimer.setSingleShot(True) 6000 self.__onlineSyntaxCheckTimer.setInterval( 6001 Preferences.getEditor("OnlineSyntaxCheckInterval") * 1000) 6002 self.__onlineSyntaxCheckTimer.timeout.connect(self.checkSyntax) 6003 self.textChanged.connect(self.__resetOnlineSyntaxCheckTimer) 6004 6005 def __resetOnlineSyntaxCheckTimer(self): 6006 """ 6007 Private method to reset the online syntax check timer. 6008 """ 6009 if Preferences.getEditor("OnlineSyntaxCheck"): 6010 self.__onlineSyntaxCheckTimer.stop() 6011 self.__onlineSyntaxCheckTimer.start() 6012 6013 def __showCodeMetrics(self): 6014 """ 6015 Private method to handle the code metrics context menu action. 6016 """ 6017 if not self.checkDirty(): 6018 return 6019 6020 from DataViews.CodeMetricsDialog import CodeMetricsDialog 6021 self.codemetrics = CodeMetricsDialog() 6022 self.codemetrics.show() 6023 self.codemetrics.start(self.fileName) 6024 6025 def __getCodeCoverageFile(self): 6026 """ 6027 Private method to get the file name of the file containing coverage 6028 info. 6029 6030 @return file name of the coverage file (string) 6031 """ 6032 files = [] 6033 6034 # first check if the file belongs to a project and there is 6035 # a project coverage file 6036 if ( 6037 self.project.isOpen() and 6038 self.project.isProjectSource(self.fileName) 6039 ): 6040 fn = self.project.getMainScript(True) 6041 if fn is not None: 6042 tfn = Utilities.getTestFileName(fn) 6043 basename = os.path.splitext(fn)[0] 6044 tbasename = os.path.splitext(tfn)[0] 6045 6046 f = "{0}.coverage".format(basename) 6047 tf = "{0}.coverage".format(tbasename) 6048 if os.path.isfile(f): 6049 files.append(f) 6050 if os.path.isfile(tf): 6051 files.append(tf) 6052 6053 # now check, if there are coverage files belonging to ourselves 6054 fn = self.getFileName() 6055 if fn is not None: 6056 tfn = Utilities.getTestFileName(fn) 6057 basename = os.path.splitext(fn)[0] 6058 tbasename = os.path.splitext(tfn)[0] 6059 6060 f = "{0}.coverage".format(basename) 6061 tf = "{0}.coverage".format(tbasename) 6062 if os.path.isfile(f) and f not in files: 6063 files.append(f) 6064 if os.path.isfile(tf) and tf not in files: 6065 files.append(tf) 6066 6067 if files: 6068 if len(files) > 1: 6069 fn, ok = QInputDialog.getItem( 6070 self, 6071 self.tr("Code Coverage"), 6072 self.tr("Please select a coverage file"), 6073 files, 6074 0, False) 6075 if not ok: 6076 return "" 6077 else: 6078 fn = files[0] 6079 else: 6080 fn = None 6081 6082 return fn 6083 6084 def __showCodeCoverage(self): 6085 """ 6086 Private method to handle the code coverage context menu action. 6087 """ 6088 fn = self.__getCodeCoverageFile() 6089 if fn: 6090 from DataViews.PyCoverageDialog import PyCoverageDialog 6091 self.codecoverage = PyCoverageDialog() 6092 self.codecoverage.show() 6093 self.codecoverage.start(fn, self.fileName) 6094 6095 def refreshCoverageAnnotations(self): 6096 """ 6097 Public method to refresh the code coverage annotations. 6098 """ 6099 if self.showingNotcoveredMarkers: 6100 self.codeCoverageShowAnnotations(silent=True) 6101 6102 def codeCoverageShowAnnotations(self, silent=False): 6103 """ 6104 Public method to handle the show code coverage annotations context 6105 menu action. 6106 6107 @param silent flag indicating to not show any dialog (boolean) 6108 """ 6109 self.__codeCoverageHideAnnotations() 6110 6111 fn = self.__getCodeCoverageFile() 6112 if fn: 6113 from coverage import Coverage 6114 cover = Coverage(data_file=fn) 6115 cover.load() 6116 missing = cover.analysis2(self.fileName)[3] 6117 if missing: 6118 for line in missing: 6119 handle = self.markerAdd(line - 1, self.notcovered) 6120 self.notcoveredMarkers.append(handle) 6121 self.coverageMarkersShown.emit(True) 6122 self.__markerMap.update() 6123 else: 6124 if not silent: 6125 E5MessageBox.information( 6126 self, 6127 self.tr("Show Code Coverage Annotations"), 6128 self.tr("""All lines have been covered.""")) 6129 self.showingNotcoveredMarkers = True 6130 else: 6131 if not silent: 6132 E5MessageBox.warning( 6133 self, 6134 self.tr("Show Code Coverage Annotations"), 6135 self.tr("""There is no coverage file available.""")) 6136 6137 def __codeCoverageHideAnnotations(self): 6138 """ 6139 Private method to handle the hide code coverage annotations context 6140 menu action. 6141 """ 6142 for handle in self.notcoveredMarkers: 6143 self.markerDeleteHandle(handle) 6144 self.notcoveredMarkers.clear() 6145 self.coverageMarkersShown.emit(False) 6146 self.showingNotcoveredMarkers = False 6147 self.__markerMap.update() 6148 6149 def getCoverageLines(self): 6150 """ 6151 Public method to get the lines containing a coverage marker. 6152 6153 @return list of lines containing a coverage marker (list of integer) 6154 """ 6155 lines = [] 6156 line = -1 6157 while True: 6158 line = self.markerFindNext(line + 1, 1 << self.notcovered) 6159 if line < 0: 6160 break 6161 else: 6162 lines.append(line) 6163 return lines 6164 6165 def hasCoverageMarkers(self): 6166 """ 6167 Public method to test, if there are coverage markers. 6168 6169 @return flag indicating the presence of coverage markers (boolean) 6170 """ 6171 return len(self.notcoveredMarkers) > 0 6172 6173 def nextUncovered(self): 6174 """ 6175 Public slot to handle the 'Next uncovered' context menu action. 6176 """ 6177 line, index = self.getCursorPosition() 6178 if line == self.lines() - 1: 6179 line = 0 6180 else: 6181 line += 1 6182 ucline = self.markerFindNext(line, 1 << self.notcovered) 6183 if ucline < 0: 6184 # wrap around 6185 ucline = self.markerFindNext(0, 1 << self.notcovered) 6186 if ucline >= 0: 6187 self.setCursorPosition(ucline, 0) 6188 self.ensureLineVisible(ucline) 6189 6190 def previousUncovered(self): 6191 """ 6192 Public slot to handle the 'Previous uncovered' context menu action. 6193 """ 6194 line, index = self.getCursorPosition() 6195 if line == 0: 6196 line = self.lines() - 1 6197 else: 6198 line -= 1 6199 ucline = self.markerFindPrevious(line, 1 << self.notcovered) 6200 if ucline < 0: 6201 # wrap around 6202 ucline = self.markerFindPrevious( 6203 self.lines() - 1, 1 << self.notcovered) 6204 if ucline >= 0: 6205 self.setCursorPosition(ucline, 0) 6206 self.ensureLineVisible(ucline) 6207 6208 def __showProfileData(self): 6209 """ 6210 Private method to handle the show profile data context menu action. 6211 """ 6212 files = [] 6213 6214 # first check if the file belongs to a project and there is 6215 # a project profile file 6216 if ( 6217 self.project.isOpen() and 6218 self.project.isProjectSource(self.fileName) 6219 ): 6220 fn = self.project.getMainScript(True) 6221 if fn is not None: 6222 tfn = Utilities.getTestFileName(fn) 6223 basename = os.path.splitext(fn)[0] 6224 tbasename = os.path.splitext(tfn)[0] 6225 6226 f = "{0}.profile".format(basename) 6227 tf = "{0}.profile".format(tbasename) 6228 if os.path.isfile(f): 6229 files.append(f) 6230 if os.path.isfile(tf): 6231 files.append(tf) 6232 6233 # now check, if there are profile files belonging to ourselves 6234 fn = self.getFileName() 6235 if fn is not None: 6236 tfn = Utilities.getTestFileName(fn) 6237 basename = os.path.splitext(fn)[0] 6238 tbasename = os.path.splitext(tfn)[0] 6239 6240 f = "{0}.profile".format(basename) 6241 tf = "{0}.profile".format(tbasename) 6242 if os.path.isfile(f) and f not in files: 6243 files.append(f) 6244 if os.path.isfile(tf) and tf not in files: 6245 files.append(tf) 6246 6247 if files: 6248 if len(files) > 1: 6249 fn, ok = QInputDialog.getItem( 6250 self, 6251 self.tr("Profile Data"), 6252 self.tr("Please select a profile file"), 6253 files, 6254 0, False) 6255 if not ok: 6256 return 6257 else: 6258 fn = files[0] 6259 else: 6260 return 6261 6262 from DataViews.PyProfileDialog import PyProfileDialog 6263 self.profiledata = PyProfileDialog() 6264 self.profiledata.show() 6265 self.profiledata.start(fn, self.fileName) 6266 6267 def __lmBbookmarks(self): 6268 """ 6269 Private method to handle the 'LMB toggles bookmark' context menu 6270 action. 6271 """ 6272 self.marginMenuActs["LMBbookmarks"].setChecked(True) 6273 self.marginMenuActs["LMBbreakpoints"].setChecked(False) 6274 6275 def __lmBbreakpoints(self): 6276 """ 6277 Private method to handle the 'LMB toggles breakpoint' context menu 6278 action. 6279 """ 6280 self.marginMenuActs["LMBbookmarks"].setChecked(True) 6281 self.marginMenuActs["LMBbreakpoints"].setChecked(False) 6282 6283 ########################################################################### 6284 ## Syntax error handling methods below 6285 ########################################################################### 6286 6287 def toggleSyntaxError(self, line, index, error, msg="", show=False): 6288 """ 6289 Public method to toggle a syntax error indicator. 6290 6291 @param line line number of the syntax error (integer) 6292 @param index index number of the syntax error (integer) 6293 @param error flag indicating if the error marker should be 6294 set or deleted (boolean) 6295 @param msg error message (string) 6296 @param show flag indicating to set the cursor to the error position 6297 (boolean) 6298 """ 6299 if line == 0: 6300 line = 1 6301 # hack to show a syntax error marker, if line is reported to be 0 6302 if error: 6303 # set a new syntax error marker 6304 markers = self.markersAtLine(line - 1) 6305 index += self.indentation(line - 1) 6306 if not (markers & (1 << self.syntaxerror)): 6307 handle = self.markerAdd(line - 1, self.syntaxerror) 6308 self.syntaxerrors[handle] = [(msg, index)] 6309 self.syntaxerrorToggled.emit(self) 6310 else: 6311 for handle in list(self.syntaxerrors.keys()): 6312 if ( 6313 self.markerLine(handle) == line - 1 and 6314 (msg, index) not in self.syntaxerrors[handle] 6315 ): 6316 self.syntaxerrors[handle].append((msg, index)) 6317 if show: 6318 self.setCursorPosition(line - 1, index) 6319 self.ensureLineVisible(line - 1) 6320 else: 6321 for handle in list(self.syntaxerrors.keys()): 6322 if self.markerLine(handle) == line - 1: 6323 del self.syntaxerrors[handle] 6324 self.markerDeleteHandle(handle) 6325 self.syntaxerrorToggled.emit(self) 6326 6327 self.__setAnnotation(line - 1) 6328 self.__markerMap.update() 6329 6330 def getSyntaxErrors(self): 6331 """ 6332 Public method to retrieve the syntax error markers. 6333 6334 @return sorted list of all lines containing a syntax error 6335 (list of integer) 6336 """ 6337 selist = [] 6338 for handle in list(self.syntaxerrors.keys()): 6339 selist.append(self.markerLine(handle) + 1) 6340 6341 selist.sort() 6342 return selist 6343 6344 def getSyntaxErrorLines(self): 6345 """ 6346 Public method to get the lines containing a syntax error. 6347 6348 @return list of lines containing a syntax error (list of integer) 6349 """ 6350 lines = [] 6351 line = -1 6352 while True: 6353 line = self.markerFindNext(line + 1, 1 << self.syntaxerror) 6354 if line < 0: 6355 break 6356 else: 6357 lines.append(line) 6358 return lines 6359 6360 def hasSyntaxErrors(self): 6361 """ 6362 Public method to check for the presence of syntax errors. 6363 6364 @return flag indicating the presence of syntax errors (boolean) 6365 """ 6366 return len(self.syntaxerrors) > 0 6367 6368 def gotoSyntaxError(self): 6369 """ 6370 Public slot to handle the 'Goto syntax error' context menu action. 6371 """ 6372 seline = self.markerFindNext(0, 1 << self.syntaxerror) 6373 if seline >= 0: 6374 index = 0 6375 for handle in self.syntaxerrors: 6376 if self.markerLine(handle) == seline: 6377 index = self.syntaxerrors[handle][0][1] 6378 self.setCursorPosition(seline, index) 6379 self.ensureLineVisible(seline) 6380 6381 def clearSyntaxError(self): 6382 """ 6383 Public slot to handle the 'Clear all syntax error' context menu action. 6384 """ 6385 for handle in list(self.syntaxerrors.keys()): 6386 line = self.markerLine(handle) + 1 6387 self.toggleSyntaxError(line, 0, False) 6388 6389 self.syntaxerrors.clear() 6390 self.syntaxerrorToggled.emit(self) 6391 6392 def __showSyntaxError(self, line=-1): 6393 """ 6394 Private slot to handle the 'Show syntax error message' 6395 context menu action. 6396 6397 @param line line number to show the syntax error for (integer) 6398 """ 6399 if line == -1: 6400 line = self.line 6401 6402 for handle in list(self.syntaxerrors.keys()): 6403 if self.markerLine(handle) == line: 6404 errors = [e[0] for e in self.syntaxerrors[handle]] 6405 E5MessageBox.critical( 6406 self, 6407 self.tr("Syntax Error"), 6408 "\n".join(errors)) 6409 break 6410 else: 6411 E5MessageBox.critical( 6412 self, 6413 self.tr("Syntax Error"), 6414 self.tr("No syntax error message available.")) 6415 6416 ########################################################################### 6417 ## VCS conflict marker handling methods below 6418 ########################################################################### 6419 6420 def getVcsConflictMarkerLines(self): 6421 """ 6422 Public method to determine the lines containing a VCS conflict marker. 6423 6424 @return list of line numbers containg a VCS conflict marker 6425 @rtype list of int 6426 """ 6427 conflictMarkerLines = [] 6428 6429 regExp = re.compile("|".join(Editor.VcsConflictMarkerLineRegExpList), 6430 re.MULTILINE) 6431 matches = [m for m in regExp.finditer(self.text())] 6432 for match in matches: 6433 line, _ = self.lineIndexFromPosition(match.start()) 6434 conflictMarkerLines.append(line) 6435 6436 return conflictMarkerLines 6437 6438 ########################################################################### 6439 ## Warning handling methods below 6440 ########################################################################### 6441 6442 def toggleWarning( 6443 self, line, col, warning, msg="", warningType=WarningCode): 6444 """ 6445 Public method to toggle a warning indicator. 6446 6447 Note: This method is used to set pyflakes and code style warnings. 6448 6449 @param line line number of the warning 6450 @param col column of the warning 6451 @param warning flag indicating if the warning marker should be 6452 set or deleted (boolean) 6453 @param msg warning message (string) 6454 @param warningType type of warning message (integer) 6455 """ 6456 if line == 0: 6457 line = 1 6458 # hack to show a warning marker, if line is reported to be 0 6459 if warning: 6460 # set/amend a new warning marker 6461 warn = (msg, warningType) 6462 markers = self.markersAtLine(line - 1) 6463 if not (markers & (1 << self.warning)): 6464 handle = self.markerAdd(line - 1, self.warning) 6465 self.warnings[handle] = [warn] 6466 self.syntaxerrorToggled.emit(self) 6467 else: 6468 for handle in list(self.warnings.keys()): 6469 if ( 6470 self.markerLine(handle) == line - 1 and 6471 warn not in self.warnings[handle] 6472 ): 6473 self.warnings[handle].append(warn) 6474 else: 6475 for handle in list(self.warnings.keys()): 6476 if self.markerLine(handle) == line - 1: 6477 del self.warnings[handle] 6478 self.markerDeleteHandle(handle) 6479 self.syntaxerrorToggled.emit(self) 6480 6481 self.__setAnnotation(line - 1) 6482 self.__markerMap.update() 6483 6484 def getWarnings(self): 6485 """ 6486 Public method to retrieve the warning markers. 6487 6488 @return sorted list of all lines containing a warning 6489 (list of integer) 6490 """ 6491 fwlist = [] 6492 for handle in list(self.warnings.keys()): 6493 fwlist.append(self.markerLine(handle) + 1) 6494 6495 fwlist.sort() 6496 return fwlist 6497 6498 def getWarningLines(self): 6499 """ 6500 Public method to get the lines containing a warning. 6501 6502 @return list of lines containing a warning (list of integer) 6503 """ 6504 lines = [] 6505 line = -1 6506 while True: 6507 line = self.markerFindNext(line + 1, 1 << self.warning) 6508 if line < 0: 6509 break 6510 else: 6511 lines.append(line) 6512 return lines 6513 6514 def hasWarnings(self): 6515 """ 6516 Public method to check for the presence of warnings. 6517 6518 @return flag indicating the presence of warnings (boolean) 6519 """ 6520 return len(self.warnings) > 0 6521 6522 def nextWarning(self): 6523 """ 6524 Public slot to handle the 'Next warning' context menu action. 6525 """ 6526 line, index = self.getCursorPosition() 6527 if line == self.lines() - 1: 6528 line = 0 6529 else: 6530 line += 1 6531 fwline = self.markerFindNext(line, 1 << self.warning) 6532 if fwline < 0: 6533 # wrap around 6534 fwline = self.markerFindNext(0, 1 << self.warning) 6535 if fwline >= 0: 6536 self.setCursorPosition(fwline, 0) 6537 self.ensureLineVisible(fwline) 6538 6539 def previousWarning(self): 6540 """ 6541 Public slot to handle the 'Previous warning' context menu action. 6542 """ 6543 line, index = self.getCursorPosition() 6544 if line == 0: 6545 line = self.lines() - 1 6546 else: 6547 line -= 1 6548 fwline = self.markerFindPrevious(line, 1 << self.warning) 6549 if fwline < 0: 6550 # wrap around 6551 fwline = self.markerFindPrevious( 6552 self.lines() - 1, 1 << self.warning) 6553 if fwline >= 0: 6554 self.setCursorPosition(fwline, 0) 6555 self.ensureLineVisible(fwline) 6556 6557 def clearFlakesWarnings(self): 6558 """ 6559 Public slot to clear all pyflakes warnings. 6560 """ 6561 self.__clearTypedWarning(Editor.WarningCode) 6562 6563 def clearStyleWarnings(self): 6564 """ 6565 Public slot to clear all style warnings. 6566 """ 6567 self.__clearTypedWarning(Editor.WarningStyle) 6568 6569 def __clearTypedWarning(self, warningKind): 6570 """ 6571 Private method to clear warnings of a specific kind. 6572 6573 @param warningKind kind of warning to clear (Editor.WarningCode, 6574 Editor.WarningStyle) 6575 """ 6576 for handle in list(self.warnings.keys()): 6577 warnings = [] 6578 for msg, warningType in self.warnings[handle]: 6579 if warningType == warningKind: 6580 continue 6581 6582 warnings.append((msg, warningType)) 6583 6584 if warnings: 6585 self.warnings[handle] = warnings 6586 self.__setAnnotation(self.markerLine(handle)) 6587 else: 6588 del self.warnings[handle] 6589 self.__setAnnotation(self.markerLine(handle)) 6590 self.markerDeleteHandle(handle) 6591 self.syntaxerrorToggled.emit(self) 6592 self.__markerMap.update() 6593 6594 def clearWarnings(self): 6595 """ 6596 Public slot to clear all warnings. 6597 """ 6598 for handle in self.warnings: 6599 self.warnings[handle] = [] 6600 self.__setAnnotation(self.markerLine(handle)) 6601 self.markerDeleteHandle(handle) 6602 self.warnings.clear() 6603 self.syntaxerrorToggled.emit(self) 6604 self.__markerMap.update() 6605 6606 def __showWarning(self, line=-1): 6607 """ 6608 Private slot to handle the 'Show warning' context menu action. 6609 6610 @param line line number to show the warning for (integer) 6611 """ 6612 if line == -1: 6613 line = self.line 6614 6615 for handle in list(self.warnings.keys()): 6616 if self.markerLine(handle) == line: 6617 E5MessageBox.warning( 6618 self, 6619 self.tr("Warning"), 6620 '\n'.join([w[0] for w in self.warnings[handle]])) 6621 break 6622 else: 6623 E5MessageBox.warning( 6624 self, 6625 self.tr("Warning"), 6626 self.tr("No warning messages available.")) 6627 6628 ########################################################################### 6629 ## Annotation handling methods below 6630 ########################################################################### 6631 6632 def __setAnnotationStyles(self): 6633 """ 6634 Private slot to define the style used by inline annotations. 6635 """ 6636 if hasattr(QsciScintilla, "annotate"): 6637 self.annotationWarningStyle = ( 6638 QsciScintilla.STYLE_LASTPREDEFINED + 1 6639 ) 6640 self.SendScintilla( 6641 QsciScintilla.SCI_STYLESETFORE, 6642 self.annotationWarningStyle, 6643 Preferences.getEditorColour("AnnotationsWarningForeground")) 6644 self.SendScintilla( 6645 QsciScintilla.SCI_STYLESETBACK, 6646 self.annotationWarningStyle, 6647 Preferences.getEditorColour("AnnotationsWarningBackground")) 6648 6649 self.annotationErrorStyle = self.annotationWarningStyle + 1 6650 self.SendScintilla( 6651 QsciScintilla.SCI_STYLESETFORE, 6652 self.annotationErrorStyle, 6653 Preferences.getEditorColour("AnnotationsErrorForeground")) 6654 self.SendScintilla( 6655 QsciScintilla.SCI_STYLESETBACK, 6656 self.annotationErrorStyle, 6657 Preferences.getEditorColour("AnnotationsErrorBackground")) 6658 6659 self.annotationStyleStyle = self.annotationErrorStyle + 1 6660 self.SendScintilla( 6661 QsciScintilla.SCI_STYLESETFORE, 6662 self.annotationStyleStyle, 6663 Preferences.getEditorColour("AnnotationsStyleForeground")) 6664 self.SendScintilla( 6665 QsciScintilla.SCI_STYLESETBACK, 6666 self.annotationStyleStyle, 6667 Preferences.getEditorColour("AnnotationsStyleBackground")) 6668 6669 def __setAnnotation(self, line): 6670 """ 6671 Private method to set the annotations for the given line. 6672 6673 @param line number of the line that needs annotation (integer) 6674 """ 6675 if hasattr(QsciScintilla, "annotate"): 6676 warningAnnotations = [] 6677 errorAnnotations = [] 6678 styleAnnotations = [] 6679 6680 # step 1: do warnings 6681 for handle in self.warnings: 6682 if self.markerLine(handle) == line: 6683 for msg, warningType in self.warnings[handle]: 6684 if warningType == self.WarningStyle: 6685 styleAnnotations.append( 6686 self.tr("Style: {0}").format(msg)) 6687 else: 6688 warningAnnotations.append( 6689 self.tr("Warning: {0}").format(msg)) 6690 6691 # step 2: do syntax errors 6692 for handle in self.syntaxerrors: 6693 if self.markerLine(handle) == line: 6694 for msg, _ in self.syntaxerrors[handle]: 6695 errorAnnotations.append( 6696 self.tr("Error: {0}").format(msg)) 6697 6698 annotations = [] 6699 if styleAnnotations: 6700 annotationStyleTxt = "\n".join(styleAnnotations) 6701 if warningAnnotations or errorAnnotations: 6702 annotationStyleTxt += "\n" 6703 annotations.append(QsciStyledText( 6704 annotationStyleTxt, self.annotationStyleStyle)) 6705 6706 if warningAnnotations: 6707 annotationWarningTxt = "\n".join(warningAnnotations) 6708 if errorAnnotations: 6709 annotationWarningTxt += "\n" 6710 annotations.append(QsciStyledText( 6711 annotationWarningTxt, self.annotationWarningStyle)) 6712 6713 if errorAnnotations: 6714 annotationErrorTxt = "\n".join(errorAnnotations) 6715 annotations.append(QsciStyledText( 6716 annotationErrorTxt, self.annotationErrorStyle)) 6717 6718 if annotations: 6719 self.annotate(line, annotations) 6720 else: 6721 self.clearAnnotations(line) 6722 6723 def __refreshAnnotations(self): 6724 """ 6725 Private method to refresh the annotations. 6726 """ 6727 if hasattr(QsciScintilla, "annotate"): 6728 self.clearAnnotations() 6729 for handle in ( 6730 list(self.warnings.keys()) + 6731 list(self.syntaxerrors.keys()) 6732 ): 6733 line = self.markerLine(handle) 6734 self.__setAnnotation(line) 6735 6736 ################################################################# 6737 ## Fold handling methods 6738 ################################################################# 6739 6740 def toggleCurrentFold(self): 6741 """ 6742 Public slot to toggle the fold containing the current line. 6743 """ 6744 line, index = self.getCursorPosition() 6745 self.foldLine(line) 6746 6747 def expandFoldWithChildren(self, line=-1): 6748 """ 6749 Public slot to expand the current fold including its children. 6750 6751 @param line number of line to be expanded 6752 @type int 6753 """ 6754 if line == -1: 6755 line, index = self.getCursorPosition() 6756 6757 self.SendScintilla(QsciScintilla.SCI_FOLDCHILDREN, line, 6758 QsciScintilla.SC_FOLDACTION_EXPAND) 6759 6760 def collapseFoldWithChildren(self, line=-1): 6761 """ 6762 Public slot to collapse the current fold including its children. 6763 6764 @param line number of line to be expanded 6765 @type int 6766 """ 6767 if line == -1: 6768 line, index = self.getCursorPosition() 6769 6770 self.SendScintilla(QsciScintilla.SCI_FOLDCHILDREN, line, 6771 QsciScintilla.SC_FOLDACTION_CONTRACT) 6772 6773 def __contextMenuExpandFoldWithChildren(self): 6774 """ 6775 Private slot to handle the context menu expand with children action. 6776 """ 6777 self.expandFoldWithChildren(self.line) 6778 6779 def __contextMenuCollapseFoldWithChildren(self): 6780 """ 6781 Private slot to handle the context menu collapse with children action. 6782 """ 6783 self.collapseFoldWithChildren(self.line) 6784 6785 ################################################################# 6786 ## Macro handling methods 6787 ################################################################# 6788 6789 def __getMacroName(self): 6790 """ 6791 Private method to select a macro name from the list of macros. 6792 6793 @return Tuple of macro name and a flag, indicating, if the user 6794 pressed ok or canceled the operation. (string, boolean) 6795 """ 6796 qs = [] 6797 for s in list(self.macros.keys()): 6798 qs.append(s) 6799 qs.sort() 6800 return QInputDialog.getItem( 6801 self, 6802 self.tr("Macro Name"), 6803 self.tr("Select a macro name:"), 6804 qs, 6805 0, False) 6806 6807 def macroRun(self): 6808 """ 6809 Public method to execute a macro. 6810 """ 6811 name, ok = self.__getMacroName() 6812 if ok and name: 6813 self.macros[name].play() 6814 6815 def macroDelete(self): 6816 """ 6817 Public method to delete a macro. 6818 """ 6819 name, ok = self.__getMacroName() 6820 if ok and name: 6821 del self.macros[name] 6822 6823 def macroLoad(self): 6824 """ 6825 Public method to load a macro from a file. 6826 """ 6827 configDir = Utilities.getConfigDir() 6828 fname = E5FileDialog.getOpenFileName( 6829 self, 6830 self.tr("Load macro file"), 6831 configDir, 6832 self.tr("Macro files (*.macro)")) 6833 6834 if not fname: 6835 return # user aborted 6836 6837 try: 6838 with open(fname, "r", encoding="utf-8") as f: 6839 lines = f.readlines() 6840 except OSError: 6841 E5MessageBox.critical( 6842 self, 6843 self.tr("Error loading macro"), 6844 self.tr( 6845 "<p>The macro file <b>{0}</b> could not be read.</p>") 6846 .format(fname)) 6847 return 6848 6849 if len(lines) != 2: 6850 E5MessageBox.critical( 6851 self, 6852 self.tr("Error loading macro"), 6853 self.tr("<p>The macro file <b>{0}</b> is corrupt.</p>") 6854 .format(fname)) 6855 return 6856 6857 macro = QsciMacro(lines[1], self) 6858 self.macros[lines[0].strip()] = macro 6859 6860 def macroSave(self): 6861 """ 6862 Public method to save a macro to a file. 6863 """ 6864 configDir = Utilities.getConfigDir() 6865 6866 name, ok = self.__getMacroName() 6867 if not ok or not name: 6868 return # user abort 6869 6870 fname, selectedFilter = E5FileDialog.getSaveFileNameAndFilter( 6871 self, 6872 self.tr("Save macro file"), 6873 configDir, 6874 self.tr("Macro files (*.macro)"), 6875 "", 6876 E5FileDialog.Options(E5FileDialog.DontConfirmOverwrite)) 6877 6878 if not fname: 6879 return # user aborted 6880 6881 ext = QFileInfo(fname).suffix() 6882 if not ext: 6883 ex = selectedFilter.split("(*")[1].split(")")[0] 6884 if ex: 6885 fname += ex 6886 if QFileInfo(fname).exists(): 6887 res = E5MessageBox.yesNo( 6888 self, 6889 self.tr("Save macro"), 6890 self.tr("<p>The macro file <b>{0}</b> already exists." 6891 " Overwrite it?</p>").format(fname), 6892 icon=E5MessageBox.Warning) 6893 if not res: 6894 return 6895 fname = Utilities.toNativeSeparators(fname) 6896 6897 try: 6898 with open(fname, "w", encoding="utf-8") as f: 6899 f.write("{0}{1}".format(name, "\n")) 6900 f.write(self.macros[name].save()) 6901 except OSError: 6902 E5MessageBox.critical( 6903 self, 6904 self.tr("Error saving macro"), 6905 self.tr( 6906 "<p>The macro file <b>{0}</b> could not be written.</p>") 6907 .format(fname)) 6908 return 6909 6910 def macroRecordingStart(self): 6911 """ 6912 Public method to start macro recording. 6913 """ 6914 if self.recording: 6915 res = E5MessageBox.yesNo( 6916 self, 6917 self.tr("Start Macro Recording"), 6918 self.tr("Macro recording is already active. Start new?"), 6919 icon=E5MessageBox.Warning, 6920 yesDefault=True) 6921 if res: 6922 self.macroRecordingStop() 6923 else: 6924 return 6925 else: 6926 self.recording = True 6927 6928 self.curMacro = QsciMacro(self) 6929 self.curMacro.startRecording() 6930 6931 def macroRecordingStop(self): 6932 """ 6933 Public method to stop macro recording. 6934 """ 6935 if not self.recording: 6936 return # we are not recording 6937 6938 self.curMacro.endRecording() 6939 self.recording = False 6940 6941 name, ok = QInputDialog.getText( 6942 self, 6943 self.tr("Macro Recording"), 6944 self.tr("Enter name of the macro:"), 6945 QLineEdit.EchoMode.Normal) 6946 6947 if ok and name: 6948 self.macros[name] = self.curMacro 6949 6950 self.curMacro = None 6951 6952 ################################################################# 6953 ## Overwritten methods 6954 ################################################################# 6955 6956 def undo(self): 6957 """ 6958 Public method to undo the last recorded change. 6959 """ 6960 super().undo() 6961 self.undoAvailable.emit(self.isUndoAvailable()) 6962 self.redoAvailable.emit(self.isRedoAvailable()) 6963 6964 def redo(self): 6965 """ 6966 Public method to redo the last recorded change. 6967 """ 6968 super().redo() 6969 self.undoAvailable.emit(self.isUndoAvailable()) 6970 self.redoAvailable.emit(self.isRedoAvailable()) 6971 6972 def close(self, alsoDelete=False): 6973 """ 6974 Public method called when the window gets closed. 6975 6976 This overwritten method redirects the action to our 6977 ViewManager.closeEditor, which in turn calls our closeIt 6978 method. 6979 6980 @param alsoDelete ignored 6981 @return flag indicating a successful close of the editor (boolean) 6982 """ 6983 return self.vm.closeEditor(self) 6984 6985 def closeIt(self): 6986 """ 6987 Public method called by the viewmanager to finally get rid of us. 6988 """ 6989 if Preferences.getEditor("ClearBreaksOnClose") and not self.__clones: 6990 self.__menuClearBreakpoints() 6991 6992 for clone in self.__clones[:]: 6993 self.removeClone(clone) 6994 clone.removeClone(self) 6995 6996 self.breakpointModel.rowsAboutToBeRemoved.disconnect( 6997 self.__deleteBreakPoints) 6998 self.breakpointModel.dataAboutToBeChanged.disconnect( 6999 self.__breakPointDataAboutToBeChanged) 7000 self.breakpointModel.dataChanged.disconnect( 7001 self.__changeBreakPoints) 7002 self.breakpointModel.rowsInserted.disconnect( 7003 self.__addBreakPoints) 7004 7005 if self.syntaxCheckService is not None: 7006 self.syntaxCheckService.syntaxChecked.disconnect( 7007 self.__processSyntaxCheckResult) 7008 self.syntaxCheckService.error.disconnect( 7009 self.__processSyntaxCheckError) 7010 7011 if self.spell: 7012 self.spell.stopIncrementalCheck() 7013 7014 with contextlib.suppress(TypeError): 7015 self.project.projectPropertiesChanged.disconnect( 7016 self.__projectPropertiesChanged) 7017 7018 if self.fileName: 7019 self.taskViewer.clearFileTasks(self.fileName, True) 7020 7021 super().close() 7022 7023 def keyPressEvent(self, ev): 7024 """ 7025 Protected method to handle the user input a key at a time. 7026 7027 @param ev key event 7028 @type QKeyEvent 7029 """ 7030 def encloseSelectedText(encString): 7031 """ 7032 Local function to enclose the current selection with some 7033 characters. 7034 7035 @param encString string to use to enclose the selection 7036 (one or two characters) 7037 @type str 7038 """ 7039 startChar = encString[0] 7040 endChar = encString[1] if len(encString) == 2 else startChar 7041 7042 sline, sindex, eline, eindex = self.getSelection() 7043 replaceText = startChar + self.selectedText() + endChar 7044 self.beginUndoAction() 7045 self.replaceSelectedText(replaceText) 7046 self.endUndoAction() 7047 self.setSelection(sline, sindex + 1, eline, eindex + 1) 7048 7049 txt = ev.text() 7050 7051 # See it is text to insert. 7052 if len(txt) and txt >= " ": 7053 if ( 7054 self.hasSelectedText() and 7055 txt in Editor.EncloseChars 7056 ): 7057 encloseSelectedText(Editor.EncloseChars[txt]) 7058 ev.accept() 7059 return 7060 7061 super().keyPressEvent(ev) 7062 else: 7063 ev.ignore() 7064 7065 def focusInEvent(self, event): 7066 """ 7067 Protected method called when the editor receives focus. 7068 7069 This method checks for modifications of the current file and 7070 rereads it upon request. The cursor is placed at the current position 7071 assuming, that it is in the vicinity of the old position after the 7072 reread. 7073 7074 @param event the event object 7075 @type QFocusEvent 7076 """ 7077 self.recolor() 7078 self.vm.editActGrp.setEnabled(True) 7079 self.vm.editorActGrp.setEnabled(True) 7080 self.vm.copyActGrp.setEnabled(True) 7081 self.vm.viewActGrp.setEnabled(True) 7082 self.vm.searchActGrp.setEnabled(True) 7083 with contextlib.suppress(AttributeError): 7084 self.setCaretWidth(self.caretWidth) 7085 self.__updateReadOnly(False) 7086 if ( 7087 self.vm.editorsCheckFocusInEnabled() and 7088 not self.inReopenPrompt and self.fileName and 7089 QFileInfo(self.fileName).lastModified().toString() != 7090 self.lastModified.toString() 7091 ): 7092 self.inReopenPrompt = True 7093 if Preferences.getEditor("AutoReopen") and not self.isModified(): 7094 self.refresh() 7095 else: 7096 msg = self.tr( 7097 """<p>The file <b>{0}</b> has been changed while it""" 7098 """ was opened in eric. Reread it?</p>""" 7099 ).format(self.fileName) 7100 yesDefault = True 7101 if self.isModified(): 7102 msg += self.tr( 7103 """<br><b>Warning:</b> You will lose""" 7104 """ your changes upon reopening it.""") 7105 yesDefault = False 7106 res = E5MessageBox.yesNo( 7107 self, 7108 self.tr("File changed"), msg, 7109 icon=E5MessageBox.Warning, 7110 yesDefault=yesDefault) 7111 if res: 7112 self.refresh() 7113 else: 7114 # do not prompt for this change again... 7115 self.lastModified = QFileInfo(self.fileName).lastModified() 7116 self.inReopenPrompt = False 7117 7118 self.setCursorFlashTime(QApplication.cursorFlashTime()) 7119 7120 super().focusInEvent(event) 7121 7122 def focusOutEvent(self, event): 7123 """ 7124 Protected method called when the editor loses focus. 7125 7126 @param event the event object 7127 @type QFocusEvent 7128 """ 7129 self.vm.editorActGrp.setEnabled(False) 7130 self.setCaretWidth(0) 7131 7132 super().focusOutEvent(event) 7133 7134 def changeEvent(self, evt): 7135 """ 7136 Protected method called to process an event. 7137 7138 This implements special handling for the events showMaximized, 7139 showMinimized and showNormal. The windows caption is shortened 7140 for the minimized mode and reset to the full filename for the 7141 other modes. This is to make the editor windows work nicer 7142 with the QWorkspace. 7143 7144 @param evt the event, that was generated 7145 @type QEvent 7146 """ 7147 if ( 7148 evt.type() == QEvent.Type.WindowStateChange and 7149 bool(self.fileName) 7150 ): 7151 if ( 7152 self.windowState() == Qt.WindowStates( 7153 Qt.WindowState.WindowMinimized) 7154 ): 7155 cap = os.path.basename(self.fileName) 7156 else: 7157 cap = self.fileName 7158 if self.isReadOnly(): 7159 cap = self.tr("{0} (ro)").format(cap) 7160 self.setWindowTitle(cap) 7161 7162 super().changeEvent(evt) 7163 7164 def mousePressEvent(self, event): 7165 """ 7166 Protected method to handle the mouse press event. 7167 7168 @param event the mouse press event 7169 @type QMouseEvent 7170 """ 7171 self.vm.eventFilter(self, event) 7172 super().mousePressEvent(event) 7173 7174 def mouseDoubleClickEvent(self, evt): 7175 """ 7176 Protected method to handle mouse double click events. 7177 7178 @param evt reference to the mouse event 7179 @type QMouseEvent 7180 """ 7181 super().mouseDoubleClickEvent(evt) 7182 7183 self.mouseDoubleClick.emit(evt.pos(), evt.buttons()) 7184 7185 def wheelEvent(self, evt): 7186 """ 7187 Protected method to handle wheel events. 7188 7189 @param evt reference to the wheel event 7190 @type QWheelEvent 7191 """ 7192 delta = evt.angleDelta().y() 7193 if evt.modifiers() & Qt.KeyboardModifier.ControlModifier: 7194 if delta < 0: 7195 self.zoomOut() 7196 elif delta > 0: 7197 self.zoomIn() 7198 evt.accept() 7199 return 7200 7201 if evt.modifiers() & Qt.KeyboardModifier.ShiftModifier: 7202 if delta < 0: 7203 self.gotoMethodClass(False) 7204 elif delta > 0: 7205 self.gotoMethodClass(True) 7206 evt.accept() 7207 return 7208 7209 super().wheelEvent(evt) 7210 7211 def event(self, evt): 7212 """ 7213 Public method handling events. 7214 7215 @param evt reference to the event 7216 @type QEvent 7217 @return flag indicating, if the event was handled 7218 @rtype bool 7219 """ 7220 if evt.type() == QEvent.Type.Gesture: 7221 self.gestureEvent(evt) 7222 return True 7223 7224 return super().event(evt) 7225 7226 def gestureEvent(self, evt): 7227 """ 7228 Protected method handling gesture events. 7229 7230 @param evt reference to the gesture event 7231 @type QGestureEvent 7232 """ 7233 pinch = evt.gesture(Qt.GestureType.PinchGesture) 7234 if pinch: 7235 if pinch.state() == Qt.GestureState.GestureStarted: 7236 zoom = (self.getZoom() + 10) / 10.0 7237 pinch.setTotalScaleFactor(zoom) 7238 elif pinch.state() == Qt.GestureState.GestureUpdated: 7239 zoom = int(pinch.totalScaleFactor() * 10) - 10 7240 if zoom <= -9: 7241 zoom = -9 7242 pinch.setTotalScaleFactor(0.1) 7243 elif zoom >= 20: 7244 zoom = 20 7245 pinch.setTotalScaleFactor(3.0) 7246 self.zoomTo(zoom) 7247 evt.accept() 7248 7249 def resizeEvent(self, evt): 7250 """ 7251 Protected method handling resize events. 7252 7253 @param evt reference to the resize event 7254 @type QResizeEvent 7255 """ 7256 super().resizeEvent(evt) 7257 self.__markerMap.calculateGeometry() 7258 7259 def viewportEvent(self, evt): 7260 """ 7261 Protected method handling event of the viewport. 7262 7263 @param evt reference to the event 7264 @type QEvent 7265 @return flag indiating that the event was handled 7266 @rtype bool 7267 """ 7268 with contextlib.suppress(AttributeError): 7269 self.__markerMap.calculateGeometry() 7270 return super().viewportEvent(evt) 7271 7272 def __updateReadOnly(self, bForce=True): 7273 """ 7274 Private method to update the readOnly information for this editor. 7275 7276 If bForce is True, then updates everything regardless if 7277 the attributes have actually changed, such as during 7278 initialization time. A signal is emitted after the 7279 caption change. 7280 7281 @param bForce True to force change, False to only update and emit 7282 signal if there was an attribute change. 7283 """ 7284 if self.fileName == "": 7285 return 7286 7287 readOnly = ( 7288 not QFileInfo(self.fileName).isWritable() or 7289 self.isReadOnly() 7290 ) 7291 if not bForce and (readOnly == self.isReadOnly()): 7292 return 7293 7294 cap = self.fileName 7295 if readOnly: 7296 cap = self.tr("{0} (ro)".format(cap)) 7297 self.setReadOnly(readOnly) 7298 self.setWindowTitle(cap) 7299 self.captionChanged.emit(cap, self) 7300 7301 def refresh(self): 7302 """ 7303 Public slot to refresh the editor contents. 7304 """ 7305 # save cursor position 7306 cline, cindex = self.getCursorPosition() 7307 7308 # save bookmarks and breakpoints and clear them 7309 bmlist = self.getBookmarks() 7310 self.clearBookmarks() 7311 7312 # clear syntax error markers 7313 self.clearSyntaxError() 7314 7315 # clear flakes warning markers 7316 self.clearWarnings() 7317 7318 # clear breakpoint markers 7319 for handle in list(self.breaks.keys()): 7320 self.markerDeleteHandle(handle) 7321 self.breaks.clear() 7322 7323 if not os.path.exists(self.fileName): 7324 # close the file, if it was deleted in the background 7325 self.close() 7326 return 7327 7328 # reread the file 7329 try: 7330 self.readFile(self.fileName) 7331 except OSError: 7332 # do not prompt for this change again... 7333 self.lastModified = QDateTime.currentDateTime() 7334 self.setModified(False) 7335 self.__convertTabs() 7336 7337 # re-initialize the online change tracer 7338 self.__reinitOnlineChangeTrace() 7339 7340 # reset cursor position 7341 self.setCursorPosition(cline, cindex) 7342 self.ensureCursorVisible() 7343 7344 # reset bookmarks and breakpoints to their old position 7345 if bmlist: 7346 for bm in bmlist: 7347 self.toggleBookmark(bm) 7348 self.__restoreBreakpoints() 7349 7350 self.editorSaved.emit(self.fileName) 7351 self.checkSyntax() 7352 7353 self.__markerMap.update() 7354 7355 self.refreshed.emit() 7356 7357 def setMonospaced(self, on): 7358 """ 7359 Public method to set/reset a monospaced font. 7360 7361 @param on flag to indicate usage of a monospace font (boolean) 7362 """ 7363 if on: 7364 if not self.lexer_: 7365 f = Preferences.getEditorOtherFonts("MonospacedFont") 7366 self.monospacedStyles(f) 7367 else: 7368 if not self.lexer_: 7369 self.clearStyles() 7370 self.__setMarginsDisplay() 7371 self.setFont(Preferences.getEditorOtherFonts("DefaultFont")) 7372 7373 self.useMonospaced = on 7374 7375 def clearStyles(self): 7376 """ 7377 Public method to set the styles according the selected Qt style 7378 or the selected editor colours. 7379 """ 7380 super().clearStyles() 7381 if Preferences.getEditor("OverrideEditAreaColours"): 7382 self.setColor(Preferences.getEditorColour("EditAreaForeground")) 7383 self.setPaper(Preferences.getEditorColour("EditAreaBackground")) 7384 7385 ################################################################# 7386 ## Drag and Drop Support 7387 ################################################################# 7388 7389 def dragEnterEvent(self, event): 7390 """ 7391 Protected method to handle the drag enter event. 7392 7393 @param event the drag enter event (QDragEnterEvent) 7394 """ 7395 self.inDragDrop = event.mimeData().hasUrls() 7396 if self.inDragDrop: 7397 event.acceptProposedAction() 7398 else: 7399 super().dragEnterEvent(event) 7400 7401 def dragMoveEvent(self, event): 7402 """ 7403 Protected method to handle the drag move event. 7404 7405 @param event the drag move event (QDragMoveEvent) 7406 """ 7407 if self.inDragDrop: 7408 event.accept() 7409 else: 7410 super().dragMoveEvent(event) 7411 7412 def dragLeaveEvent(self, event): 7413 """ 7414 Protected method to handle the drag leave event. 7415 7416 @param event the drag leave event (QDragLeaveEvent) 7417 """ 7418 if self.inDragDrop: 7419 self.inDragDrop = False 7420 event.accept() 7421 else: 7422 super().dragLeaveEvent(event) 7423 7424 def dropEvent(self, event): 7425 """ 7426 Protected method to handle the drop event. 7427 7428 @param event the drop event (QDropEvent) 7429 """ 7430 if event.mimeData().hasUrls(): 7431 for url in event.mimeData().urls(): 7432 fname = url.toLocalFile() 7433 if fname: 7434 if not QFileInfo(fname).isDir(): 7435 self.vm.openSourceFile(fname) 7436 else: 7437 E5MessageBox.information( 7438 self, 7439 self.tr("Drop Error"), 7440 self.tr("""<p><b>{0}</b> is not a file.</p>""") 7441 .format(fname)) 7442 event.acceptProposedAction() 7443 else: 7444 super().dropEvent(event) 7445 7446 self.inDragDrop = False 7447 7448 ################################################################# 7449 ## Support for Qt resources files 7450 ################################################################# 7451 7452 def __initContextMenuResources(self): 7453 """ 7454 Private method used to setup the Resources context sub menu. 7455 7456 @return reference to the generated menu (QMenu) 7457 """ 7458 menu = QMenu(self.tr('Resources')) 7459 7460 menu.addAction( 7461 self.tr('Add file...'), self.__addFileResource) 7462 menu.addAction( 7463 self.tr('Add files...'), self.__addFileResources) 7464 menu.addAction( 7465 self.tr('Add aliased file...'), 7466 self.__addFileAliasResource) 7467 menu.addAction( 7468 self.tr('Add localized resource...'), 7469 self.__addLocalizedResource) 7470 menu.addSeparator() 7471 menu.addAction( 7472 self.tr('Add resource frame'), self.__addResourceFrame) 7473 7474 menu.aboutToShow.connect(self.__showContextMenuResources) 7475 7476 return menu 7477 7478 def __showContextMenuResources(self): 7479 """ 7480 Private slot handling the aboutToShow signal of the resources context 7481 menu. 7482 """ 7483 self.showMenu.emit("Resources", self.resourcesMenu, self) 7484 7485 def __addFileResource(self): 7486 """ 7487 Private method to handle the Add file context menu action. 7488 """ 7489 dirStr = os.path.dirname(self.fileName) 7490 file = E5FileDialog.getOpenFileName( 7491 self, 7492 self.tr("Add file resource"), 7493 dirStr, 7494 "") 7495 if file: 7496 relFile = QDir(dirStr).relativeFilePath(file) 7497 line, index = self.getCursorPosition() 7498 self.insert(" <file>{0}</file>\n".format(relFile)) 7499 self.setCursorPosition(line + 1, index) 7500 7501 def __addFileResources(self): 7502 """ 7503 Private method to handle the Add files context menu action. 7504 """ 7505 dirStr = os.path.dirname(self.fileName) 7506 files = E5FileDialog.getOpenFileNames( 7507 self, 7508 self.tr("Add file resources"), 7509 dirStr, 7510 "") 7511 if files: 7512 myDir = QDir(dirStr) 7513 filesText = "" 7514 for file in files: 7515 relFile = myDir.relativeFilePath(file) 7516 filesText += " <file>{0}</file>\n".format(relFile) 7517 line, index = self.getCursorPosition() 7518 self.insert(filesText) 7519 self.setCursorPosition(line + len(files), index) 7520 7521 def __addFileAliasResource(self): 7522 """ 7523 Private method to handle the Add aliased file context menu action. 7524 """ 7525 dirStr = os.path.dirname(self.fileName) 7526 file = E5FileDialog.getOpenFileName( 7527 self, 7528 self.tr("Add aliased file resource"), 7529 dirStr, 7530 "") 7531 if file: 7532 relFile = QDir(dirStr).relativeFilePath(file) 7533 alias, ok = QInputDialog.getText( 7534 self, 7535 self.tr("Add aliased file resource"), 7536 self.tr("Alias for file <b>{0}</b>:").format(relFile), 7537 QLineEdit.EchoMode.Normal, 7538 relFile) 7539 if ok and alias: 7540 line, index = self.getCursorPosition() 7541 self.insert(' <file alias="{1}">{0}</file>\n' 7542 .format(relFile, alias)) 7543 self.setCursorPosition(line + 1, index) 7544 7545 def __addLocalizedResource(self): 7546 """ 7547 Private method to handle the Add localized resource context menu 7548 action. 7549 """ 7550 from Project.AddLanguageDialog import AddLanguageDialog 7551 dlg = AddLanguageDialog(self) 7552 if dlg.exec() == QDialog.DialogCode.Accepted: 7553 lang = dlg.getSelectedLanguage() 7554 line, index = self.getCursorPosition() 7555 self.insert('<qresource lang="{0}">\n</qresource>\n'.format(lang)) 7556 self.setCursorPosition(line + 2, index) 7557 7558 def __addResourceFrame(self): 7559 """ 7560 Private method to handle the Add resource frame context menu action. 7561 """ 7562 line, index = self.getCursorPosition() 7563 self.insert('<!DOCTYPE RCC>\n' 7564 '<RCC version="1.0">\n' 7565 '<qresource>\n' 7566 '</qresource>\n' 7567 '</RCC>\n') 7568 self.setCursorPosition(line + 5, index) 7569 7570 ################################################################# 7571 ## Support for diagrams below 7572 ################################################################# 7573 7574 def __showClassDiagram(self): 7575 """ 7576 Private method to handle the Class Diagram context menu action. 7577 """ 7578 from Graphics.UMLDialog import UMLDialog, UMLDialogType 7579 if not self.checkDirty(): 7580 return 7581 7582 self.classDiagram = UMLDialog( 7583 UMLDialogType.CLASS_DIAGRAM, self.project, self.fileName, 7584 self, noAttrs=False) 7585 self.classDiagram.show() 7586 7587 def __showPackageDiagram(self): 7588 """ 7589 Private method to handle the Package Diagram context menu action. 7590 """ 7591 from Graphics.UMLDialog import UMLDialog, UMLDialogType 7592 if not self.checkDirty(): 7593 return 7594 7595 package = ( 7596 os.path.isdir(self.fileName) and 7597 self.fileName or os.path.dirname(self.fileName) 7598 ) 7599 res = E5MessageBox.yesNo( 7600 self, 7601 self.tr("Package Diagram"), 7602 self.tr("""Include class attributes?"""), 7603 yesDefault=True) 7604 self.packageDiagram = UMLDialog( 7605 UMLDialogType.PACKAGE_DIAGRAM, self.project, package, 7606 self, noAttrs=not res) 7607 self.packageDiagram.show() 7608 7609 def __showImportsDiagram(self): 7610 """ 7611 Private method to handle the Imports Diagram context menu action. 7612 """ 7613 from Graphics.UMLDialog import UMLDialog, UMLDialogType 7614 if not self.checkDirty(): 7615 return 7616 7617 package = os.path.dirname(self.fileName) 7618 res = E5MessageBox.yesNo( 7619 self, 7620 self.tr("Imports Diagram"), 7621 self.tr("""Include imports from external modules?""")) 7622 self.importsDiagram = UMLDialog( 7623 UMLDialogType.IMPORTS_DIAGRAM, self.project, package, 7624 self, showExternalImports=res) 7625 self.importsDiagram.show() 7626 7627 def __showApplicationDiagram(self): 7628 """ 7629 Private method to handle the Imports Diagram context menu action. 7630 """ 7631 from Graphics.UMLDialog import UMLDialog, UMLDialogType 7632 res = E5MessageBox.yesNo( 7633 self, 7634 self.tr("Application Diagram"), 7635 self.tr("""Include module names?"""), 7636 yesDefault=True) 7637 self.applicationDiagram = UMLDialog( 7638 UMLDialogType.APPLICATION_DIAGRAM, self.project, 7639 self, noModules=not res) 7640 self.applicationDiagram.show() 7641 7642 def __loadDiagram(self): 7643 """ 7644 Private slot to load a diagram from file. 7645 """ 7646 from Graphics.UMLDialog import UMLDialog, UMLDialogType 7647 self.loadedDiagram = UMLDialog( 7648 UMLDialogType.NO_DIAGRAM, self.project, parent=self) 7649 if self.loadedDiagram.load(): 7650 self.loadedDiagram.show(fromFile=True) 7651 else: 7652 self.loadedDiagram = None 7653 7654 ####################################################################### 7655 ## Typing aids related methods below 7656 ####################################################################### 7657 7658 def __toggleTypingAids(self): 7659 """ 7660 Private slot to toggle the typing aids. 7661 """ 7662 if self.menuActs["TypingAidsEnabled"].isChecked(): 7663 self.completer.setEnabled(True) 7664 else: 7665 self.completer.setEnabled(False) 7666 7667 ####################################################################### 7668 ## Auto-completing templates 7669 ####################################################################### 7670 7671 def editorCommand(self, cmd): 7672 """ 7673 Public method to perform a simple editor command. 7674 7675 @param cmd the scintilla command to be performed 7676 """ 7677 if cmd == QsciScintilla.SCI_TAB: 7678 try: 7679 templateViewer = e5App().getObject("TemplateViewer") 7680 except KeyError: 7681 # template viewer is not active 7682 templateViewer = None 7683 7684 if templateViewer is not None: 7685 line, index = self.getCursorPosition() 7686 tmplName = self.getWordLeft(line, index) 7687 if tmplName: 7688 if templateViewer.hasTemplate(tmplName, 7689 self.getLanguage()): 7690 self.__applyTemplate(tmplName, self.getLanguage()) 7691 return 7692 else: 7693 templateNames = templateViewer.getTemplateNames( 7694 tmplName, self.getLanguage()) 7695 if len(templateNames) == 1: 7696 self.__applyTemplate(templateNames[0], 7697 self.getLanguage()) 7698 return 7699 elif len(templateNames) > 1: 7700 self.showUserList( 7701 TemplateCompletionListID, 7702 ["{0}?{1:d}".format(t, self.TemplateImageID) 7703 for t in templateNames]) 7704 return 7705 7706 elif cmd == QsciScintilla.SCI_DELETEBACK: 7707 line, index = self.getCursorPosition() 7708 text = self.text(line)[index - 1:index + 1] 7709 matchingPairs = ['()', '[]', '{}', '<>', "''", '""'] 7710 # __IGNORE_WARNING_M613__ 7711 if text in matchingPairs: 7712 self.delete() 7713 7714 super().editorCommand(cmd) 7715 7716 def __applyTemplate(self, templateName, language): 7717 """ 7718 Private method to apply a template by name. 7719 7720 @param templateName name of the template to apply (string) 7721 @param language name of the language (group) to get the template 7722 from (string) 7723 """ 7724 try: 7725 templateViewer = e5App().getObject("TemplateViewer") 7726 except KeyError: 7727 # template viewer is not active 7728 return 7729 7730 if templateViewer.hasTemplate(templateName, self.getLanguage()): 7731 self.extendSelectionWordLeft() 7732 templateViewer.applyNamedTemplate(templateName, 7733 self.getLanguage()) 7734 7735 ####################################################################### 7736 ## Project related methods 7737 ####################################################################### 7738 7739 def __projectPropertiesChanged(self): 7740 """ 7741 Private slot to handle changes of the project properties. 7742 """ 7743 if self.spell: 7744 pwl, pel = self.project.getProjectDictionaries() 7745 self.__setSpellingLanguage(self.project.getProjectSpellLanguage(), 7746 pwl=pwl, pel=pel) 7747 7748 editorConfigEol = self.__getEditorConfig("EOLMode", nodefault=True) 7749 if editorConfigEol is not None: 7750 self.setEolMode(editorConfigEol) 7751 else: 7752 self.setEolModeByEolString(self.project.getEolString()) 7753 self.convertEols(self.eolMode()) 7754 7755 def addedToProject(self): 7756 """ 7757 Public method to signal, that this editor has been added to a project. 7758 """ 7759 if self.spell: 7760 pwl, pel = self.project.getProjectDictionaries() 7761 self.__setSpellingLanguage(self.project.getProjectSpellLanguage(), 7762 pwl=pwl, pel=pel) 7763 7764 self.project.projectPropertiesChanged.connect( 7765 self.__projectPropertiesChanged) 7766 7767 def projectOpened(self): 7768 """ 7769 Public slot to handle the opening of a project. 7770 """ 7771 if ( 7772 self.fileName and 7773 self.project.isProjectSource(self.fileName) 7774 ): 7775 self.project.projectPropertiesChanged.connect( 7776 self.__projectPropertiesChanged) 7777 self.setSpellingForProject() 7778 7779 def projectClosed(self): 7780 """ 7781 Public slot to handle the closing of a project. 7782 """ 7783 with contextlib.suppress(TypeError): 7784 self.project.projectPropertiesChanged.disconnect( 7785 self.__projectPropertiesChanged) 7786 7787 ####################################################################### 7788 ## Spell checking related methods 7789 ####################################################################### 7790 7791 def getSpellingLanguage(self): 7792 """ 7793 Public method to get the current spelling language. 7794 7795 @return current spelling language 7796 @rtype str 7797 """ 7798 if self.spell: 7799 return self.spell.getLanguage() 7800 7801 return "" 7802 7803 def __setSpellingLanguage(self, language, pwl="", pel=""): 7804 """ 7805 Private slot to set the spell checking language. 7806 7807 @param language spell checking language to be set (string) 7808 @param pwl name of the personal/project word list (string) 7809 @param pel name of the personal/project exclude list (string) 7810 """ 7811 if self.spell and self.spell.getLanguage() != language: 7812 self.spell.setLanguage(language, pwl=pwl, pel=pel) 7813 self.spell.checkDocumentIncrementally() 7814 7815 def __setSpelling(self): 7816 """ 7817 Private method to initialize the spell checking functionality. 7818 """ 7819 if Preferences.getEditor("SpellCheckingEnabled"): 7820 self.__spellCheckStringsOnly = Preferences.getEditor( 7821 "SpellCheckStringsOnly") 7822 if self.spell is None: 7823 self.spell = SpellChecker(self, self.spellingIndicator, 7824 checkRegion=self.isSpellCheckRegion) 7825 self.setSpellingForProject() 7826 self.spell.setMinimumWordSize( 7827 Preferences.getEditor("SpellCheckingMinWordSize")) 7828 7829 self.setAutoSpellChecking() 7830 else: 7831 self.spell = None 7832 self.clearAllIndicators(self.spellingIndicator) 7833 7834 def setSpellingForProject(self): 7835 """ 7836 Public method to set the spell checking options for files belonging 7837 to the current project. 7838 """ 7839 if ( 7840 self.fileName and 7841 self.project.isOpen() and 7842 self.project.isProjectSource(self.fileName) 7843 ): 7844 pwl, pel = self.project.getProjectDictionaries() 7845 self.__setSpellingLanguage(self.project.getProjectSpellLanguage(), 7846 pwl=pwl, pel=pel) 7847 7848 def setAutoSpellChecking(self): 7849 """ 7850 Public method to set the automatic spell checking. 7851 """ 7852 if Preferences.getEditor("AutoSpellCheckingEnabled"): 7853 with contextlib.suppress(TypeError): 7854 self.SCN_CHARADDED.connect( 7855 self.__spellCharAdded, Qt.ConnectionType.UniqueConnection) 7856 self.spell.checkDocumentIncrementally() 7857 else: 7858 with contextlib.suppress(TypeError): 7859 self.SCN_CHARADDED.disconnect(self.__spellCharAdded) 7860 self.clearAllIndicators(self.spellingIndicator) 7861 7862 def isSpellCheckRegion(self, pos): 7863 """ 7864 Public method to check, if the given position is within a region, that 7865 should be spell checked. 7866 7867 For files with a configured full text file extension all regions will 7868 be regarded as to be checked. Depending on configuration, all unknown 7869 files (i.e. those without a file extension) will be checked fully as 7870 well. 7871 7872 @param pos position to be checked 7873 @type int 7874 @return flag indicating pos is in a spell check region 7875 @rtype bool 7876 """ 7877 if self.__spellCheckStringsOnly: 7878 if ( 7879 (self.__fileNameExtension in 7880 Preferences.getEditor("FullSpellCheckExtensions")) or 7881 (not self.__fileNameExtension and 7882 Preferences.getEditor("FullSpellCheckUnknown")) 7883 ): 7884 return True 7885 else: 7886 style = self.styleAt(pos) 7887 if self.lexer_ is not None: 7888 return ( 7889 self.lexer_.isCommentStyle(style) or 7890 self.lexer_.isStringStyle(style) 7891 ) 7892 7893 return True 7894 7895 @pyqtSlot(int) 7896 def __spellCharAdded(self, charNumber): 7897 """ 7898 Private slot called to handle the user entering a character. 7899 7900 @param charNumber value of the character entered (integer) 7901 """ 7902 if self.spell: 7903 if not chr(charNumber).isalnum(): 7904 self.spell.checkWord( 7905 self.positionBefore(self.currentPosition()), True) 7906 elif self.hasIndicator( 7907 self.spellingIndicator, self.currentPosition()): 7908 self.spell.checkWord(self.currentPosition()) 7909 7910 def checkSpelling(self): 7911 """ 7912 Public slot to perform an interactive spell check of the document. 7913 """ 7914 if self.spell: 7915 cline, cindex = self.getCursorPosition() 7916 from .SpellCheckingDialog import SpellCheckingDialog 7917 dlg = SpellCheckingDialog(self.spell, 0, self.length(), self) 7918 dlg.exec() 7919 self.setCursorPosition(cline, cindex) 7920 if Preferences.getEditor("AutoSpellCheckingEnabled"): 7921 self.spell.checkDocumentIncrementally() 7922 7923 def __checkSpellingSelection(self): 7924 """ 7925 Private slot to spell check the current selection. 7926 """ 7927 from .SpellCheckingDialog import SpellCheckingDialog 7928 sline, sindex, eline, eindex = self.getSelection() 7929 startPos = self.positionFromLineIndex(sline, sindex) 7930 endPos = self.positionFromLineIndex(eline, eindex) 7931 dlg = SpellCheckingDialog(self.spell, startPos, endPos, self) 7932 dlg.exec() 7933 7934 def __checkSpellingWord(self): 7935 """ 7936 Private slot to check the word below the spelling context menu. 7937 """ 7938 from .SpellCheckingDialog import SpellCheckingDialog 7939 line, index = self.lineIndexFromPosition(self.spellingMenuPos) 7940 wordStart, wordEnd = self.getWordBoundaries(line, index) 7941 wordStartPos = self.positionFromLineIndex(line, wordStart) 7942 wordEndPos = self.positionFromLineIndex(line, wordEnd) 7943 dlg = SpellCheckingDialog(self.spell, wordStartPos, wordEndPos, self) 7944 dlg.exec() 7945 7946 def __showContextMenuSpelling(self): 7947 """ 7948 Private slot to set up the spelling menu before it is shown. 7949 """ 7950 self.spellingMenu.clear() 7951 self.spellingSuggActs = [] 7952 line, index = self.lineIndexFromPosition(self.spellingMenuPos) 7953 word = self.getWord(line, index) 7954 suggestions = self.spell.getSuggestions(word) 7955 for suggestion in suggestions[:5]: 7956 self.spellingSuggActs.append( 7957 self.spellingMenu.addAction(suggestion)) 7958 if suggestions: 7959 self.spellingMenu.addSeparator() 7960 self.spellingMenu.addAction( 7961 UI.PixmapCache.getIcon("spellchecking"), 7962 self.tr("Check spelling..."), self.__checkSpellingWord) 7963 self.spellingMenu.addAction( 7964 self.tr("Add to dictionary"), self.__addToSpellingDictionary) 7965 self.spellingMenu.addAction( 7966 self.tr("Ignore All"), self.__ignoreSpellingAlways) 7967 7968 self.showMenu.emit("Spelling", self.spellingMenu, self) 7969 7970 def __contextMenuSpellingTriggered(self, action): 7971 """ 7972 Private slot to handle the selection of a suggestion of the spelling 7973 context menu. 7974 7975 @param action reference to the action that was selected (QAction) 7976 """ 7977 if action in self.spellingSuggActs: 7978 replacement = action.text() 7979 line, index = self.lineIndexFromPosition(self.spellingMenuPos) 7980 wordStart, wordEnd = self.getWordBoundaries(line, index) 7981 self.setSelection(line, wordStart, line, wordEnd) 7982 self.beginUndoAction() 7983 self.removeSelectedText() 7984 self.insert(replacement) 7985 self.endUndoAction() 7986 7987 def __addToSpellingDictionary(self): 7988 """ 7989 Private slot to add the word below the spelling context menu to the 7990 dictionary. 7991 """ 7992 line, index = self.lineIndexFromPosition(self.spellingMenuPos) 7993 word = self.getWord(line, index) 7994 self.spell.add(word) 7995 7996 wordStart, wordEnd = self.getWordBoundaries(line, index) 7997 self.clearIndicator(self.spellingIndicator, line, wordStart, 7998 line, wordEnd) 7999 if Preferences.getEditor("AutoSpellCheckingEnabled"): 8000 self.spell.checkDocumentIncrementally() 8001 8002 def __removeFromSpellingDictionary(self): 8003 """ 8004 Private slot to remove the word below the context menu to the 8005 dictionary. 8006 """ 8007 line, index = self.lineIndexFromPosition(self.spellingMenuPos) 8008 word = self.getWord(line, index) 8009 self.spell.remove(word) 8010 8011 if Preferences.getEditor("AutoSpellCheckingEnabled"): 8012 self.spell.checkDocumentIncrementally() 8013 8014 def __ignoreSpellingAlways(self): 8015 """ 8016 Private to always ignore the word below the spelling context menu. 8017 """ 8018 line, index = self.lineIndexFromPosition(self.spellingMenuPos) 8019 word = self.getWord(line, index) 8020 self.spell.ignoreAlways(word) 8021 if Preferences.getEditor("AutoSpellCheckingEnabled"): 8022 self.spell.checkDocumentIncrementally() 8023 8024 ####################################################################### 8025 ## Cooperation related methods 8026 ####################################################################### 8027 8028 def getSharingStatus(self): 8029 """ 8030 Public method to get some share status info. 8031 8032 @return tuple indicating, if the editor is sharable, the sharing 8033 status, if it is inside a locally initiated shared edit session 8034 and if it is inside a remotely initiated shared edit session 8035 (boolean, boolean, boolean, boolean) 8036 """ 8037 return ( 8038 (bool(self.fileName) and 8039 self.project.isOpen() and 8040 self.project.isProjectFile(self.fileName)), 8041 self.__isShared, 8042 self.__inSharedEdit, 8043 self.__inRemoteSharedEdit 8044 ) 8045 8046 def shareConnected(self, connected): 8047 """ 8048 Public slot to handle a change of the connected state. 8049 8050 @param connected flag indicating the connected state (boolean) 8051 """ 8052 if not connected: 8053 self.__inRemoteSharedEdit = False 8054 self.setReadOnly(False) 8055 self.__updateReadOnly() 8056 self.cancelSharedEdit(send=False) 8057 self.__isSyncing = False 8058 self.__receivedWhileSyncing = [] 8059 8060 def shareEditor(self, share): 8061 """ 8062 Public slot to set the shared status of the editor. 8063 8064 @param share flag indicating the share status (boolean) 8065 """ 8066 self.__isShared = share 8067 if not share: 8068 self.shareConnected(False) 8069 8070 def startSharedEdit(self): 8071 """ 8072 Public slot to start a shared edit session for the editor. 8073 """ 8074 self.__inSharedEdit = True 8075 self.__savedText = self.text() 8076 hashStr = str( 8077 QCryptographicHash.hash( 8078 Utilities.encode(self.__savedText, self.encoding)[0], 8079 QCryptographicHash.Algorithm.Sha1).toHex(), 8080 encoding="utf-8") 8081 self.__send(Editor.StartEditToken, hashStr) 8082 8083 def sendSharedEdit(self): 8084 """ 8085 Public slot to end a shared edit session for the editor and 8086 send the changes. 8087 """ 8088 commands = self.__calculateChanges(self.__savedText, self.text()) 8089 self.__send(Editor.EndEditToken, commands) 8090 self.__inSharedEdit = False 8091 self.__savedText = "" 8092 8093 def cancelSharedEdit(self, send=True): 8094 """ 8095 Public slot to cancel a shared edit session for the editor. 8096 8097 @param send flag indicating to send the CancelEdit command (boolean) 8098 """ 8099 self.__inSharedEdit = False 8100 self.__savedText = "" 8101 if send: 8102 self.__send(Editor.CancelEditToken) 8103 8104 def __send(self, token, args=None): 8105 """ 8106 Private method to send an editor command to remote editors. 8107 8108 @param token command token (string) 8109 @param args arguments for the command (string) 8110 """ 8111 if self.vm.isConnected(): 8112 msg = "" 8113 if token in (Editor.StartEditToken, 8114 Editor.EndEditToken, 8115 Editor.RequestSyncToken, 8116 Editor.SyncToken): 8117 msg = "{0}{1}{2}".format( 8118 token, 8119 Editor.Separator, 8120 args 8121 ) 8122 elif token == Editor.CancelEditToken: 8123 msg = "{0}{1}c".format( 8124 token, 8125 Editor.Separator 8126 ) 8127 8128 self.vm.send(self.fileName, msg) 8129 8130 def receive(self, command): 8131 """ 8132 Public slot to handle received editor commands. 8133 8134 @param command command string (string) 8135 """ 8136 if self.__isShared: 8137 if ( 8138 self.__isSyncing and 8139 not command.startswith(Editor.SyncToken + Editor.Separator) 8140 ): 8141 self.__receivedWhileSyncing.append(command) 8142 else: 8143 self.__dispatchCommand(command) 8144 8145 def __dispatchCommand(self, command): 8146 """ 8147 Private method to dispatch received commands. 8148 8149 @param command command to be processed (string) 8150 """ 8151 token, argsString = command.split(Editor.Separator, 1) 8152 if token == Editor.StartEditToken: 8153 self.__processStartEditCommand(argsString) 8154 elif token == Editor.CancelEditToken: 8155 self.shareConnected(False) 8156 elif token == Editor.EndEditToken: 8157 self.__processEndEditCommand(argsString) 8158 elif token == Editor.RequestSyncToken: 8159 self.__processRequestSyncCommand(argsString) 8160 elif token == Editor.SyncToken: 8161 self.__processSyncCommand(argsString) 8162 8163 def __processStartEditCommand(self, argsString): 8164 """ 8165 Private slot to process a remote StartEdit command. 8166 8167 @param argsString string containing the command parameters (string) 8168 """ 8169 if not self.__inSharedEdit and not self.__inRemoteSharedEdit: 8170 self.__inRemoteSharedEdit = True 8171 self.setReadOnly(True) 8172 self.__updateReadOnly() 8173 hashStr = str( 8174 QCryptographicHash.hash( 8175 Utilities.encode(self.text(), self.encoding)[0], 8176 QCryptographicHash.Algorithm.Sha1).toHex(), 8177 encoding="utf-8") 8178 if hashStr != argsString: 8179 # text is different to the remote site, request to sync it 8180 self.__isSyncing = True 8181 self.__send(Editor.RequestSyncToken, argsString) 8182 8183 def __calculateChanges(self, old, new): 8184 """ 8185 Private method to determine change commands to convert old text into 8186 new text. 8187 8188 @param old old text (string) 8189 @param new new text (string) 8190 @return commands to change old into new (string) 8191 """ 8192 oldL = old.splitlines() 8193 newL = new.splitlines() 8194 matcher = difflib.SequenceMatcher(None, oldL, newL) 8195 8196 formatStr = "@@{0} {1} {2} {3}" 8197 commands = [] 8198 for token, i1, i2, j1, j2 in matcher.get_opcodes(): 8199 if token == "insert": # secok 8200 commands.append(formatStr.format("i", j1, j2 - j1, -1)) 8201 commands.extend(newL[j1:j2]) 8202 elif token == "delete": # secok 8203 commands.append(formatStr.format("d", j1, i2 - i1, -1)) 8204 elif token == "replace": # secok 8205 commands.append(formatStr.format("r", j1, i2 - i1, j2 - j1)) 8206 commands.extend(newL[j1:j2]) 8207 8208 return "\n".join(commands) + "\n" 8209 8210 def __processEndEditCommand(self, argsString): 8211 """ 8212 Private slot to process a remote EndEdit command. 8213 8214 @param argsString string containing the command parameters (string) 8215 """ 8216 commands = argsString.splitlines() 8217 sep = self.getLineSeparator() 8218 cur = self.getCursorPosition() 8219 8220 self.setReadOnly(False) 8221 self.beginUndoAction() 8222 while commands: 8223 commandLine = commands.pop(0) 8224 if not commandLine.startswith("@@"): 8225 continue 8226 8227 args = commandLine.split() 8228 command = args.pop(0) 8229 pos, l1, l2 = [int(arg) for arg in args] 8230 if command == "@@i": 8231 txt = sep.join(commands[0:l1]) + sep 8232 self.insertAt(txt, pos, 0) 8233 del commands[0:l1] 8234 elif command == "@@d": 8235 self.setSelection(pos, 0, pos + l1, 0) 8236 self.removeSelectedText() 8237 elif command == "@@r": 8238 self.setSelection(pos, 0, pos + l1, 0) 8239 self.removeSelectedText() 8240 txt = sep.join(commands[0:l2]) + sep 8241 self.insertAt(txt, pos, 0) 8242 del commands[0:l2] 8243 self.endUndoAction() 8244 self.__updateReadOnly() 8245 self.__inRemoteSharedEdit = False 8246 8247 self.setCursorPosition(*cur) 8248 8249 def __processRequestSyncCommand(self, argsString): 8250 """ 8251 Private slot to process a remote RequestSync command. 8252 8253 @param argsString string containing the command parameters (string) 8254 """ 8255 if self.__inSharedEdit: 8256 hashStr = str( 8257 QCryptographicHash.hash( 8258 Utilities.encode(self.__savedText, self.encoding)[0], 8259 QCryptographicHash.Algorithm.Sha1).toHex(), 8260 encoding="utf-8") 8261 8262 if hashStr == argsString: 8263 self.__send(Editor.SyncToken, self.__savedText) 8264 8265 def __processSyncCommand(self, argsString): 8266 """ 8267 Private slot to process a remote Sync command. 8268 8269 @param argsString string containing the command parameters (string) 8270 """ 8271 if self.__isSyncing: 8272 cur = self.getCursorPosition() 8273 8274 self.setReadOnly(False) 8275 self.beginUndoAction() 8276 self.selectAll() 8277 self.removeSelectedText() 8278 self.insertAt(argsString, 0, 0) 8279 self.endUndoAction() 8280 self.setReadOnly(True) 8281 8282 self.setCursorPosition(*cur) 8283 8284 while self.__receivedWhileSyncing: 8285 command = self.__receivedWhileSyncing.pop(0) 8286 self.__dispatchCommand(command) 8287 8288 self.__isSyncing = False 8289 8290 ####################################################################### 8291 ## Special search related methods 8292 ####################################################################### 8293 8294 def searchCurrentWordForward(self): 8295 """ 8296 Public slot to search the current word forward. 8297 """ 8298 self.__searchCurrentWord(forward=True) 8299 8300 def searchCurrentWordBackward(self): 8301 """ 8302 Public slot to search the current word backward. 8303 """ 8304 self.__searchCurrentWord(forward=False) 8305 8306 def __searchCurrentWord(self, forward=True): 8307 """ 8308 Private slot to search the next occurrence of the current word. 8309 8310 @param forward flag indicating the search direction (boolean) 8311 """ 8312 self.hideFindIndicator() 8313 line, index = self.getCursorPosition() 8314 word = self.getCurrentWord() 8315 wordStart, wordEnd = self.getCurrentWordBoundaries() 8316 wordStartPos = self.positionFromLineIndex(line, wordStart) 8317 wordEndPos = self.positionFromLineIndex(line, wordEnd) 8318 8319 regExp = re.compile(r"\b{0}\b".format(word)) 8320 startPos = wordEndPos if forward else wordStartPos 8321 8322 matches = [m for m in regExp.finditer(self.text())] 8323 if matches: 8324 if forward: 8325 matchesAfter = [m for m in matches if m.start() >= startPos] 8326 if matchesAfter: 8327 match = matchesAfter[0] 8328 else: 8329 # wrap around 8330 match = matches[0] 8331 else: 8332 matchesBefore = [m for m in matches if m.start() < startPos] 8333 if matchesBefore: 8334 match = matchesBefore[-1] 8335 else: 8336 # wrap around 8337 match = matches[-1] 8338 line, index = self.lineIndexFromPosition(match.start()) 8339 self.setSelection(line, index + len(match.group(0)), line, index) 8340 self.showFindIndicator(line, index, 8341 line, index + len(match.group(0))) 8342 8343 ####################################################################### 8344 ## Sort related methods 8345 ####################################################################### 8346 8347 def sortLines(self): 8348 """ 8349 Public slot to sort the lines spanned by a rectangular selection. 8350 """ 8351 if not self.selectionIsRectangle(): 8352 return 8353 8354 from .SortOptionsDialog import SortOptionsDialog 8355 dlg = SortOptionsDialog() 8356 if dlg.exec() == QDialog.DialogCode.Accepted: 8357 ascending, alnum, caseSensitive = dlg.getData() 8358 origStartLine, origStartIndex, origEndLine, origEndIndex = ( 8359 self.getRectangularSelection() 8360 ) 8361 # convert to upper-left to lower-right 8362 startLine = min(origStartLine, origEndLine) 8363 startIndex = min(origStartIndex, origEndIndex) 8364 endLine = max(origStartLine, origEndLine) 8365 endIndex = max(origStartIndex, origEndIndex) 8366 8367 # step 1: extract the text of the rectangular selection and 8368 # the lines 8369 selText = {} 8370 txtLines = {} 8371 for line in range(startLine, endLine + 1): 8372 txtLines[line] = self.text(line) 8373 txt = txtLines[line][startIndex:endIndex].strip() 8374 if not alnum: 8375 try: 8376 txt = float(txt) 8377 except ValueError: 8378 E5MessageBox.critical( 8379 self, 8380 self.tr("Sort Lines"), 8381 self.tr( 8382 """The selection contains illegal data for a""" 8383 """ numerical sort.""")) 8384 return 8385 8386 if txt in selText: 8387 selText[txt].append(line) 8388 else: 8389 selText[txt] = [line] 8390 8391 # step 2: calculate the sort parameters 8392 reverse = not ascending 8393 if alnum and not caseSensitive: 8394 keyFun = str.lower 8395 else: 8396 keyFun = None 8397 8398 # step 3: sort the lines 8399 eol = self.getLineSeparator() 8400 lastWithEol = True 8401 newLines = [] 8402 for txt in sorted(selText.keys(), key=keyFun, reverse=reverse): 8403 for line in selText[txt]: 8404 txt = txtLines[line] 8405 if not txt.endswith(eol): 8406 lastWithEol = False 8407 txt += eol 8408 newLines.append(txt) 8409 if not lastWithEol: 8410 newLines[-1] = newLines[-1][:-len(eol)] 8411 8412 # step 4: replace the lines by the sorted ones 8413 self.setSelection(startLine, 0, endLine + 1, 0) 8414 self.beginUndoAction() 8415 self.replaceSelectedText("".join(newLines)) 8416 self.endUndoAction() 8417 8418 # step 5: reset the rectangular selection 8419 self.setRectangularSelection(origStartLine, origStartIndex, 8420 origEndLine, origEndIndex) 8421 self.selectionChanged.emit() 8422 8423 ####################################################################### 8424 ## Mouse click handler related methods 8425 ####################################################################### 8426 8427 def mouseReleaseEvent(self, evt): 8428 """ 8429 Protected method calling a registered mouse click handler function. 8430 8431 @param evt event object 8432 @type QMouseEvent 8433 """ 8434 modifiers = evt.modifiers() 8435 button = evt.button() 8436 key = (int(modifiers), int(button)) 8437 8438 self.vm.eventFilter(self, evt) 8439 super().mouseReleaseEvent(evt) 8440 8441 if ( 8442 button != Qt.MouseButton.NoButton and 8443 Preferences.getEditor("MouseClickHandlersEnabled") and 8444 key in self.__mouseClickHandlers 8445 ): 8446 evt.accept() 8447 self.__mouseClickHandlers[key][1](self) 8448 8449 def setMouseClickHandler(self, name, modifiers, button, function): 8450 """ 8451 Public method to set a mouse click handler. 8452 8453 @param name name of the plug-in (or 'internal') setting this handler 8454 @type str 8455 @param modifiers keyboard modifiers of the handler 8456 @type Qt.KeyboardModifiers or int 8457 @param button mouse button of the handler 8458 @type Qt.MouseButton or int 8459 @param function handler function 8460 @type func 8461 @return flag indicating success 8462 @rtype bool 8463 """ 8464 if int(button): 8465 key = (int(modifiers), int(button)) 8466 if key in self.__mouseClickHandlers: 8467 E5MessageBox.warning( 8468 self, 8469 self.tr("Register Mouse Click Handler"), 8470 self.tr("""A mouse click handler for "{0}" was already""" 8471 """ registered by "{1}". Aborting request by""" 8472 """ "{2}"...""").format( 8473 MouseUtilities.MouseButtonModifier2String( 8474 modifiers, button), 8475 self.__mouseClickHandlers[key][0], 8476 name)) 8477 return False 8478 8479 self.__mouseClickHandlers[key] = (name, function) 8480 return True 8481 8482 return False 8483 8484 def getMouseClickHandler(self, modifiers, button): 8485 """ 8486 Public method to get a registered mouse click handler. 8487 8488 @param modifiers keyboard modifiers of the handler 8489 @type Qt.KeyboardModifiers 8490 @param button mouse button of the handler 8491 @type Qt.MouseButton 8492 @return plug-in name and registered function 8493 @rtype tuple of str and func 8494 """ 8495 key = (int(modifiers), int(button)) 8496 if key in self.__mouseClickHandlers: 8497 return self.__mouseClickHandlers[key] 8498 else: 8499 return ("", None) 8500 8501 def getMouseClickHandlers(self, name): 8502 """ 8503 Public method to get all registered mouse click handlers of 8504 a plug-in. 8505 8506 @param name name of the plug-in 8507 @type str 8508 @return registered mouse click handlers as list of modifiers, 8509 mouse button and function 8510 @rtype list of tuple of (Qt.KeyboardModifiers, Qt.MouseButton,func) 8511 """ 8512 lst = [] 8513 for key, value in self.__mouseClickHandlers.items(): 8514 if value[0] == name: 8515 lst.append((key[0], key[1], value[1])) 8516 return lst 8517 8518 def removeMouseClickHandler(self, modifiers, button): 8519 """ 8520 Public method to un-registered a mouse click handler. 8521 8522 @param modifiers keyboard modifiers of the handler 8523 @type Qt.KeyboardModifiers 8524 @param button mouse button of the handler 8525 @type Qt.MouseButton 8526 """ 8527 key = (int(modifiers), int(button)) 8528 if key in self.__mouseClickHandlers: 8529 del self.__mouseClickHandlers[key] 8530 8531 def removeMouseClickHandlers(self, name): 8532 """ 8533 Public method to un-registered all mouse click handlers of 8534 a plug-in. 8535 8536 @param name name of the plug-in 8537 @type str 8538 """ 8539 keys = [] 8540 for key in self.__mouseClickHandlers: 8541 if self.__mouseClickHandlers[key][0] == name: 8542 keys.append(key) 8543 for key in keys: 8544 del self.__mouseClickHandlers[key] 8545 8546 def __executeSelection(self): 8547 """ 8548 Private slot to execute the selected text in the shell window. 8549 """ 8550 txt = self.selectedText() 8551 e5App().getObject("Shell").executeLines(txt) 8552 8553 ####################################################################### 8554 ## Methods implementing the interface to EditorConfig 8555 ####################################################################### 8556 8557 def __loadEditorConfig(self, fileName=""): 8558 """ 8559 Private method to load the EditorConfig properties. 8560 8561 @param fileName name of the file 8562 @type str 8563 """ 8564 if not fileName: 8565 fileName = self.fileName 8566 8567 self.__editorConfig = self.__loadEditorConfigObject(fileName) 8568 8569 if fileName: 8570 self.__setTabAndIndent() 8571 8572 def __loadEditorConfigObject(self, fileName): 8573 """ 8574 Private method to load the EditorConfig properties for the given 8575 file name. 8576 8577 @param fileName name of the file 8578 @type str 8579 @return EditorConfig dictionary 8580 @rtype dict 8581 """ 8582 editorConfig = {} 8583 8584 if fileName: 8585 try: 8586 editorConfig = editorconfig.get_properties(fileName) 8587 except editorconfig.EditorConfigError: 8588 E5MessageBox.warning( 8589 self, 8590 self.tr("EditorConfig Properties"), 8591 self.tr("""<p>The EditorConfig properties for file""" 8592 """ <b>{0}</b> could not be loaded.</p>""") 8593 .format(fileName)) 8594 8595 return editorConfig 8596 8597 def __getEditorConfig(self, option, nodefault=False, config=None): 8598 """ 8599 Private method to get the requested option via EditorConfig. 8600 8601 If there is no EditorConfig defined, the equivalent built-in option 8602 will be used (Preferences.getEditor() ). The option must be given as 8603 the Preferences option key. The mapping to the EditorConfig option name 8604 will be done within this method. 8605 8606 @param option Preferences option key 8607 @type str 8608 @param nodefault flag indicating to not get the default value from 8609 Preferences but return None instead 8610 @type bool 8611 @param config reference to an EditorConfig object or None 8612 @type dict 8613 @return value of requested setting or None if nothing was found and 8614 nodefault parameter was True 8615 @rtype any 8616 """ 8617 if config is None: 8618 config = self.__editorConfig 8619 8620 if not config: 8621 if nodefault: 8622 return None 8623 else: 8624 value = self.__getOverrideValue(option) 8625 if value is None: 8626 # no override 8627 value = Preferences.getEditor(option) 8628 return value 8629 8630 try: 8631 if option == "EOLMode": 8632 value = config["end_of_line"] 8633 if value == "lf": 8634 value = QsciScintilla.EolMode.EolUnix 8635 elif value == "crlf": 8636 value = QsciScintilla.EolMode.EolWindows 8637 elif value == "cr": 8638 value = QsciScintilla.EolMode.EolMac 8639 else: 8640 value = None 8641 elif option == "DefaultEncoding": 8642 value = config["charset"] 8643 elif option == "InsertFinalNewline": 8644 value = Utilities.toBool(config["insert_final_newline"]) 8645 elif option == "StripTrailingWhitespace": 8646 value = Utilities.toBool(config["trim_trailing_whitespace"]) 8647 elif option == "TabWidth": 8648 value = int(config["tab_width"]) 8649 elif option == "IndentWidth": 8650 value = config["indent_size"] 8651 if value == "tab": 8652 value = self.__getEditorConfig("TabWidth", config=config) 8653 else: 8654 value = int(value) 8655 elif option == "TabForIndentation": 8656 value = config["indent_style"] == "tab" 8657 except KeyError: 8658 value = None 8659 8660 if value is None and not nodefault: 8661 # use Preferences in case of error 8662 value = self.__getOverrideValue(option) 8663 if value is None: 8664 # no override 8665 value = Preferences.getEditor(option) 8666 8667 return value 8668 8669 def getEditorConfig(self, option): 8670 """ 8671 Public method to get the requested option via EditorConfig. 8672 8673 @param option Preferences option key 8674 @type str 8675 @return value of requested setting 8676 @rtype any 8677 """ 8678 return self.__getEditorConfig(option) 8679 8680 def __getOverrideValue(self, option): 8681 """ 8682 Private method to get an override value for the current file type. 8683 8684 @param option Preferences option key 8685 @type str 8686 @return override value; None in case nothing is defined 8687 @rtype any 8688 """ 8689 if option in ("TabWidth", "IndentWidth"): 8690 overrides = Preferences.getEditor("TabIndentOverride") 8691 language = self.filetype or self.apiLanguage 8692 if language in overrides: 8693 if option == "TabWidth": 8694 return overrides[language][0] 8695 elif option == "IndentWidth": 8696 return overrides[language][1] 8697 8698 return None 8699 8700 ####################################################################### 8701 ## Methods implementing the docstring generator interface 8702 ####################################################################### 8703 8704 def getDocstringGenerator(self): 8705 """ 8706 Public method to get a reference to the docstring generator. 8707 8708 @return reference to the docstring generator 8709 @rtype BaseDocstringGenerator 8710 """ 8711 if self.__docstringGenerator is None: 8712 from . import DocstringGenerator 8713 self.__docstringGenerator = ( 8714 DocstringGenerator.getDocstringGenerator(self) 8715 ) 8716 8717 return self.__docstringGenerator 8718 8719 def insertDocstring(self): 8720 """ 8721 Public method to generate and insert a docstring for the function under 8722 the cursor. 8723 8724 Note: This method is called via a keyboard shortcut or through the 8725 global 'Edit' menu. 8726 """ 8727 generator = self.getDocstringGenerator() 8728 generator.insertDocstringFromShortcut(self.getCursorPosition()) 8729 8730 @pyqtSlot() 8731 def __insertDocstring(self): 8732 """ 8733 Private slot to generate and insert a docstring for the function under 8734 the cursor. 8735 """ 8736 generator = self.getDocstringGenerator() 8737 generator.insertDocstring(self.getCursorPosition(), fromStart=True) 8738 8739 def __delayedDocstringMenuPopup(self, cursorPosition): 8740 """ 8741 Private method to test, if the user might want to insert a docstring. 8742 8743 @param cursorPosition current cursor position (line and column) 8744 @type tuple of (int, int) 8745 """ 8746 if ( 8747 Preferences.getEditor("DocstringAutoGenerate") and 8748 self.getDocstringGenerator().isDocstringIntro(cursorPosition) 8749 ): 8750 lineText2Cursor = self.text(cursorPosition[0])[:cursorPosition[1]] 8751 8752 QTimer.singleShot( 8753 300, 8754 lambda: self.__popupDocstringMenu(lineText2Cursor, 8755 cursorPosition) 8756 ) 8757 8758 def __popupDocstringMenu(self, lastLineText, lastCursorPosition): 8759 """ 8760 Private slot to pop up a menu asking the user, if a docstring should be 8761 inserted. 8762 8763 @param lastLineText line contents when the delay timer was started 8764 @type str 8765 @param lastCursorPosition position of the cursor when the delay timer 8766 was started (line and index) 8767 @type tuple of (int, int) 8768 """ 8769 cursorPosition = self.getCursorPosition() 8770 if lastCursorPosition != cursorPosition: 8771 return 8772 8773 if self.text(cursorPosition[0])[:cursorPosition[1]] != lastLineText: 8774 return 8775 8776 generator = self.getDocstringGenerator() 8777 if generator.hasFunctionDefinition(cursorPosition): 8778 from .DocstringGenerator.BaseDocstringGenerator import ( 8779 DocstringMenuForEnterOnly 8780 ) 8781 docstringMenu = DocstringMenuForEnterOnly(self) 8782 act = docstringMenu.addAction( 8783 UI.PixmapCache.getIcon("fileText"), 8784 self.tr("Generate Docstring"), 8785 lambda: generator.insertDocstring(cursorPosition, 8786 fromStart=False) 8787 ) 8788 docstringMenu.setActiveAction(act) 8789 docstringMenu.popup( 8790 self.mapToGlobal(self.getGlobalCursorPosition())) 8791