1# -*- coding: utf-8 -*- 2 3# Copyright (c) 2002 - 2021 Detlev Offenbach <detlev@die-offenbachs.de> 4# 5 6""" 7Module implementing the variables viewer view based on QTreeView. 8""" 9 10import ast 11import re 12import contextlib 13 14from PyQt5.QtCore import ( 15 Qt, QAbstractItemModel, QModelIndex, QCoreApplication, 16 QSortFilterProxyModel, pyqtSignal 17) 18from PyQt5.QtGui import QBrush, QFontMetrics 19from PyQt5.QtWidgets import QTreeView, QAbstractItemView, QToolTip, QMenu 20 21from E5Gui.E5Application import e5App 22 23from .Config import ConfigVarTypeDispStrings 24from DebugClients.Python.DebugConfig import ConfigQtNames, ConfigKnownQtTypes 25 26import Preferences 27import Utilities 28 29SORT_ROLE = Qt.ItemDataRole.UserRole 30 31 32class VariableItem: 33 """ 34 Class implementing the data structure for all variable items. 35 """ 36 Type2Indicators = { 37 # Python types 38 'list': '[]', 39 'tuple': '()', 40 'dict': '{:}', # __IGNORE_WARNING_M613__ 41 'set': '{}', # __IGNORE_WARNING_M613__ 42 'frozenset': '{}', # __IGNORE_WARNING_M613__ 43 'numpy.ndarray': '[ndarray]', # __IGNORE_WARNING_M613__ 44 } 45 46 # Initialize regular expression for unprintable strings 47 rx_nonprintable = re.compile(r"""(\\x\d\d)+""") 48 49 noOfItemsStr = QCoreApplication.translate("VariablesViewer", "{0} items") 50 unsized = QCoreApplication.translate("VariablesViewer", "unsized") 51 52 arrayTypes = { 53 'list', 'tuple', 'dict', 'set', 'frozenset', 'numpy.ndarray', 54 'django.MultiValueDict', 'array.array', 'collections.defaultdict', 55 "class 'dict_items'", "class 'dict_keys'", "class 'dict_values'", 56 } 57 58 nonExpandableTypes = ( 59 'method_descriptor', 'wrapper_descriptor', '', 'getset_descriptor', 60 'method-wrapper', 'member_descriptor', 61 ) 62 63 def __init__(self, parent, dvar, dtype, dvalue): 64 """ 65 Constructor 66 67 @param parent reference to the parent item 68 @type VariableItem 69 @param dvar variable name 70 @type str 71 @param dtype type string 72 @type str 73 @param dvalue value string 74 @type str 75 """ 76 self.parent = parent 77 # Take the additional methods into account for childCount 78 self.methodCount = 0 79 self.childCount = 0 80 self.currentCount = -1 # -1 indicates to (re)load children 81 # Indicator that there are children 82 self.hasChildren = False 83 self.populated = False 84 # Indicator that item was at least once fully populated 85 self.wasPopulated = False 86 87 self.children = [] 88 # Flag to prevent endless reloading of current item while waiting on 89 # a response from debugger 90 self.pendigFetch = False 91 92 # Set of child items, which are displayed the first time or changed 93 self.newItems = set() 94 self.changedItems = set() 95 # Name including its ID if it's a dict, set, etc. 96 self.nameWithId = dvar 97 98 self.name = '' 99 self.sort = '' 100 self.type = '' 101 self.indicator = '' 102 self.value = None 103 self.valueShort = None 104 self.tooltip = '' 105 106 self.__getName(dvar) 107 self.__getType(dtype) 108 self.__getValue(dtype, dvalue) 109 110 def __getName(self, dvar): 111 """ 112 Private method to extract the variable name. 113 114 @param dvar name of variable maybe with ID 115 @type str 116 """ 117 try: 118 idx = dvar.index(" (ID:") 119 dvar = dvar[:idx] 120 except AttributeError: 121 idx = dvar 122 dvar = str(dvar) 123 except ValueError: 124 pass 125 126 self.name = dvar 127 try: 128 # Convert numbers to strings with preceding zeros 129 sort = int(dvar) 130 sort = "{0:06}".format(sort) 131 except ValueError: 132 sort = dvar.lower() 133 134 self.sort = sort 135 136 def __getType(self, dtype): 137 """ 138 Private method to process the type of the variable. 139 140 If type is known to have children, the corresponding flag is set. 141 142 @param dtype type string 143 @type str 144 """ 145 # Python class? 146 if dtype.startswith('class '): 147 dtype = dtype[7:-1] 148 # Qt related stuff? 149 elif ( 150 (dtype.startswith(ConfigQtNames) and 151 dtype.endswith(ConfigKnownQtTypes)) or 152 dtype in ('instance', 'class') 153 ): 154 self.hasChildren = True 155 156 # Special Qt types should not be expanded infinite 157 elif ".{0}".format(dtype) in ConfigKnownQtTypes: 158 self.type = dtype # It's a Qt type, so skipping translation is ok 159 return 160 161 vtype = ConfigVarTypeDispStrings.get(dtype, dtype) 162 # Unkown types should be expandable by default 163 if vtype is dtype and dtype not in self.nonExpandableTypes: 164 self.hasChildren = True 165 self.type = QCoreApplication.translate("VariablesViewer", vtype) 166 167 def __getValue(self, dtype, dvalue): 168 """ 169 Private method to process the variables value. 170 171 Define and limit value, set tooltip text. If type is known to have 172 children, the corresponding flag is set. 173 174 @param dtype type string 175 @type str 176 @param dvalue value of variable encoded as utf-8 177 @type str 178 """ 179 if dtype == 'collections.defaultdict': 180 dvalue, default_factory = dvalue.split('|') 181 self.indicator = '{{:<{0}>}}'.format(default_factory) 182 elif dtype == 'array.array': 183 dvalue, typecode = dvalue.split('|') 184 self.indicator = '[<{0}>]'.format(typecode) 185 else: 186 self.indicator = VariableItem.Type2Indicators.get(dtype, '') 187 188 if dtype == 'numpy.ndarray': 189 if dvalue: 190 self.childCount = int(dvalue.split('x')[0]) 191 dvalue = VariableItem.noOfItemsStr.format(dvalue) 192 else: 193 dvalue = VariableItem.unsized 194 self.hasChildren = True 195 196 elif dtype in VariableItem.arrayTypes: 197 self.childCount = int(dvalue) 198 dvalue = VariableItem.noOfItemsStr.format(dvalue) 199 self.hasChildren = True 200 201 elif dtype == "Shiboken.EnumType": 202 self.hasChildren = True 203 204 elif dtype == 'str': 205 if VariableItem.rx_nonprintable.search(dvalue) is None: 206 with contextlib.suppress(Exception): 207 dvalue = ast.literal_eval(dvalue) 208 dvalue = str(dvalue) 209 210 elif ( 211 dvalue.startswith(("{", "(", "[")) and 212 dvalue.endswith(("}", ")", "]")) 213 ): 214 # it is most probably a dict, tuple or list derived class 215 with contextlib.suppress(Exception): 216 value = ast.literal_eval(dvalue) 217 valueTypeStr = str(type(value))[8:-2] 218 if valueTypeStr in VariableItem.arrayTypes: 219 self.childCount = len(value) 220 self.hasChildren = True 221 222 elif ( 223 (dvalue.endswith("})") and "({" in dvalue) or 224 (dvalue.endswith("])") and "([" in dvalue) 225 ): 226 # that is probably a set derived class 227 with contextlib.suppress(Exception): 228 value = ast.literal_eval(dvalue.split("(", 1)[1][:-1]) 229 valueTypeStr = str(type(value))[8:-2] 230 if valueTypeStr in VariableItem.arrayTypes: 231 self.childCount = len(value) 232 self.hasChildren = True 233 234 self.value = dvalue 235 236 if len(dvalue) > 2048: # 2 kB 237 self.tooltip = dvalue[:2048] 238 dvalue = QCoreApplication.translate( 239 "VariableItem", "<double click to show value>") 240 else: 241 self.tooltip = dvalue 242 243 lines = dvalue[:2048].splitlines() 244 if len(lines) > 1: 245 # only show the first non-empty line; 246 # indicate skipped lines by <...> at the 247 # beginning and/or end 248 index = 0 249 while index < len(lines) - 1 and lines[index].strip(' \t') == "": 250 index += 1 251 252 dvalue = "" 253 if index > 0: 254 dvalue += "<...>" 255 dvalue += lines[index] 256 if index < len(lines) - 1 or len(dvalue) > 2048: 257 dvalue += "<...>" 258 259 self.valueShort = dvalue 260 261 @property 262 def absolutCount(self): 263 """ 264 Public property to get the total number of children. 265 266 @return total number of children 267 @rtype int 268 """ 269 return self.childCount + self.methodCount 270 271 272class VariablesModel(QAbstractItemModel): 273 """ 274 Class implementing the data model for QTreeView. 275 276 @signal expand trigger QTreeView to expand given index 277 """ 278 expand = pyqtSignal(QModelIndex) 279 280 def __init__(self, treeView, globalScope): 281 """ 282 Constructor 283 284 @param treeView QTreeView showing the data 285 @type VariablesViewer 286 @param globalScope flag indicating global (True) or local (False) 287 variables 288 @type bool 289 """ 290 super().__init__() 291 self.treeView = treeView 292 self.proxyModel = treeView.proxyModel 293 294 self.framenr = -1 295 self.openItems = [] 296 self.closedItems = [] 297 298 visibility = self.tr("Globals") if globalScope else self.tr("Locals") 299 self.rootNode = VariableItem(None, visibility, self.tr("Type"), 300 self.tr("Value")) 301 302 self.__globalScope = globalScope 303 304 def clear(self, reset=False): 305 """ 306 Public method to clear the complete data model. 307 308 @param reset flag to clear the expanded keys also 309 @type bool 310 """ 311 self.beginResetModel() 312 self.rootNode.children = [] 313 self.rootNode.newItems.clear() 314 self.rootNode.changedItems.clear() 315 self.rootNode.wasPopulated = False 316 if reset: 317 self.openItems = [] 318 self.closedItems = [] 319 self.endResetModel() 320 321 def __findVariable(self, pathlist): 322 """ 323 Private method to get to the given variable. 324 325 @param pathlist full path to the variable 326 @type list of str 327 @return the found variable or None if it doesn't exist 328 @rtype VariableItem or None 329 """ 330 node = self.rootNode 331 332 for childName in pathlist or []: 333 for item in node.children: 334 if item.nameWithId == childName: 335 node = item 336 break 337 else: 338 return None 339 340 return node # __IGNORE_WARNING_M834__ 341 342 def showVariables(self, vlist, frmnr, pathlist=None): 343 """ 344 Public method to update the data model of variable in pathlist. 345 346 @param vlist the list of variables to be displayed. Each 347 list entry is a tuple of three values. 348 <ul> 349 <li>the variable name (string)</li> 350 <li>the variables type (string)</li> 351 <li>the variables value (string)</li> 352 </ul> 353 @type list of str 354 @param frmnr frame number (0 is the current frame) 355 @type int 356 @param pathlist full path to the variable 357 @type list of str 358 """ 359 if pathlist: 360 itemStartIndex = pathlist.pop(0) 361 else: 362 itemStartIndex = -1 363 if self.framenr != frmnr: 364 self.clear() 365 self.framenr = frmnr 366 367 parent = self.__findVariable(pathlist) 368 if parent is None: 369 return 370 371 parent.pendigFetch = False 372 373 if parent == self.rootNode: 374 parentIdx = QModelIndex() 375 parent.methodCount = len(vlist) 376 else: 377 row = parent.parent.children.index(parent) 378 parentIdx = self.createIndex(row, 0, parent) 379 380 if itemStartIndex == -3: 381 # Item doesn't exist any more 382 parentIdx = self.parent(parentIdx) 383 self.beginRemoveRows(parentIdx, row, row) 384 del parent.parent.children[row] 385 self.endRemoveRows() 386 parent.parent.childCount -= 1 387 return 388 389 elif itemStartIndex == -2: 390 parent.wasPopulated = True 391 parent.currentCount = parent.absolutCount 392 parent.populated = True 393 # Remove items which are left over at the end of child list 394 self.__cleanupParentList(parent, parentIdx) 395 return 396 397 elif itemStartIndex == -1: 398 parent.methodCount = len(vlist) 399 idx = max(parent.currentCount, 0) 400 parent.currentCount = idx + len(vlist) 401 parent.populated = True 402 else: 403 idx = itemStartIndex 404 parent.currentCount = idx + len(vlist) 405 406 # Sort items for Python versions where dict doesn't retain order 407 vlist.sort(key=lambda x: x[0]) 408 # Now update the table 409 endIndex = idx + len(vlist) 410 newChild = None 411 knownChildrenCount = len(parent.children) 412 while idx < endIndex: 413 # Fetch next old item from last cycle 414 try: 415 child = parent.children[idx] 416 except IndexError: 417 child = None 418 419 # Fetch possible new item 420 if not newChild and vlist: 421 newChild = vlist.pop(0) 422 423 # Process parameters of new item 424 newItem = VariableItem(parent, *newChild) 425 sort = newItem.sort 426 427 # Append or insert before already existing item 428 if child is None or newChild and sort < child.sort: 429 self.beginInsertRows(parentIdx, idx, idx) 430 parent.children.insert(idx, newItem) 431 if knownChildrenCount <= idx and not parent.wasPopulated: 432 parent.newItems.add(newItem) 433 knownChildrenCount += 1 434 else: 435 parent.changedItems.add(newItem) 436 self.endInsertRows() 437 438 idx += 1 439 newChild = None 440 continue 441 442 # Check if same name, type and afterwards value 443 elif sort == child.sort and child.type == newItem.type: 444 # Check if value has changed 445 if child.value != newItem.value: 446 child.value = newItem.value 447 child.valueShort = newItem.valueShort 448 child.tooltip = newItem.tooltip 449 child.nameWithId = newItem.nameWithId 450 451 child.currentCount = -1 452 child.populated = False 453 child.childCount = newItem.childCount 454 455 # Highlight item because it has changed 456 parent.changedItems.add(child) 457 458 changedIndexStart = self.index(idx, 0, parentIdx) 459 changedIndexEnd = self.index(idx, 2, parentIdx) 460 self.dataChanged.emit(changedIndexStart, changedIndexEnd) 461 462 newChild = None 463 idx += 1 464 continue 465 466 # Remove obsolete item 467 self.beginRemoveRows(parentIdx, idx, idx) 468 parent.children.remove(child) 469 self.endRemoveRows() 470 # idx stay unchanged 471 knownChildrenCount -= 1 472 473 # Remove items which are left over at the end of child list 474 if itemStartIndex == -1: 475 parent.wasPopulated = True 476 self.__cleanupParentList(parent, parentIdx) 477 478 # Request data for any expanded node 479 self.getMore() 480 481 def __cleanupParentList(self, parent, parentIdx): 482 """ 483 Private method to remove items which are left over at the end of the 484 child list. 485 486 @param parent to clean up 487 @type VariableItem 488 @param parentIdx the parent index as QModelIndex 489 @type QModelIndex 490 """ 491 end = len(parent.children) 492 if end > parent.absolutCount: 493 self.beginRemoveRows(parentIdx, parent.absolutCount, end) 494 del parent.children[parent.absolutCount:] 495 self.endRemoveRows() 496 497 def resetModifiedMarker(self, parentIdx=QModelIndex(), pathlist=()): 498 """ 499 Public method to remove the modified marker from changed items. 500 501 @param parentIdx item to reset marker 502 @type QModelIndex 503 @param pathlist full path to the variable 504 @type list of str 505 """ 506 parent = (parentIdx.internalPointer() if parentIdx.isValid() 507 else self.rootNode) 508 509 parent.newItems.clear() 510 parent.changedItems.clear() 511 512 pll = len(pathlist) 513 posPaths = {x for x in self.openItems if len(x) > pll} 514 posPaths |= {x for x in self.closedItems if len(x) > pll} 515 posPaths = {x[pll] for x in posPaths if x[:pll] == pathlist} 516 517 if posPaths: 518 for child in parent.children: 519 if ( 520 child.hasChildren and 521 child.nameWithId in posPaths and 522 child.currentCount >= 0 523 ): 524 # Discard loaded elements and refresh if still expanded 525 child.currentCount = -1 526 child.populated = False 527 row = parent.children.index(child) 528 newParentIdx = self.index(row, 0, parentIdx) 529 self.resetModifiedMarker( 530 newParentIdx, pathlist + (child.nameWithId,)) 531 532 self.closedItems = [] 533 534 # Little quirk: Refresh all visible items to clear the changed marker 535 if parentIdx == QModelIndex(): 536 self.rootNode.currentCount = -1 537 self.rootNode.populated = False 538 idxStart = self.index(0, 0, QModelIndex()) 539 idxEnd = self.index(0, 2, QModelIndex()) 540 self.dataChanged.emit(idxStart, idxEnd) 541 542 def columnCount(self, parent=QModelIndex()): 543 """ 544 Public method to get the column count. 545 546 @param parent the model parent 547 @type QModelIndex 548 @return number of columns 549 @rtype int 550 """ 551 return 3 552 553 def rowCount(self, parent=QModelIndex()): 554 """ 555 Public method to get the row count. 556 557 @param parent the model parent 558 @type QModelIndex 559 @return number of rows 560 @rtype int 561 """ 562 node = parent.internalPointer() if parent.isValid() else self.rootNode 563 564 return len(node.children) 565 566 def flags(self, index): 567 """ 568 Public method to get the item flags. 569 570 @param index of item 571 @type QModelIndex 572 @return item flags 573 @rtype QtCore.Qt.ItemFlag 574 """ 575 if not index.isValid(): 576 return Qt.ItemFlag.NoItemFlags 577 578 return Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsSelectable 579 580 def hasChildren(self, parent=QModelIndex()): 581 """ 582 Public method to get a flag if parent has children. 583 584 @param parent the model parent 585 @type QModelIndex 586 @return flag indicating parent has children 587 @rtype bool 588 """ 589 if not parent.isValid(): 590 return self.rootNode.children != [] 591 592 return parent.internalPointer().hasChildren 593 594 def index(self, row, column, parent=QModelIndex()): 595 """ 596 Public method to get the index of item at row:column of parent. 597 598 @param row number of rows 599 @type int 600 @param column number of columns 601 @type int 602 @param parent the model parent 603 @type QModelIndex 604 @return new model index for child 605 @rtype QModelIndex 606 """ 607 if not self.hasIndex(row, column, parent): 608 return QModelIndex() 609 610 node = parent.internalPointer() if parent.isValid() else self.rootNode 611 612 return self.createIndex(row, column, node.children[row]) 613 614 def parent(self, child): 615 """ 616 Public method to get the parent of the given child. 617 618 @param child the model child node 619 @type QModelIndex 620 @return new model index for parent 621 @rtype QModelIndex 622 """ 623 if not child.isValid(): 624 return QModelIndex() 625 626 childNode = child.internalPointer() 627 if childNode == self.rootNode: 628 return QModelIndex() 629 630 parentNode = childNode.parent 631 632 if parentNode == self.rootNode: 633 return QModelIndex() 634 635 row = parentNode.parent.children.index(parentNode) 636 return self.createIndex(row, 0, parentNode) 637 638 def data(self, index, role=Qt.ItemDataRole.DisplayRole): 639 """ 640 Public method get the role data of item. 641 642 @param index the model index 643 @type QModelIndex 644 @param role the requested data role 645 @type QtCore.Qt.ItemDataRole 646 @return role data of item 647 @rtype Any 648 """ 649 if not index.isValid() or index.row() < 0: 650 return None 651 652 node = index.internalPointer() 653 column = index.column() 654 655 if role in ( 656 Qt.ItemDataRole.DisplayRole, SORT_ROLE, Qt.ItemDataRole.EditRole 657 ): 658 try: 659 if column == 0: 660 # Sort first column with values from third column 661 if role == SORT_ROLE: 662 return node.sort 663 return node.name + node.indicator 664 else: 665 return { 666 1: node.valueShort, 667 2: node.type, 668 3: node.sort 669 }.get(column) 670 except AttributeError: 671 return ['None', '', '', ''][column] 672 673 elif role == Qt.ItemDataRole.BackgroundRole: 674 if node in node.parent.changedItems: 675 return self.__bgColorChanged 676 elif node in node.parent.newItems: 677 return self.__bgColorNew 678 679 elif role == Qt.ItemDataRole.ToolTipRole: 680 if column == 0: 681 tooltip = node.name + node.indicator 682 elif column == 1: 683 tooltip = node.tooltip 684 elif column == 2: 685 tooltip = node.type 686 elif column == 3: 687 tooltip = node.sort 688 else: 689 return None 690 691 if Qt.mightBeRichText(tooltip): 692 tooltip = Utilities.html_encode(tooltip) 693 694 if column == 0: 695 indentation = self.treeView.indentation() 696 indentCount = 0 697 currentNode = node 698 while currentNode.parent: 699 indentCount += 1 700 currentNode = currentNode.parent 701 702 indentation *= indentCount 703 else: 704 indentation = 0 705 # Check if text is longer than available space 706 fontMetrics = QFontMetrics(self.treeView.font()) 707 try: 708 textSize = fontMetrics.horizontalAdvance(tooltip) 709 except AttributeError: 710 textSize = fontMetrics.width(tooltip) 711 textSize += indentation + 5 # How to determine border size? 712 header = self.treeView.header() 713 if textSize >= header.sectionSize(column): 714 return tooltip 715 else: 716 QToolTip.hideText() 717 718 return None 719 720 def headerData(self, section, orientation, 721 role=Qt.ItemDataRole.DisplayRole): 722 """ 723 Public method get the header names. 724 725 @param section the header section (row/coulumn) 726 @type int 727 @param orientation the header's orientation 728 @type QtCore.Qt.Orientation 729 @param role the requested data role 730 @type QtCore.Qt.ItemDataRole 731 @return header name 732 @rtype str or None 733 """ 734 if ( 735 role != Qt.ItemDataRole.DisplayRole or 736 orientation != Qt.Orientation.Horizontal 737 ): 738 return None 739 740 return { 741 0: self.rootNode.name, 742 1: self.rootNode.value, 743 2: self.rootNode.type, 744 3: self.rootNode.sort 745 }.get(section) 746 747 def __findPendingItem(self, parent=None, pathlist=()): 748 """ 749 Private method to find the next item to request data from debugger. 750 751 @param parent the model parent 752 @type VariableItem 753 @param pathlist full path to the variable 754 @type list of str 755 @return next item index to request data from debugger 756 @rtype QModelIndex 757 """ 758 if parent is None: 759 parent = self.rootNode 760 761 for child in parent.children: 762 if not child.hasChildren: 763 continue 764 765 if pathlist + (child.nameWithId,) in self.openItems: 766 if child.populated: 767 index = None 768 else: 769 idx = parent.children.index(child) 770 index = self.createIndex(idx, 0, child) 771 self.expand.emit(index) 772 773 if child.currentCount < 0: 774 return index 775 776 possibleIndex = self.__findPendingItem( 777 child, pathlist + (child.nameWithId,)) 778 779 if (possibleIndex or index) is None: 780 continue 781 782 return possibleIndex or index 783 784 return None 785 786 def getMore(self): 787 """ 788 Public method to fetch the next variable from debugger. 789 """ 790 # step 1: find expanded but not populated items 791 item = self.__findPendingItem() 792 if not item or not item.isValid(): 793 return 794 795 # step 2: check if data has to be retrieved 796 node = item.internalPointer() 797 lastVisibleItem = self.index(node.currentCount - 1, 0, item) 798 lastVisibleItem = self.proxyModel.mapFromSource(lastVisibleItem) 799 rect = self.treeView.visualRect(lastVisibleItem) 800 if rect.y() > self.treeView.height() or node.pendigFetch: 801 return 802 803 node.pendigFetch = True 804 # step 3: get a pathlist up to the requested variable 805 pathlist = self.__buildTreePath(node) 806 # step 4: request the variable from the debugger 807 variablesFilter = e5App().getObject("DebugUI").variablesFilter( 808 self.__globalScope) 809 e5App().getObject("DebugServer").remoteClientVariable( 810 e5App().getObject("DebugUI").getSelectedDebuggerId(), 811 self.__globalScope, variablesFilter, pathlist, self.framenr) 812 813 def setExpanded(self, index, state): 814 """ 815 Public method to set the expanded state of item. 816 817 @param index item to change expanded state 818 @type QModelIndex 819 @param state state of the item 820 @type bool 821 """ 822 node = index.internalPointer() 823 pathlist = self.__buildTreePath(node) 824 if state: 825 if pathlist not in self.openItems: 826 self.openItems.append(pathlist) 827 if pathlist in self.closedItems: 828 self.closedItems.remove(pathlist) 829 self.getMore() 830 else: 831 if pathlist in self.openItems: 832 self.openItems.remove(pathlist) 833 self.closedItems.append(pathlist) 834 835 def __buildTreePath(self, parent): 836 """ 837 Private method to build up a path from the root to parent. 838 839 @param parent item to build the path for 840 @type VariableItem 841 @return list of names denoting the path from the root 842 @rtype tuple of str 843 """ 844 pathlist = [] 845 846 # build up a path from the top to the item 847 while parent.parent: 848 pathlist.append(parent.nameWithId) 849 parent = parent.parent 850 851 pathlist.reverse() 852 return tuple(pathlist) 853 854 def handlePreferencesChanged(self): 855 """ 856 Public slot to handle the preferencesChanged signal. 857 """ 858 self.__bgColorNew = QBrush(Preferences.getDebugger("BgColorNew")) 859 self.__bgColorChanged = QBrush( 860 Preferences.getDebugger("BgColorChanged")) 861 862 idxStart = self.index(0, 0, QModelIndex()) 863 idxEnd = self.index(0, 2, QModelIndex()) 864 self.dataChanged.emit(idxStart, idxEnd) 865 866 867class VariablesProxyModel(QSortFilterProxyModel): 868 """ 869 Class for handling the sort operations. 870 """ 871 def __init__(self, parent=None): 872 """ 873 Constructor 874 875 @param parent the parent model index 876 @type QModelIndex 877 """ 878 super().__init__(parent) 879 self.setSortRole(SORT_ROLE) 880 881 def hasChildren(self, parent): 882 """ 883 Public method to get a flag if parent has children. 884 885 The given model index has to be transformed to the underlying source 886 model to get the correct result. 887 888 @param parent the model parent 889 @type QModelIndex 890 @return flag if parent has children 891 @rtype bool 892 """ 893 return self.sourceModel().hasChildren(self.mapToSource(parent)) 894 895 def setExpanded(self, index, state): 896 """ 897 Public slot to get a flag if parent has children. 898 899 The given model index has to be transformed to the underlying source 900 model to get the correct result. 901 @param index item to change expanded state 902 @type QModelIndex 903 @param state state of the item 904 @type bool 905 """ 906 self.sourceModel().setExpanded(self.mapToSource(index), state) 907 908 909class VariablesViewer(QTreeView): 910 """ 911 Class implementing the variables viewer view. 912 913 This view is used to display the variables of the program being 914 debugged in a tree. Compound types will be shown with 915 their main entry first. Once the subtree has been expanded, the 916 individual entries will be shown. Double clicking an entry will 917 expand or collapse the item, if it has children and the double click 918 was performed on the first column of the tree, otherwise it'll 919 popup a dialog showing the variables parameters in a more readable 920 form. This is especially useful for lengthy strings. 921 922 This view has two modes for displaying the global and the local 923 variables. 924 925 @signal preferencesChanged() to inform model about new background colours 926 """ 927 preferencesChanged = pyqtSignal() 928 929 def __init__(self, viewer, globalScope, parent=None): 930 """ 931 Constructor 932 933 @param viewer reference to the debug viewer object 934 @type DebugViewer 935 @param globalScope flag indicating global (True) or local (False) 936 variables 937 @type bool 938 @param parent the parent 939 @type QWidget 940 """ 941 super().__init__(parent) 942 943 self.__debugViewer = viewer 944 self.__globalScope = globalScope 945 self.framenr = 0 946 947 # Massive performance gain 948 self.setUniformRowHeights(True) 949 950 # Implements sorting and filtering 951 self.proxyModel = VariablesProxyModel() 952 # Variable model implements the underlying data model 953 self.varModel = VariablesModel(self, globalScope) 954 self.proxyModel.setSourceModel(self.varModel) 955 self.setModel(self.proxyModel) 956 self.preferencesChanged.connect(self.varModel.handlePreferencesChanged) 957 self.preferencesChanged.emit() # Force initialization of colors 958 959 self.expanded.connect( 960 lambda idx: self.proxyModel.setExpanded(idx, True)) 961 self.collapsed.connect( 962 lambda idx: self.proxyModel.setExpanded(idx, False)) 963 964 self.setExpandsOnDoubleClick(False) 965 self.doubleClicked.connect(self.__itemDoubleClicked) 966 967 self.varModel.expand.connect(self.__mdlRequestExpand) 968 969 self.setSortingEnabled(True) 970 self.setAlternatingRowColors(True) 971 self.setSelectionBehavior( 972 QAbstractItemView.SelectionBehavior.SelectRows) 973 974 if self.__globalScope: 975 self.setWindowTitle(self.tr("Global Variables")) 976 self.setWhatsThis(self.tr( 977 """<b>The Global Variables Viewer Window</b>""" 978 """<p>This window displays the global variables""" 979 """ of the debugged program.</p>""" 980 )) 981 else: 982 self.setWindowTitle(self.tr("Local Variables")) 983 self.setWhatsThis(self.tr( 984 """<b>The Local Variables Viewer Window</b>""" 985 """<p>This window displays the local variables""" 986 """ of the debugged program.</p>""" 987 )) 988 989 header = self.header() 990 header.setSortIndicator(0, Qt.SortOrder.AscendingOrder) 991 header.setSortIndicatorShown(True) 992 993 try: 994 header.setSectionsClickable(True) 995 except Exception: 996 header.setClickable(True) 997 998 header.resizeSection(0, 130) # variable column 999 header.resizeSection(1, 180) # value column 1000 header.resizeSection(2, 50) # type column 1001 1002 header.sortIndicatorChanged.connect(lambda *x: self.varModel.getMore()) 1003 1004 self.__createPopupMenus() 1005 self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) 1006 self.customContextMenuRequested.connect(self.__showContextMenu) 1007 1008 self.resortEnabled = True 1009 1010 def showVariables(self, vlist, frmnr): 1011 """ 1012 Public method to show variables in a list. 1013 1014 @param vlist the list of variables to be displayed. Each 1015 list entry is a tuple of three values. 1016 <ul> 1017 <li>the variable name (string)</li> 1018 <li>the variables type (string)</li> 1019 <li>the variables value (string)</li> 1020 </ul> 1021 @type list 1022 @param frmnr frame number (0 is the current frame) 1023 @type int 1024 """ 1025 self.varModel.resetModifiedMarker() 1026 self.varModel.showVariables(vlist, frmnr) 1027 1028 def showVariable(self, vlist): 1029 """ 1030 Public method to show variables in a list. 1031 1032 @param vlist the list of subitems to be displayed. 1033 The first element gives the path of the 1034 parent variable. Each other list entry is 1035 a tuple of three values. 1036 <ul> 1037 <li>the variable name (string)</li> 1038 <li>the variables type (string)</li> 1039 <li>the variables value (string)</li> 1040 </ul> 1041 @type list 1042 """ 1043 self.varModel.showVariables(vlist[1:], 0, vlist[0]) 1044 1045 def handleResetUI(self): 1046 """ 1047 Public method to reset the VariablesViewer. 1048 """ 1049 self.varModel.clear(True) 1050 1051 def verticalScrollbarValueChanged(self, value): 1052 """ 1053 Public slot informing about the scrollbar change. 1054 1055 @param value current value of the vertical scrollbar 1056 @type int 1057 """ 1058 self.varModel.getMore() 1059 super().verticalScrollbarValueChanged(value) 1060 1061 def resizeEvent(self, event): 1062 """ 1063 Protected slot informing about the widget size change. 1064 1065 @param event information 1066 @type QResizeEvent 1067 """ 1068 self.varModel.getMore() 1069 super().resizeEvent(event) 1070 1071 def __itemDoubleClicked(self, index): 1072 """ 1073 Private method called if an item was double clicked. 1074 1075 @param index the double clicked item 1076 @type QModelIndex 1077 """ 1078 node = self.proxyModel.mapToSource(index).internalPointer() 1079 if node.hasChildren and index.column() == 0: 1080 state = self.isExpanded(index) 1081 self.setExpanded(index, not state) 1082 else: 1083 self.__showVariableDetails(index) 1084 1085 def __mdlRequestExpand(self, modelIndex): 1086 """ 1087 Private method to inform the view about items to be expand. 1088 1089 @param modelIndex the model index 1090 @type QModelIndex 1091 """ 1092 index = self.proxyModel.mapFromSource(modelIndex) 1093 self.expand(index) 1094 1095 def __createPopupMenus(self): 1096 """ 1097 Private method to generate the popup menus. 1098 """ 1099 self.menu = QMenu() 1100 self.menu.addAction(self.tr("Show Details..."), self.__showDetails) 1101 self.menu.addSeparator() 1102 self.menu.addAction(self.tr("Expand"), self.__expandChildren) 1103 self.menu.addAction(self.tr("Collapse"), self.__collapseChildren) 1104 self.menu.addAction(self.tr("Collapse All"), self.collapseAll) 1105 self.menu.addSeparator() 1106 self.menu.addAction(self.tr("Refresh"), self.__refreshView) 1107 self.menu.addSeparator() 1108 self.menu.addAction(self.tr("Configure..."), self.__configure) 1109 self.menu.addAction(self.tr("Variables Type Filter..."), 1110 self.__configureFilter) 1111 1112 self.backMenu = QMenu() 1113 self.backMenu.addAction(self.tr("Refresh"), self.__refreshView) 1114 self.backMenu.addSeparator() 1115 self.backMenu.addAction(self.tr("Configure..."), self.__configure) 1116 self.backMenu.addAction(self.tr("Variables Type Filter..."), 1117 self.__configureFilter) 1118 1119 def __showContextMenu(self, coord): 1120 """ 1121 Private slot to show the context menu. 1122 1123 @param coord the position of the mouse pointer 1124 @type QPoint 1125 """ 1126 gcoord = self.mapToGlobal(coord) 1127 if self.indexAt(coord).isValid(): 1128 self.menu.popup(gcoord) 1129 else: 1130 self.backMenu.popup(gcoord) 1131 1132 def __expandChildren(self): 1133 """ 1134 Private slot to expand all child items of current parent. 1135 """ 1136 index = self.currentIndex() 1137 node = self.proxyModel.mapToSource(index).internalPointer() 1138 for child in node.children: 1139 if child.hasChildren: 1140 row = node.children.index(child) 1141 idx = self.varModel.createIndex(row, 0, child) 1142 idx = self.proxyModel.mapFromSource(idx) 1143 self.expand(idx) 1144 1145 def __collapseChildren(self): 1146 """ 1147 Private slot to collapse all child items of current parent. 1148 """ 1149 index = self.currentIndex() 1150 node = self.proxyModel.mapToSource(index).internalPointer() 1151 for child in node.children: 1152 row = node.children.index(child) 1153 idx = self.varModel.createIndex(row, 0, child) 1154 idx = self.proxyModel.mapFromSource(idx) 1155 if self.isExpanded(idx): 1156 self.collapse(idx) 1157 1158 def __refreshView(self): 1159 """ 1160 Private slot to refresh the view. 1161 """ 1162 if self.__globalScope: 1163 self.__debugViewer.setGlobalsFilter() 1164 else: 1165 self.__debugViewer.setLocalsFilter() 1166 1167 def __showDetails(self): 1168 """ 1169 Private slot to show details about the selected variable. 1170 """ 1171 idx = self.currentIndex() 1172 self.__showVariableDetails(idx) 1173 1174 def __showVariableDetails(self, index): 1175 """ 1176 Private method to show details about a variable. 1177 1178 @param index reference to the variable item 1179 @type QModelIndex 1180 """ 1181 node = self.proxyModel.mapToSource(index).internalPointer() 1182 1183 val = node.value 1184 vtype = node.type 1185 name = node.name 1186 1187 par = node.parent 1188 nlist = [name] 1189 1190 # build up the fully qualified name 1191 while par.parent is not None: 1192 pname = par.name 1193 if par.indicator: 1194 if nlist[0].endswith("."): 1195 nlist[0] = '[{0}].'.format(nlist[0][:-1]) 1196 else: 1197 nlist[0] = '[{0}]'.format(nlist[0]) 1198 nlist.insert(0, pname) 1199 else: 1200 if par.type == "django.MultiValueDict": 1201 nlist[0] = 'getlist({0})'.format(nlist[0]) 1202 elif par.type == "numpy.ndarray": 1203 if nlist and nlist[0][0].isalpha(): 1204 if nlist[0] in ["min", "max", "mean"]: 1205 nlist[0] = ".{0}()".format(nlist[0]) 1206 else: 1207 nlist[0] = ".{0}".format(nlist[0]) 1208 nlist.insert(0, pname) 1209 else: 1210 nlist.insert(0, '{0}.'.format(pname)) 1211 par = par.parent 1212 1213 name = ''.join(nlist) 1214 # now show the dialog 1215 from .VariableDetailDialog import VariableDetailDialog 1216 dlg = VariableDetailDialog(name, vtype, val) 1217 dlg.exec() 1218 1219 def __configure(self): 1220 """ 1221 Private method to open the configuration dialog. 1222 """ 1223 e5App().getObject("UserInterface").showPreferences( 1224 "debuggerGeneralPage") 1225 1226 def __configureFilter(self): 1227 """ 1228 Private method to open the variables filter dialog. 1229 """ 1230 e5App().getObject("DebugUI").dbgFilterAct.triggered.emit() 1231 1232# 1233# eflag: noqa = M822 1234