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