1# -*- coding: utf-8 -*- 2 3# Copyright (c) 2011 - 2021 Detlev Offenbach <detlev@die-offenbachs.de> 4# 5 6""" 7Module implementing the editor assembly widget containing the navigation 8combos and the editor widget. 9""" 10 11import contextlib 12 13from PyQt5.QtCore import QTimer 14from PyQt5.QtWidgets import QWidget, QGridLayout, QComboBox 15 16from E5Gui.E5Application import e5App 17 18import UI.PixmapCache 19import Preferences 20 21 22class EditorAssembly(QWidget): 23 """ 24 Class implementing the editor assembly widget containing the navigation 25 combos and the editor widget. 26 """ 27 def __init__(self, dbs, fn="", vm=None, filetype="", editor=None, 28 tv=None): 29 """ 30 Constructor 31 32 @param dbs reference to the debug server object 33 @type DebugServer 34 @param fn name of the file to be opened. If it is None, 35 a new (empty) editor is opened. 36 @type str 37 @param vm reference to the view manager object 38 @type ViewManager.ViewManager 39 @param filetype type of the source file 40 @type str 41 @param editor reference to an Editor object, if this is a cloned view 42 @type Editor 43 @param tv reference to the task viewer object 44 @type TaskViewer 45 """ 46 super().__init__() 47 48 self.__layout = QGridLayout(self) 49 self.__layout.setContentsMargins(0, 0, 0, 0) 50 self.__layout.setSpacing(1) 51 52 from .EditorButtonsWidget import EditorButtonsWidget 53 from .Editor import Editor 54 from .EditorOutline import EditorOutlineView 55 56 self.__showOutline = Preferences.getEditor("ShowSourceOutline") 57 58 self.__editor = Editor(dbs, fn, vm, filetype, editor, tv) 59 self.__buttonsWidget = EditorButtonsWidget(self.__editor, self) 60 self.__globalsCombo = QComboBox() 61 self.__globalsCombo.setDuplicatesEnabled(True) 62 self.__membersCombo = QComboBox() 63 self.__membersCombo.setDuplicatesEnabled(True) 64 self.__sourceOutline = EditorOutlineView( 65 self.__editor, populate=self.__showOutline) 66 self.__sourceOutline.setMaximumWidth( 67 Preferences.getEditor("SourceOutlineWidth")) 68 69 self.__layout.addWidget(self.__buttonsWidget, 1, 0, -1, 1) 70 self.__layout.addWidget(self.__globalsCombo, 0, 1) 71 self.__layout.addWidget(self.__membersCombo, 0, 2) 72 self.__layout.addWidget(self.__editor, 1, 1, 1, 2) 73 self.__layout.addWidget(self.__sourceOutline, 0, 3, -1, -1) 74 75 self.setFocusProxy(self.__editor) 76 77 self.__module = None 78 79 self.__shutdownTimerCalled = False 80 self.__parseTimer = QTimer(self) 81 self.__parseTimer.setSingleShot(True) 82 self.__parseTimer.setInterval(5 * 1000) 83 self.__editor.textChanged.connect(self.__resetParseTimer) 84 self.__editor.refreshed.connect(self.__resetParseTimer) 85 86 self.__selectedGlobal = "" 87 self.__selectedMember = "" 88 self.__globalsBoundaries = {} 89 self.__membersBoundaries = {} 90 91 self.__activateOutline(self.__showOutline) 92 self.__activateCombos(not self.__showOutline) 93 94 e5App().getObject("UserInterface").preferencesChanged.connect( 95 self.__preferencesChanged) 96 97 def shutdownTimer(self): 98 """ 99 Public method to stop and disconnect the timer. 100 """ 101 self.__parseTimer.stop() 102 if not self.__shutdownTimerCalled: 103 self.__editor.textChanged.disconnect(self.__resetParseTimer) 104 self.__editor.refreshed.disconnect(self.__resetParseTimer) 105 self.__shutdownTimerCalled = True 106 107 def getEditor(self): 108 """ 109 Public method to get the reference to the editor widget. 110 111 @return reference to the editor widget 112 @rtype Editor 113 """ 114 return self.__editor 115 116 def __preferencesChanged(self): 117 """ 118 Private slot handling a change of preferences. 119 """ 120 showOutline = Preferences.getEditor("ShowSourceOutline") 121 if showOutline != self.__showOutline: 122 self.__showOutline = showOutline 123 self.__activateOutline(self.__showOutline) 124 self.__activateCombos(not self.__showOutline) 125 126 ####################################################################### 127 ## Methods dealing with the navigation combos below 128 ####################################################################### 129 130 def __activateCombos(self, activate): 131 """ 132 Private slot to activate the navigation combo boxes. 133 134 @param activate flag indicating to activate the combo boxes 135 @type bool 136 """ 137 self.__globalsCombo.setVisible(activate) 138 self.__membersCombo.setVisible(activate) 139 if activate: 140 self.__globalsCombo.activated[int].connect( 141 self.__globalsActivated) 142 self.__membersCombo.activated[int].connect( 143 self.__membersActivated) 144 self.__editor.cursorLineChanged.connect( 145 self.__editorCursorLineChanged) 146 self.__parseTimer.timeout.connect(self.__parseEditor) 147 148 self.__parseEditor() 149 150 line, _ = self.__editor.getCursorPosition() 151 self.__editorCursorLineChanged(line) 152 else: 153 with contextlib.suppress(TypeError): 154 self.__globalsCombo.activated[int].disconnect( 155 self.__globalsActivated) 156 self.__membersCombo.activated[int].disconnect( 157 self.__membersActivated) 158 self.__editor.cursorLineChanged.disconnect( 159 self.__editorCursorLineChanged) 160 self.__parseTimer.timeout.disconnect(self.__parseEditor) 161 162 self.__globalsCombo.clear() 163 self.__membersCombo.clear() 164 self.__globalsBoundaries = {} 165 self.__membersBoundaries = {} 166 167 def __globalsActivated(self, index, moveCursor=True): 168 """ 169 Private method to jump to the line of the selected global entry and to 170 populate the members combo box. 171 172 @param index index of the selected entry 173 @type int 174 @param moveCursor flag indicating to move the editor cursor 175 @type bool 176 """ 177 # step 1: go to the line of the selected entry 178 lineno = self.__globalsCombo.itemData(index) 179 if lineno is not None: 180 if moveCursor: 181 txt = self.__editor.text(lineno - 1).rstrip() 182 pos = len(txt.replace(txt.strip(), "")) 183 self.__editor.gotoLine( 184 lineno, pos if pos == 0 else pos + 1, True) 185 self.__editor.setFocus() 186 187 # step 2: populate the members combo, if the entry is a class 188 self.__membersCombo.clear() 189 self.__membersBoundaries = {} 190 self.__membersCombo.addItem("") 191 memberIndex = 0 192 entryName = self.__globalsCombo.itemText(index) 193 if self.__module: 194 if entryName in self.__module.classes: 195 entry = self.__module.classes[entryName] 196 elif entryName in self.__module.modules: 197 entry = self.__module.modules[entryName] 198 # step 2.0: add module classes 199 items = [] 200 for cl in entry.classes.values(): 201 if cl.isPrivate(): 202 icon = UI.PixmapCache.getIcon("class_private") 203 elif cl.isProtected(): 204 icon = UI.PixmapCache.getIcon( 205 "class_protected") 206 else: 207 icon = UI.PixmapCache.getIcon("class") 208 items.append((icon, cl.name, cl.lineno, cl.endlineno)) 209 for itm in sorted(items, key=lambda x: (x[1], x[2])): 210 self.__membersCombo.addItem(itm[0], itm[1], itm[2]) 211 memberIndex += 1 212 self.__membersBoundaries[(itm[2], itm[3])] = ( 213 memberIndex 214 ) 215 else: 216 return 217 218 # step 2.1: add class methods 219 from Utilities.ModuleParser import Function 220 items = [] 221 for meth in entry.methods.values(): 222 if meth.modifier == Function.Static: 223 icon = UI.PixmapCache.getIcon("method_static") 224 elif meth.modifier == Function.Class: 225 icon = UI.PixmapCache.getIcon("method_class") 226 elif meth.isPrivate(): 227 icon = UI.PixmapCache.getIcon("method_private") 228 elif meth.isProtected(): 229 icon = UI.PixmapCache.getIcon("method_protected") 230 else: 231 icon = UI.PixmapCache.getIcon("method") 232 items.append( 233 (icon, meth.name, meth.lineno, meth.endlineno) 234 ) 235 for itm in sorted(items, key=lambda x: (x[1], x[2])): 236 self.__membersCombo.addItem(itm[0], itm[1], itm[2]) 237 memberIndex += 1 238 self.__membersBoundaries[(itm[2], itm[3])] = memberIndex 239 240 # step 2.2: add class instance attributes 241 items = [] 242 for attr in entry.attributes.values(): 243 if attr.isPrivate(): 244 icon = UI.PixmapCache.getIcon("attribute_private") 245 elif attr.isProtected(): 246 icon = UI.PixmapCache.getIcon( 247 "attribute_protected") 248 else: 249 icon = UI.PixmapCache.getIcon("attribute") 250 items.append((icon, attr.name, attr.lineno)) 251 for itm in sorted(items, key=lambda x: (x[1], x[2])): 252 self.__membersCombo.addItem(itm[0], itm[1], itm[2]) 253 254 # step 2.3: add class attributes 255 items = [] 256 icon = UI.PixmapCache.getIcon("attribute_class") 257 for globalVar in entry.globals.values(): 258 items.append((icon, globalVar.name, globalVar.lineno)) 259 for itm in sorted(items, key=lambda x: (x[1], x[2])): 260 self.__membersCombo.addItem(itm[0], itm[1], itm[2]) 261 262 def __membersActivated(self, index, moveCursor=True): 263 """ 264 Private method to jump to the line of the selected members entry. 265 266 @param index index of the selected entry 267 @type int 268 @param moveCursor flag indicating to move the editor cursor 269 @type bool 270 """ 271 lineno = self.__membersCombo.itemData(index) 272 if lineno is not None and moveCursor: 273 txt = self.__editor.text(lineno - 1).rstrip() 274 pos = len(txt.replace(txt.strip(), "")) 275 self.__editor.gotoLine(lineno, pos if pos == 0 else pos + 1, 276 firstVisible=True, expand=True) 277 self.__editor.setFocus() 278 279 def __resetParseTimer(self): 280 """ 281 Private slot to reset the parse timer. 282 """ 283 self.__parseTimer.stop() 284 self.__parseTimer.start() 285 286 def __parseEditor(self): 287 """ 288 Private method to parse the editor source and repopulate the globals 289 combo. 290 """ 291 from Utilities.ModuleParser import Module, getTypeFromTypeName 292 293 self.__module = None 294 sourceType = getTypeFromTypeName(self.__editor.determineFileType()) 295 if sourceType != -1: 296 src = self.__editor.text() 297 if src: 298 fn = self.__editor.getFileName() 299 if fn is None: 300 fn = "" 301 self.__module = Module("", fn, sourceType) 302 self.__module.scan(src) 303 304 # remember the current selections 305 self.__selectedGlobal = self.__globalsCombo.currentText() 306 self.__selectedMember = self.__membersCombo.currentText() 307 308 self.__globalsCombo.clear() 309 self.__membersCombo.clear() 310 self.__globalsBoundaries = {} 311 self.__membersBoundaries = {} 312 313 self.__globalsCombo.addItem("") 314 index = 0 315 316 # step 1: add modules 317 items = [] 318 for module in self.__module.modules.values(): 319 items.append( 320 (UI.PixmapCache.getIcon("module"), module.name, 321 module.lineno, module.endlineno) 322 ) 323 for itm in sorted(items, key=lambda x: (x[1], x[2])): 324 self.__globalsCombo.addItem(itm[0], itm[1], itm[2]) 325 index += 1 326 self.__globalsBoundaries[(itm[2], itm[3])] = index 327 328 # step 2: add classes 329 items = [] 330 for cl in self.__module.classes.values(): 331 if cl.isPrivate(): 332 icon = UI.PixmapCache.getIcon("class_private") 333 elif cl.isProtected(): 334 icon = UI.PixmapCache.getIcon("class_protected") 335 else: 336 icon = UI.PixmapCache.getIcon("class") 337 items.append( 338 (icon, cl.name, cl.lineno, cl.endlineno) 339 ) 340 for itm in sorted(items, key=lambda x: (x[1], x[2])): 341 self.__globalsCombo.addItem(itm[0], itm[1], itm[2]) 342 index += 1 343 self.__globalsBoundaries[(itm[2], itm[3])] = index 344 345 # step 3: add functions 346 items = [] 347 for func in self.__module.functions.values(): 348 if func.isPrivate(): 349 icon = UI.PixmapCache.getIcon("method_private") 350 elif func.isProtected(): 351 icon = UI.PixmapCache.getIcon("method_protected") 352 else: 353 icon = UI.PixmapCache.getIcon("method") 354 items.append( 355 (icon, func.name, func.lineno, func.endlineno) 356 ) 357 for itm in sorted(items, key=lambda x: (x[1], x[2])): 358 self.__globalsCombo.addItem(itm[0], itm[1], itm[2]) 359 index += 1 360 self.__globalsBoundaries[(itm[2], itm[3])] = index 361 362 # step 4: add attributes 363 items = [] 364 for globalValue in self.__module.globals.values(): 365 if globalValue.isPrivate(): 366 icon = UI.PixmapCache.getIcon("attribute_private") 367 elif globalValue.isProtected(): 368 icon = UI.PixmapCache.getIcon( 369 "attribute_protected") 370 else: 371 icon = UI.PixmapCache.getIcon("attribute") 372 items.append( 373 (icon, globalValue.name, globalValue.lineno) 374 ) 375 for itm in sorted(items, key=lambda x: (x[1], x[2])): 376 self.__globalsCombo.addItem(itm[0], itm[1], itm[2]) 377 378 # reset the currently selected entries without moving the 379 # text cursor 380 index = self.__globalsCombo.findText(self.__selectedGlobal) 381 if index != -1: 382 self.__globalsCombo.setCurrentIndex(index) 383 self.__globalsActivated(index, moveCursor=False) 384 index = self.__membersCombo.findText(self.__selectedMember) 385 if index != -1: 386 self.__membersCombo.setCurrentIndex(index) 387 self.__membersActivated(index, moveCursor=False) 388 else: 389 self.__globalsCombo.clear() 390 self.__membersCombo.clear() 391 self.__globalsBoundaries = {} 392 self.__membersBoundaries = {} 393 394 def __editorCursorLineChanged(self, lineno): 395 """ 396 Private slot handling a line change of the cursor of the editor. 397 398 @param lineno line number of the cursor 399 @type int 400 """ 401 lineno += 1 # cursor position is zero based, code info one based 402 403 # step 1: search in the globals 404 indexFound = 0 405 for (lower, upper), index in self.__globalsBoundaries.items(): 406 if upper == -1: 407 upper = 1000000 # it is the last line 408 if lower <= lineno <= upper: 409 indexFound = index 410 break 411 self.__globalsCombo.setCurrentIndex(indexFound) 412 self.__globalsActivated(indexFound, moveCursor=False) 413 414 # step 2: search in members 415 indexFound = 0 416 for (lower, upper), index in self.__membersBoundaries.items(): 417 if upper == -1: 418 upper = 1000000 # it is the last line 419 if lower <= lineno <= upper: 420 indexFound = index 421 break 422 self.__membersCombo.setCurrentIndex(indexFound) 423 self.__membersActivated(indexFound, moveCursor=False) 424 425 ####################################################################### 426 ## Methods dealing with the source outline below 427 ####################################################################### 428 429 def __activateOutline(self, activate): 430 """ 431 Private slot to activate the source outline view. 432 433 @param activate flag indicating to activate the source outline view 434 @type bool 435 """ 436 self.__sourceOutline.setActive(activate) 437 438 if activate: 439 self.__sourceOutline.setVisible( 440 self.__sourceOutline.isSupportedLanguage( 441 self.__editor.getLanguage() 442 ) 443 ) 444 445 self.__parseTimer.timeout.connect(self.__sourceOutline.repopulate) 446 self.__editor.languageChanged.connect(self.__editorChanged) 447 self.__editor.editorRenamed.connect(self.__editorChanged) 448 else: 449 self.__sourceOutline.hide() 450 451 with contextlib.suppress(TypeError): 452 self.__parseTimer.timeout.disconnect( 453 self.__sourceOutline.repopulate) 454 self.__editor.languageChanged.disconnect(self.__editorChanged) 455 self.__editor.editorRenamed.disconnect(self.__editorChanged) 456 457 def __editorChanged(self): 458 """ 459 Private slot handling changes of the editor language or file name. 460 """ 461 supported = self.__sourceOutline.isSupportedLanguage( 462 self.__editor.getLanguage()) 463 464 self.__sourceOutline.setVisible(supported) 465 466# 467# eflag: noqa = Y113 468