--- a/eric6/Debugger/VariablesViewer.py Sun Apr 21 17:27:52 2019 +0200 +++ b/eric6/Debugger/VariablesViewer.py Sun Apr 21 21:20:24 2019 +0200 @@ -4,33 +4,37 @@ # """ -Module implementing the variables viewer widget. +Module implementing the variables viewer view based on QTreeView. """ from __future__ import unicode_literals + try: str = unicode except NameError: pass -from PyQt5.QtCore import Qt, QRegExp, QCoreApplication -from PyQt5.QtWidgets import QTreeWidget, QTreeWidgetItem, QAbstractItemView, \ - QMenu +import ast + +from PyQt5.QtCore import (Qt, QAbstractItemModel, QModelIndex, QRegExp, + QCoreApplication, QSortFilterProxyModel, pyqtSignal) +from PyQt5.QtGui import QBrush, QColor, QFontMetrics +from PyQt5.QtWidgets import QTreeView, QAbstractItemView, QToolTip, QMenu from E5Gui.E5Application import e5App from .Config import ConfigVarTypeDispStrings +from DebugClients.Python.DebugConfig import ConfigQtNames, ConfigKnownQtTypes -import Preferences import Utilities -from Globals import qVersionTuple + +SORT_ROLE = Qt.UserRole -class VariableItem(QTreeWidgetItem): +class VariableItem(object): """ - Class implementing the data structure for variable items. + Class implementing the data structure for all variable items. """ - Indicators = ("()", "[]", "{:}", "{}") # __IGNORE_WARNING_M613__ Type2Indicators = { # Python types 'list': '[]', @@ -38,330 +42,859 @@ 'dict': '{:}', # __IGNORE_WARNING_M613__ 'set': '{}', # __IGNORE_WARNING_M613__ 'frozenset': '{}', # __IGNORE_WARNING_M613__ + 'numpy.ndarray': '[ndarray]', # __IGNORE_WARNING_M613__ } - def __init__(self, parent, dvar, dvalue, dtype): + # Initialize regular expression for unprintable strings + rx_nonprintable = QRegExp(r"""(\\x\d\d)+""") + + noOfItemsStr = QCoreApplication.translate("VariablesViewer", "{0} items") + + arrayTypes = {'list', 'tuple', 'dict', 'set', 'frozenset', + 'numpy.ndarray', 'django.MultiValueDict', 'array.array', + 'collections.defaultdict'} + + def __init__(self, parent, dvar, dtype, dvalue): """ Constructor @param parent reference to the parent item - @param dvar variable name (string) - @param dvalue value string (string) - @param dtype type string (string) + @type VariableItem + @param dvar variable name + @type str + @param dtype type string + @type str + @param dvalue value string + @type str + """ + self.parent = parent + # Take the additional methods into account for childCount + self.methodCount = 0 + self.childCount = 0 + self.currentCount = -1 # -1 indicates to (re)load childs + # Indicator that there are childs + self.hasChilds = False + + self.childs = [] + # Flag to prevent endless reloading of current item while waiting on + # a response from debugger + self.pendigFetch = False + + # Set of childs items, which are displayed the first time or changed + self.newItems = set() + # Name including its ID if it's a dict, set, etc. + self.nameWithId = dvar + + self.name = '' + self.sort = '' + self.type = '' + self.indicator = '' + self.value = None + self.valueShort = None + self.tooltip = '' + + self.__getName(dvar) + self.__getType(dtype) + self.__getValue(dtype, dvalue) + + def __getName(self, dvar): + """ + Private method to extract the variable name. + + @param dvar name of variable maybe with ID + @type str + """ + try: + idx = dvar.index(" (ID:") + dvar = dvar[:idx] + except ValueError: + pass + + self.name = dvar + try: + # Convert numbers to strings with preceding zeros + sort = int(dvar) + sort = "{0:06}".format(sort) + except ValueError: + sort = dvar.lower() + + self.sort = sort + + def __getType(self, dtype): """ - dvar, self.__varID = VariableItem.extractId(dvar) + Private method to process the type of the variable. + + If type is known to have childs, the corresponding flag is set. + @param dtype type string + @type str + """ + # Python class? + if dtype.startswith('class '): + dtype = dtype[7:-1] + self.hasChilds = True + elif dtype == 'classobj': + dtype = 'instance' + # Qt related stuff? + elif (dtype.startswith(ConfigQtNames) and + dtype.endswith(ConfigKnownQtTypes)): + self.hasChilds = True + + vtype = ConfigVarTypeDispStrings.get(dtype, dtype) + self.type = QCoreApplication.translate("VariablesViewer", vtype) + + def __getValue(self, dtype, dvalue): + """ + Private method to process the variables value. - self.__value = dvalue - if len(dvalue) > 2048: # 1024 * 2 + Define and limit value, set tooltip text. + If type is known to have childs, the corresponding flag is set. + @param dtype type string + @type str + @param dvalue value of variable encoded as utf-8 + @type str + """ + if dtype == 'collections.defaultdict': + dvalue, default_factory = dvalue.split('|') + self.indicator = '{{:<{0}>}}'.format(default_factory[7:-2]) + elif dtype == 'array.array': + dvalue, typecode = dvalue.split('|') + self.indicator = '[<{0}>]'.format(typecode) + else: + self.indicator = VariableItem.Type2Indicators.get(dtype, '') + + if dtype == 'numpy.ndarray': + self.childCount = int(dvalue.split('x')[0]) + dvalue = VariableItem.noOfItemsStr.format(dvalue) + self.hasChilds = True + elif dtype in VariableItem.arrayTypes: + self.childCount = int(dvalue) + dvalue = VariableItem.noOfItemsStr.format(dvalue) + self.hasChilds = True + + elif dtype == "Shiboken.EnumType": + self.hasChilds = True + + elif dtype in ['str', 'unicode']: + if VariableItem.rx_nonprintable.indexIn(dvalue) == -1: + try: + dvalue = ast.literal_eval(dvalue) + except Exception: + pass + try: + dvalue = str(dvalue) + except UnicodeDecodeError: # Never reached under Python 3 + dvalue = unicode(dvalue, 'utf-8') + + self.value = dvalue + + if len(dvalue) > 2048: # 2 kB + self.tooltip = dvalue[:2048] dvalue = QCoreApplication.translate( "VariableItem", "<double click to show value>") - self.__tooltip = dvalue - elif dvalue == "@@TOO_BIG_TO_SHOW@@": - dvalue = QCoreApplication.translate( - "VariableItem", "<variable value is too big>") else: - if Qt.mightBeRichText(dvalue): - self.__tooltip = Utilities.html_encode(dvalue) - else: - self.__tooltip = dvalue - lines = dvalue.splitlines() - if len(lines) > 1: - # only show the first non-empty line; - # indicate skipped lines by <...> at the - # beginning and/or end - index = 0 - while index < len(lines) - 1 and lines[index] == "": - index += 1 - dvalue = "" - if index > 0: - dvalue += "<...>" - dvalue += lines[index] - if index < len(lines) - 1: - dvalue += "<...>" - - super(VariableItem, self).__init__(parent, [dvar, dvalue, dtype]) - - self.populated = True - - def getValue(self): - """ - Public method to return the value of the item. + self.tooltip = dvalue - @return value of the item (string) - """ - return self.__value - - def getId(self): - """ - Public method to get the ID string. - - @return ID string - @rtype str - """ - return self.__varID - - @classmethod - def extractId(cls, var): - """ - Class method to extract the ID string from a variable text. - - @param var variable text - @type str - @return tuple containing the variable text without ID and the ID string - @rtype tuple of two str - """ - if " (ID:" in var: - dvar, varID = var.rsplit(None, 1) - if varID.endswith(VariableItem.Indicators): - varID, indicators = VariableItem.extractIndicators(varID) - dvar += indicators - else: - dvar = var - varID = None - - return dvar, varID - - @classmethod - def extractIndicators(cls, var): - """ - Class method to extract the indicator string from a variable text. + lines = dvalue[:2048].splitlines() + if len(lines) > 1: + # only show the first non-empty line; + # indicate skipped lines by <...> at the + # beginning and/or end + index = 0 + while index < len(lines) - 1 and lines[index].strip(' \t') == "": + index += 1 + + dvalue = "" + if index > 0: + dvalue += "<...>" + dvalue += lines[index] + if index < len(lines) - 1 or len(dvalue) > 2048: + dvalue += "<...>" - @param var variable text - @type str - @return tuple containing the variable text without indicators and the - indicator string - @rtype tuple of two str - """ - for indicator in VariableItem.Indicators: - if var.endswith(indicator): - return var[:-len(indicator)], indicator - - return var, "" + self.valueShort = dvalue - def _buildKey(self): + @property + def absolutCount(self): """ - Protected method to build the access key for the variable. - - @return access key - @rtype str - """ - indicators = "" - txt = self.text(0) - if txt.endswith(VariableItem.Indicators): - txt, indicators = VariableItem.extractIndicators(txt) - if self.__varID: - txt = "{0} {1}{2}".format(txt, self.__varID, indicators) - else: - txt = "{0}{1}".format(txt, indicators) - return txt - - def data(self, column, role): - """ - Public method to return the data for the requested role. - - This implementation changes the original behavior in a way, that the - display data is returned as the tooltip for column 1. + Public property to get the total number of childs. - @param column column number (integer) - @param role data role (Qt.ItemDataRole) - @return requested data - """ - if column == 1 and role == Qt.ToolTipRole: - return self.__tooltip - return super(VariableItem, self).data(column, role) - - def attachDummy(self): - """ - Public method to attach a dummy sub item to allow for lazy population. - """ - QTreeWidgetItem(self, ["DUMMY"]) - - def deleteChildren(self): - """ - Public method to delete all children (cleaning the subtree). + @return total number of childs + @rtype int """ - for itm in self.takeChildren(): - del itm - - def expand(self): - """ - Public method to expand the item. - - Note: This is just a do nothing and should be overwritten. + return self.childCount + self.methodCount + + @property + def populated(self): """ - return + Public property returning a flag indicating if item is fully populated. - def collapse(self): + @return item is fully populated + @rtype bool """ - Public method to collapse the item. - - Note: This is just a do nothing and should be overwritten. - """ - return + return self.currentCount >= (self.childCount + self.methodCount) -class SpecialVarItem(VariableItem): +class VariableModel(QAbstractItemModel): """ - Class implementing a VariableItem that represents a special variable node. + Class implementing the data model for QTreeView. - These special variable nodes are generated for classes, lists, - tuples and dictionaries. + @signal expand trigger QTreeView to expand given index """ - def __init__(self, parent, dvar, dvalue, dtype, frmnr, globalScope): + expand = pyqtSignal(QModelIndex) + + def __init__(self, treeView, globalScope): """ Constructor - @param parent parent of this item - @param dvar variable name (string) - @param dvalue value string (string) - @param dtype type string (string) - @param frmnr frame number (0 is the current frame) (int) + @param treeView QTreeView showing the data + @type VariablesViewer @param globalScope flag indicating global (True) or local (False) variables + @type bool """ - VariableItem.__init__(self, parent, dvar, dvalue, dtype) - self.attachDummy() - self.populated = False + super(VariableModel, self).__init__() + self.treeView = treeView + self.proxyModel = treeView.proxyModel + + self.framenr = -1 + self.openItems = [] + self.closedItems = [] + + if globalScope: + visibility = self.tr("Globals") + else: + visibility = self.tr("Locals") + + self.rootNode = VariableItem(None, visibility, self.tr("Type"), + self.tr("Value")) + + self.__globalScope = globalScope + + def clear(self, reset=False): + """ + Public method to clear the complete data model. + + @param reset flag to clear the expanded keys also + @type bool + """ + self.beginResetModel() + self.rootNode.childs = [] + self.rootNode.newItems.clear() + if reset: + self.openItems = [] + self.closedItems = [] + self.endResetModel() + + def __findVariable(self, pathlist): + """ + Private method to get to the given variable. + + @param pathlist full path to the variable + @type list of str + @return the found variable or None if it doesn't exist + @rtype VariableItem or None + """ + node = self.rootNode + + for childName in pathlist or []: + for item in node.childs: + if item.nameWithId == childName: + node = item + break + else: + return None + + return node # __IGNORE_WARNING_M834__ + + def showVariables(self, vlist, frmnr, pathlist=None): + """ + Public method to update the data model of variable in pathlist. + + @param vlist the list of variables to be displayed. Each + list entry is a tuple of three values. + <ul> + <li>the variable name (string)</li> + <li>the variables type (string)</li> + <li>the variables value (string)</li> + </ul> + @type list of str + @param frmnr frame number (0 is the current frame) + @type int + @param pathlist full path to the variable + @type list of str + """ + if pathlist: + itemStartIndex = pathlist.pop(0) + else: + itemStartIndex = -1 + if self.framenr != frmnr: + self.clear() + self.framenr = frmnr + + parent = self.__findVariable(pathlist) + if parent is None: + return + + parent.pendigFetch = False + + if parent == self.rootNode: + parentIdx = QModelIndex() + parent.methodCount = len(vlist) + else: + row = parent.parent.childs.index(parent) + parentIdx = self.createIndex(row, 0, parent) + + if itemStartIndex == -2: + parent.currentCount = parent.absolutCount + # Remove items which are left over at the end of child list + self.__cleanupParentList(parent, parentIdx) + return + + elif itemStartIndex == -1: + parent.methodCount = len(vlist) + parent.currentCount = parent.absolutCount + idx = parent.childCount + else: + parent.currentCount += len(vlist) + idx = itemStartIndex - self.framenr = frmnr - self.globalScope = globalScope - - def expand(self): + # Sort items for Python versions where dict doesn't retain order + vlist.sort(key=lambda x: x[0]) + # Now update the table + endIndex = idx + len(vlist) + newChild = None + while idx < endIndex: + # Fetch next old item from last cycle + try: + child = parent.childs[idx] + except IndexError: + child = None + + # Fetch possible new item + if not newChild and vlist: + newChild = vlist.pop(0) + + # Process parameters of new item + newItem = VariableItem(parent, *newChild) + sort = newItem.sort + + # Append or insert before already existing item + if child is None or newChild and sort < child.sort: + self.beginInsertRows(parentIdx, idx, idx) + parent.childs.insert(idx, newItem) + parent.newItems.add(newItem) + self.endInsertRows() + idx += 1 + newChild = None + continue + + # Check if same name, type and afterwards value + elif sort == child.sort and child.type == newItem.type: + # Check if value has changed + if child.value != newItem.value: + child.value = newItem.value + child.valueShort = newItem.valueShort + child.tooltip = newItem.tooltip + + child.currentCount = -1 + child.childCount = newItem.childCount + + # Highlight item because it has changed + parent.newItems.add(child) + + changedIndexStart = self.index(idx, 0, parentIdx) + changedIndexEnd = self.index(idx, 2, parentIdx) + self.dataChanged.emit(changedIndexStart, changedIndexEnd) + + newChild = None + idx += 1 + continue + + # Remove obsolete item + self.beginRemoveRows(parentIdx, idx, idx) + parent.childs.remove(child) + self.endRemoveRows() + # idx stay unchanged + + # Remove items which are left over at the end of child list + if itemStartIndex == -1: + self.__cleanupParentList(parent, parentIdx) + + # Request data for any expanded node + self.getMore() + + def __cleanupParentList(self, parent, parentIdx): + """ + Private method to remove items which are left over at the end of the + child list. + + @param parent to clean up + @type VariableItem + @param parentIdx the parent index as QModelIndex + @type QModelIndex """ - Public method to expand the item. + end = len(parent.childs) + if end > parent.absolutCount: + self.beginRemoveRows(parentIdx, parent.absolutCount, end) + del parent.childs[parent.absolutCount:] + self.endRemoveRows() + + def resetModifiedMarker(self, parentIdx=QModelIndex(), pathlist=()): + """ + Public method to remove the modified marker from changed items. + + @param parentIdx item to reset marker + @type QModelIndex + @param pathlist full path to the variable + @type list of str + """ + if parentIdx.isValid(): + parent = parentIdx.internalPointer() + else: + parent = self.rootNode + + if parent.newItems: + parent.newItems.clear() + + pll = len(pathlist) + posPaths = {x for x in self.openItems if len(x) > pll} + posPaths |= {x for x in self.closedItems if len(x) > pll} + posPaths = {x[pll] for x in posPaths if x[:pll] == pathlist} + + if posPaths: + for child in parent.childs: + if child.hasChilds and child.nameWithId in posPaths: + if child.currentCount >= 0: + # Discard loaded elements and refresh if still expanded + child.currentCount = -1 + row = parent.childs.index(child) + newParentIdx = self.index(row, 0, parentIdx) + self.resetModifiedMarker( + newParentIdx, pathlist + (child.nameWithId,)) + + self.closedItems = [] + + # Little quirk: Refresh all visible items to clear the changed marker + if parentIdx == QModelIndex(): + idxStart = self.index(0, 0, QModelIndex()) + idxEnd = self.index(0, 2, QModelIndex()) + self.dataChanged.emit(idxStart, idxEnd) + + def columnCount(self, parent=QModelIndex()): + """ + Public Qt slot to get the column count. + + @param parent the model parent + @type QModelIndex + @return number of columns + @rtype int + """ + return 3 + + def rowCount(self, parent=QModelIndex()): + """ + Public Qt slot to get the row count. + + @param parent the model parent + @type QModelIndex + @return number of rows + @rtype int + """ + if parent.isValid(): + node = parent.internalPointer() + else: + node = self.rootNode + + return len(node.childs) + + def flags(self, index): + """ + Public Qt slot to get the item flags. + + @param index of item + @type QModelIndex + @return item flags + @rtype QtCore.Qt.ItemFlag """ - self.deleteChildren() - self.populated = True + if not index.isValid(): + return Qt.NoItemFlags + + return Qt.ItemIsEnabled | Qt.ItemIsSelectable + + def hasChildren(self, parent=QModelIndex()): + """ + Public Qt slot to get a flag if parent has childs. + + @param parent the model parent + @type QModelIndex + @return flag if parent has childs + @rtype bool + """ + if not parent.isValid(): + return self.rootNode.childs != [] + + return parent.internalPointer().hasChilds + + def index(self, row, column, parent=QModelIndex()): + """ + Public Qt slot to get the index of item at row:column of parent. + + @param row number of rows + @rtype int + @param column number of columns + @type int + @param parent the model parent + @type QModelIndex + @return new model index for child + @rtype QModelIndex + """ + if not self.hasIndex(row, column, parent): + return QModelIndex() + + if not parent.isValid(): + node = self.rootNode + else: + node = parent.internalPointer() + + return self.createIndex(row, column, node.childs[row]) + + def parent(self, child): + """ + Public Qt slot to get the parent of the given child. - pathlist = [self._buildKey()] - par = self.parent() + @param child the model child node + @type QModelIndex + @return new model index for parent + @rtype QModelIndex + """ + if not child.isValid(): + return QModelIndex() + + childNode = child.internalPointer() + if childNode == self.rootNode: + return QModelIndex() + + parentNode = childNode.parent + + if parentNode == self.rootNode: + return QModelIndex() + + row = parentNode.parent.childs.index(parentNode) + return self.createIndex(row, 0, parentNode) + + def data(self, index, role=Qt.DisplayRole): + """ + Public Qt slot get the role data of item. + + @param index the model index + @type QModelIndex + @param role the requested data role + @type QtCore.Qt.ItemDataRole + @return role data of item + @rtype Any + """ + if not index.isValid() or index.row() < 0: + return None + + node = index.internalPointer() + column = index.column() + + if role in (Qt.DisplayRole, SORT_ROLE, Qt.EditRole): + try: + if column == 0: + # Sort first column with values from third column + if role == SORT_ROLE: + return node.sort + return node.name + node.indicator + elif column == 1: + return node.valueShort + elif column == 2: + return node.type + elif column == 3: + return node.sort + else: + return None + except AttributeError: + return ['None', '', '', ''][column] + + elif role == Qt.BackgroundRole: + if node in node.parent.newItems: + color = QColor('#70FF66') + # Set Alpha chanel to get alternating row colors done by Qt + color.setAlpha(40) + return QBrush(color) - # step 1: get a pathlist up to the requested variable - while par is not None: - pathlist.insert(0, par._buildKey()) - par = par.parent() + elif role == Qt.ToolTipRole: + if column == 0: + tooltip = node.name + node.indicator + elif column == 1: + tooltip = node.tooltip + elif column == 2: + tooltip = node.type + elif column == 3: + tooltip = node.sort + else: + return None + + if Qt.mightBeRichText(tooltip): + tooltip = Utilities.html_encode(tooltip) + + if column == 0: + indentation = self.treeView.indentation() + indentCount = 0 + currentNode = node + while currentNode.parent: + indentCount += 1 + currentNode = currentNode.parent + + indentation *= indentCount + else: + indentation = 0 + # Check if text is longer than available space + fontMetrics = QFontMetrics(self.treeView.font()) + textSize = fontMetrics.width(tooltip) + textSize += indentation + 5 # How to determine border size? + header = self.treeView.header() + if textSize >= header.sectionSize(column): + return tooltip + else: + QToolTip.hideText() + + return None + + def headerData(self, section, orientation, role=Qt.DisplayRole): + """ + Public Qt slot get the header names. + + @param section the header section (row/coulumn) + @type int + @param orientation the header's orientation + @type QtCore.Qt.Orientation + @param role the requested data role + @type QtCore.Qt.ItemDataRole + @return header name + @rtype str or None + """ + if role != Qt.DisplayRole or orientation != Qt.Horizontal: + return None + + if section == 0: + return self.rootNode.name + elif section == 1: + return self.rootNode.value + elif section == 2: + return self.rootNode.type + elif section == 3: + return self.rootNode.sort + + return None + + def __findPendingItem(self, parent=None, pathlist=()): + """ + Private method to find the next item to request data from debugger. + + @param parent the model parent + @type VariableItem + @param pathlist full path to the variable + @type list of str + @return next item index to request data from debugger + @rtype QModelIndex + """ + if parent is None: + parent = self.rootNode - # step 2: request the variable from the debugger + for child in parent.childs: + if not child.hasChilds: + continue + + if pathlist + (child.nameWithId,) in self.openItems: + if child.populated: + index = None + else: + idx = parent.childs.index(child) + index = self.createIndex(idx, 0, child) + self.expand.emit(index) + + if child.currentCount < 0: + return index + + possibleIndex = self.__findPendingItem( + child, pathlist + (child.nameWithId,)) + + if (possibleIndex or index) is None: + continue + + return possibleIndex or index + + return None + + def getMore(self): + """ + Public method to fetch the next variable from debugger. + """ + # step 1: find expanded but not populated items + item = self.__findPendingItem() + if not item or not item.isValid(): + return + + # step 2: check if data has to be retrieved + node = item.internalPointer() + lastVisibleItem = self.index(node.currentCount - 1, 0, item) + lastVisibleItem = self.proxyModel.mapFromSource(lastVisibleItem) + rect = self.treeView.visualRect(lastVisibleItem) + if rect.y() > self.treeView.height() or node.pendigFetch: + return + + node.pendigFetch = True + # step 3: get a pathlist up to the requested variable + pathlist = self.__buildTreePath(node) + # step 4: request the variable from the debugger variablesFilter = e5App().getObject("DebugUI").variablesFilter( - self.globalScope) + self.__globalScope) e5App().getObject("DebugServer").remoteClientVariable( - self.globalScope, variablesFilter, pathlist, self.framenr) + self.__globalScope, variablesFilter, pathlist, self.framenr) + + def setExpanded(self, index, state): + """ + Public method to set the expanded state of item. + + @param index item to change expanded state + @type QModelIndex + @param state state of the item + @type bool + """ + node = index.internalPointer() + pathlist = self.__buildTreePath(node) + if state: + if pathlist not in self.openItems: + self.openItems.append(pathlist) + if pathlist in self.closedItems: + self.closedItems.remove(pathlist) + self.getMore() + else: + self.openItems.remove(pathlist) + self.closedItems.append(pathlist) + + def __buildTreePath(self, parent): + """ + Private method to build up a path from the root to parent. + + @param parent item to build the path for + @type VariableItem + @return list of names denoting the path from the root + @rtype tuple of str + """ + pathlist = [] + + # build up a path from the top to the item + while parent.parent: + pathlist.append(parent.nameWithId) + parent = parent.parent + + pathlist.reverse() + return tuple(pathlist) -class ArrayElementVarItem(VariableItem): +class ProxyModel(QSortFilterProxyModel): """ - Class implementing a VariableItem that represents an array element. + Class for handling the sort operations. """ - def __init__(self, parent, dvar, dvalue, dtype): + def __init__(self, parent=None): """ Constructor - @param parent parent of this item - @param dvar variable name (string) - @param dvalue value string (string) - @param dtype type string (string) + @param parent the parent model index + @type QModelIndex """ - VariableItem.__init__(self, parent, dvar, dvalue, dtype) - + super(ProxyModel, self).__init__(parent) + self.setSortRole(SORT_ROLE) + + def hasChildren(self, parent): """ - Array elements have numbers as names, but the key must be - right justified and zero filled to 6 decimal places. Then - element 2 will have a key of '000002' and appear before - element 10 with a key of '000010' + Public Qt slot to get a flag if parent has childs. + + The given model index has to be transformed to the underlying source + model to get the correct result. + @param parent the model parent + @type QModelIndex + @return flag if parent has childs + @rtype bool """ - col0Str = self.text(0) - self.setText(0, "{0:6d}".format(int(col0Str))) + return self.sourceModel().hasChildren(self.mapToSource(parent)) + + def setExpanded(self, index, state): + """ + Public Qt slot to get a flag if parent has childs. + + The given model index has to be transformed to the underlying source + model to get the correct result. + @param index item to change expanded state + @type QModelIndex + @param state state of the item + @type bool + """ + self.sourceModel().setExpanded(self.mapToSource(index), state) -class SpecialArrayElementVarItem(SpecialVarItem): - """ - Class implementing a QTreeWidgetItem that represents a special array - variable node. +class VariablesViewer(QTreeView): """ - def __init__(self, parent, dvar, dvalue, dtype, frmnr, globalScope): - """ - Constructor - - @param parent parent of this item - @param dvar variable name (string) - @param dvalue value string (string) - @param dtype type string (string) - @param frmnr frame number (0 is the current frame) (int) - @param globalScope flag indicating global (True) or local (False) - variables - """ - SpecialVarItem.__init__(self, parent, dvar, dvalue, dtype, frmnr, - globalScope) - - """ - Array elements have numbers as names, but the key must be - right justified and zero filled to 6 decimal places. Then - element 2 will have a key of '000002' and appear before - element 10 with a key of '000010' - """ - # strip off [], () or {} - col0Str, indicators = VariableItem.extractIndicators(self.text(0)) - self.setText(0, "{0:6d}{1}".format(int(col0Str), indicators)) - - -class VariablesViewer(QTreeWidget): - """ - Class implementing the variables viewer widget. + Class implementing the variables viewer view. - This widget is used to display the variables of the program being + This view is used to display the variables of the program being debugged in a tree. Compound types will be shown with their main entry first. Once the subtree has been expanded, the individual entries will be shown. Double clicking an entry will + expand or collapse the item, if it has childs and the double click + was performed on the first column of the tree, otherwise it'll popup a dialog showing the variables parameters in a more readable form. This is especially useful for lengthy strings. - This widget has two modes for displaying the global and the local + This view has two modes for displaying the global and the local variables. """ def __init__(self, viewer, globalScope, parent=None): """ Constructor - @param viewer reference to the debug viewer object (DebugViewer) + @param viewer reference to the debug viewer object + @type DebugViewer @param globalScope flag indicating global (True) or local (False) variables - @param parent the parent (QWidget) + @type bool + @param parent the parent + @type QWidget """ super(VariablesViewer, self).__init__(parent) self.__debugViewer = viewer self.__globalScope = globalScope - - indicatorPattern = "|".join([QRegExp.escape(indicator) - for indicator in VariableItem.Indicators]) - self.rx_class = QRegExp('<.*(instance|object) at 0x.*>') - self.rx_class2 = QRegExp('class .*') - self.rx_class3 = QRegExp('<class .* at 0x.*>') - self.dvar_rx_class1 = QRegExp( - r'<.*(instance|object) at 0x.*>({0})'.format(indicatorPattern)) - self.dvar_rx_class2 = QRegExp( - r'<class .* at 0x.*>({0})'.format(indicatorPattern)) - self.dvar_rx_array_element = QRegExp(r'^\d+$') - self.dvar_rx_special_array_element = QRegExp( - r'^\d+({0})$'.format(indicatorPattern)) - self.rx_nonprintable = QRegExp(r"""(\\x\d\d)+""") - self.framenr = 0 - self.loc = Preferences.getSystem("StringEncoding") + # Massive performance gain + self.setUniformRowHeights(True) + + # Implements sorting and filtering + self.proxyModel = ProxyModel() + # Variable model implements the underlying data model + self.varModel = VariableModel(self, globalScope) + self.proxyModel.setSourceModel(self.varModel) + self.setModel(self.proxyModel) - self.openItems = [] + self.expanded.connect( + lambda idx: self.proxyModel.setExpanded(idx, True)) + self.collapsed.connect( + lambda idx: self.proxyModel.setExpanded(idx, False)) - self.setRootIsDecorated(True) + self.setExpandsOnDoubleClick(False) + self.doubleClicked.connect(self.__itemDoubleClicked) + + self.varModel.expand.connect(self.__mdlRequestExpand) + + self.setSortingEnabled(True) self.setAlternatingRowColors(True) self.setSelectionBehavior(QAbstractItemView.SelectRows) if self.__globalScope: self.setWindowTitle(self.tr("Global Variables")) - self.setHeaderLabels([ - self.tr("Globals"), - self.tr("Value"), - self.tr("Type")]) self.setWhatsThis(self.tr( """<b>The Global Variables Viewer Window</b>""" """<p>This window displays the global variables""" @@ -369,10 +902,6 @@ )) else: self.setWindowTitle(self.tr("Local Variables")) - self.setHeaderLabels([ - self.tr("Locals"), - self.tr("Value"), - self.tr("Type")]) self.setWhatsThis(self.tr( """<b>The Local Variables Viewer Window</b>""" """<p>This window displays the local variables""" @@ -382,85 +911,24 @@ header = self.header() header.setSortIndicator(0, Qt.AscendingOrder) header.setSortIndicatorShown(True) - if qVersionTuple() >= (5, 0, 0): + + try: header.setSectionsClickable(True) - else: + except Exception: header.setClickable(True) - header.sectionClicked.connect(self.__sectionClicked) - header.resizeSection(0, 120) # variable column - header.resizeSection(1, 150) # value column + + header.resizeSection(0, 130) # variable column + header.resizeSection(1, 180) # value column + header.resizeSection(2, 50) # type column + + header.sortIndicatorChanged.connect(lambda *x: self.varModel.getMore()) self.__createPopupMenus() self.setContextMenuPolicy(Qt.CustomContextMenu) self.customContextMenuRequested.connect(self.__showContextMenu) - self.itemExpanded.connect(self.__expandItemSignal) - self.itemCollapsed.connect(self.collapseItem) - self.resortEnabled = True - - def __createPopupMenus(self): - """ - Private method to generate the popup menus. - """ - self.menu = QMenu() - self.menu.addAction(self.tr("Show Details..."), self.__showDetails) - self.menu.addAction(self.tr("Refresh"), self.__refreshView) - self.menu.addSeparator() - self.menu.addAction(self.tr("Configure..."), self.__configure) - - self.backMenu = QMenu() - self.backMenu.addAction(self.tr("Refresh"), self.__refreshView) - self.backMenu.addSeparator() - self.backMenu.addAction(self.tr("Configure..."), self.__configure) - - def __showContextMenu(self, coord): - """ - Private slot to show the context menu. - - @param coord the position of the mouse pointer (QPoint) - """ - gcoord = self.mapToGlobal(coord) - if self.itemAt(coord) is not None: - self.menu.popup(gcoord) - else: - self.backMenu.popup(gcoord) - - def __findItem(self, slist, column, node=None): - """ - Private method to search for an item. - - It is used to find a specific item in column, - that is a child of node. If node is None, a child of the - QTreeWidget is searched. - - @param slist searchlist (list of strings) - @param column index of column to search in (int) - @param node start point of the search - @return the found item or None - """ - if node is None: - count = self.topLevelItemCount() - else: - count = node.childCount() - - if column == 0: - searchStr = VariableItem.extractId(slist[0])[0] - else: - searchStr = slist[0] - - for index in range(count): - if node is None: - itm = self.topLevelItem(index) - else: - itm = node.child(index) - if itm.text(column) == searchStr: - if len(slist) > 1: - itm = self.__findItem(slist[1:], column, itm) - return itm - - return None - + def showVariables(self, vlist, frmnr): """ Public method to show variables in a list. @@ -472,41 +940,13 @@ <li>the variables type (string)</li> <li>the variables value (string)</li> </ul> - @param frmnr frame number (0 is the current frame) (int) + @type list + @param frmnr frame number (0 is the current frame) + @type int """ - self.current = self.currentItem() - if self.current: - self.curpathlist = self.__buildTreePath(self.current) - self.clear() - self.__scrollToItem = None - self.framenr = frmnr - - if len(vlist): - self.resortEnabled = False - for (var, vtype, value) in vlist: - self.__addItem(None, vtype, var, value) - - # re-expand tree - openItems = sorted(self.openItems[:]) - self.openItems = [] - for itemPath in openItems: - itm = self.__findItem(itemPath, 0) - if itm is not None: - self.expandItem(itm) - else: - self.openItems.append(itemPath) - - if self.current: - citm = self.__findItem(self.curpathlist, 0) - if citm: - self.setCurrentItem(citm) - citm.setSelected(True) - self.scrollToItem(citm, QAbstractItemView.PositionAtTop) - self.current = None - - self.resortEnabled = True - self.__resort() - + self.varModel.resetModifiedMarker() + self.varModel.showVariables(vlist, frmnr) + def showVariable(self, vlist): """ Public method to show variables in a list. @@ -520,152 +960,118 @@ <li>the variables type (string)</li> <li>the variables value (string)</li> </ul> + @type list """ - resortEnabled = self.resortEnabled - self.resortEnabled = False - if self.current is None: - self.current = self.currentItem() - if self.current: - self.curpathlist = self.__buildTreePath(self.current) + self.varModel.showVariables(vlist[1:], 0, vlist[0]) + + def handleResetUI(self): + """ + Public method to reset the VariablesViewer. + """ + self.varModel.clear(True) + + def verticalScrollbarValueChanged(self, value): + """ + Public Qt slot informing about the scrollbar change. + + @param value current value of the vertical scrollbar + @type int + """ + self.varModel.getMore() + super(VariablesViewer, self).verticalScrollbarValueChanged(value) + + def resizeEvent(self, event): + """ + Protected Qt slot informing about the widget size change. - if vlist: - itm = self.__findItem(vlist[0], 0) - for var, vtype, value in vlist[1:]: - self.__addItem(itm, vtype, var, value) - - # re-expand tree - openItems = sorted(self.openItems[:]) - self.openItems = [] - for itemPath in openItems: - itm = self.__findItem(itemPath, 0) - if itm is not None and not itm.isExpanded(): - if itm.populated: - self.blockSignals(True) - itm.setExpanded(True) - self.blockSignals(False) - else: - self.expandItem(itm) - self.openItems = openItems[:] - - if self.current: - citm = self.__findItem(self.curpathlist, 0) - if citm: - self.setCurrentItem(citm) - citm.setSelected(True) - if self.__scrollToItem: - self.scrollToItem(self.__scrollToItem, - QAbstractItemView.PositionAtTop) - else: - self.scrollToItem(citm, QAbstractItemView.PositionAtTop) - self.current = None - elif self.__scrollToItem: - self.scrollToItem(self.__scrollToItem, - QAbstractItemView.PositionAtTop) + @param event information + @type QResizeEvent + """ + self.varModel.getMore() + super(VariablesViewer, self).resizeEvent(event) + + def __itemDoubleClicked(self, index): + """ + Private method called if an item was double clicked. - self.resortEnabled = resortEnabled - self.__resort() - - def __generateItem(self, parent, dvar, dvalue, dtype, isSpecial=False): + @param index the double clicked item + @type QModelIndex """ - Private method used to generate a VariableItem. - - @param parent parent of the item to be generated - @param dvar variable name (string) - @param dvalue value string (string) - @param dtype type string (string) - @param isSpecial flag indicating that a special node should be - generated (boolean) - @return The item that was generated (VariableItem). + node = self.proxyModel.mapToSource(index).internalPointer() + if node.hasChilds and index.column() == 0: + state = self.isExpanded(index) + self.setExpanded(index, not state) + else: + self.__showVariableDetails(index) + + def __mdlRequestExpand(self, modelIndex): """ - if isSpecial and \ - (self.dvar_rx_class1.exactMatch(dvar) or - self.dvar_rx_class2.exactMatch(dvar)): - isSpecial = False + Private method to inform the view about items to be expand. - if self.rx_class2.exactMatch(dtype): - return SpecialVarItem( - parent, dvar, dvalue, dtype[7:-1], self.framenr, - self.__globalScope) - elif dtype != "void *" and \ - (self.rx_class.exactMatch(dvalue) or - self.rx_class3.exactMatch(dvalue) or - isSpecial): - if self.dvar_rx_special_array_element.exactMatch(dvar): - return SpecialArrayElementVarItem( - parent, dvar, dvalue, dtype, self.framenr, - self.__globalScope) - else: - return SpecialVarItem(parent, dvar, dvalue, dtype, - self.framenr, self.__globalScope) - elif dtype in ["numpy.ndarray", "django.MultiValueDict", - "array.array"]: - return SpecialVarItem( - parent, dvar, self.tr("{0} items").format(dvalue), dtype, - self.framenr, self.__globalScope) - else: - if self.dvar_rx_array_element.exactMatch(dvar): - return ArrayElementVarItem(parent, dvar, dvalue, dtype) - else: - return VariableItem(parent, dvar, dvalue, dtype) + @param modelIndex the model index + @type QModelIndex + """ + index = self.proxyModel.mapFromSource(modelIndex) + self.expand(index) + + def __createPopupMenus(self): + """ + Private method to generate the popup menus. + """ + self.menu = QMenu() + self.menu.addAction(self.tr("Show Details..."), self.__showDetails) + self.menu.addSeparator() + self.menu.addAction(self.tr("Expand childs"), self.__expandChilds) + self.menu.addAction(self.tr("Collapse childs"), self.__collapseChilds) + self.menu.addAction(self.tr("Collapse all"), self.collapseAll) + self.menu.addSeparator() + self.menu.addAction(self.tr("Refresh"), self.__refreshView) + self.menu.addSeparator() + self.menu.addAction(self.tr("Configure..."), self.__configure) - def __addItem(self, parent, vtype, var, value): - """ - Private method used to add an item to the list. - - If the item is of a type with subelements (i.e. list, dictionary, - tuple), these subelements are added by calling this method recursively. - - @param parent the parent of the item to be added - (QTreeWidgetItem or None) - @param vtype the type of the item to be added - (string) - @param var the variable name (string) - @param value the value string (string) - @return The item that was added to the listview (QTreeWidgetItem). + self.backMenu = QMenu() + self.backMenu.addAction(self.tr("Refresh"), self.__refreshView) + self.backMenu.addSeparator() + self.backMenu.addAction(self.tr("Configure..."), self.__configure) + + def __showContextMenu(self, coord): """ - if parent is None: - parent = self - try: - dvar = '{0}{1}'.format(var, VariableItem.Type2Indicators[vtype]) - except KeyError: - dvar = var - dvtype = self.__getDispType(vtype) - - if vtype in ['list', 'tuple', 'dict', 'set', 'frozenset']: - itm = self.__generateItem(parent, dvar, - self.tr("{0} items").format(value), - dvtype, True) - elif vtype in ['unicode', 'str']: - if self.rx_nonprintable.indexIn(value) != -1: - sval = value - else: - try: - sval = eval(value) - except Exception: - sval = value - itm = self.__generateItem(parent, dvar, str(sval), dvtype) + Private slot to show the context menu. + @param coord the position of the mouse pointer + @type QPoint + """ + gcoord = self.mapToGlobal(coord) + if self.indexAt(coord).isValid(): + self.menu.popup(gcoord) else: - itm = self.__generateItem(parent, dvar, value, dvtype) - - return itm - - def __getDispType(self, vtype): + self.backMenu.popup(gcoord) + + def __expandChilds(self): + """ + Private slot to expand all childs of current parent. """ - Private method used to get the display string for type vtype. - - @param vtype the type, the display string should be looked up for - (string) - @return displaystring (string) + index = self.currentIndex() + node = self.proxyModel.mapToSource(index).internalPointer() + for child in node.childs: + if child.hasChilds: + row = node.childs.index(child) + idx = self.varModel.createIndex(row, 0, child) + idx = self.proxyModel.mapFromSource(idx) + self.expand(idx) + + def __collapseChilds(self): """ - try: - dvtype = self.tr(ConfigVarTypeDispStrings[vtype]) - except KeyError: - if vtype == 'classobj': - dvtype = self.tr(ConfigVarTypeDispStrings['instance']) - else: - dvtype = vtype - return dvtype + Private slot to collapse all childs of current parent. + """ + index = self.currentIndex() + node = self.proxyModel.mapToSource(index).internalPointer() + for child in node.childs: + row = node.childs.index(child) + idx = self.varModel.createIndex(row, 0, child) + idx = self.proxyModel.mapFromSource(idx) + if self.isExpanded(idx): + self.collapse(idx) def __refreshView(self): """ @@ -676,63 +1082,42 @@ else: self.__debugViewer.setLocalsFilter() - def mouseDoubleClickEvent(self, mouseEvent): - """ - Protected method of QAbstractItemView. - - Reimplemented to disable expanding/collapsing of items when - double-clicking. Instead the double-clicked entry is opened. - - @param mouseEvent the mouse event object (QMouseEvent) - """ - itm = self.itemAt(mouseEvent.pos()) - self.__showVariableDetails(itm) - def __showDetails(self): """ Private slot to show details about the selected variable. """ - itm = self.currentItem() - self.__showVariableDetails(itm) - - def __showVariableDetails(self, itm): + idx = self.currentIndex() + self.__showVariableDetails(idx) + + def __showVariableDetails(self, index): """ Private method to show details about a variable. - @param itm reference to the variable item + @param index reference to the variable item + @type QModelIndex """ - if itm is None: - return - - val = itm.getValue() + node = self.proxyModel.mapToSource(index).internalPointer() - if not val: - return # do not display anything, if the variable has no value - - vtype = itm.text(2) - name = VariableItem.extractIndicators(itm.text(0).strip())[0] + val = node.value + vtype = node.type + name = node.name - par = itm.parent() - if name.startswith("["): # numpy.ndarray, array.array - nlist = [] - else: - nlist = [name] + par = node.parent + nlist = [name] # build up the fully qualified name - while par is not None: - pname, indicators = VariableItem.extractIndicators( - par.text(0).strip()) - if indicators: + while par.parent is not None: + pname = par.name + if par.indicator: if nlist[0].endswith("."): nlist[0] = '[{0}].'.format(nlist[0][:-1]) else: nlist[0] = '[{0}]'.format(nlist[0]) - if not pname.startswith("["): # numpy.ndarray, array.array - nlist.insert(0, pname) + nlist.insert(0, pname) else: - if par.text(2) == "django.MultiValueDict": + if par.type == "django.MultiValueDict": nlist[0] = 'getlist({0})'.format(nlist[0]) - elif par.text(2) == "numpy.ndarray": + elif par.type == "numpy.ndarray": if nlist and nlist[0][0].isalpha(): if nlist[0] in ["min", "max", "mean"]: nlist[0] = ".{0}()".format(nlist[0]) @@ -741,106 +1126,21 @@ nlist.insert(0, pname) else: nlist.insert(0, '{0}.'.format(pname)) - par = par.parent() - + par = par.parent + name = ''.join(nlist) # now show the dialog from .VariableDetailDialog import VariableDetailDialog dlg = VariableDetailDialog(name, vtype, val) dlg.exec_() - def __buildTreePath(self, itm): - """ - Private method to build up a path from the top to an item. - - @param itm item to build the path for (QTreeWidgetItem) - @return list of names denoting the path from the top (list of strings) - """ - name = itm.text(0) - pathlist = [name] - - par = itm.parent() - # build up a path from the top to the item - while par is not None: - pname = par.text(0) - pathlist.insert(0, pname) - par = par.parent() - - return pathlist[:] - - def __expandItemSignal(self, parentItem): - """ - Private slot to handle the expanded signal. - - @param parentItem reference to the item being expanded - (QTreeWidgetItem) - """ - self.expandItem(parentItem) - self.__scrollToItem = parentItem - - def expandItem(self, parentItem): - """ - Public slot to handle the expanded signal. - - @param parentItem reference to the item being expanded - (QTreeWidgetItem) - """ - pathlist = self.__buildTreePath(parentItem) - self.openItems.append(pathlist) - if parentItem.populated: - return - - try: - parentItem.expand() - except AttributeError: - super(VariablesViewer, self).expandItem(parentItem) - - def collapseItem(self, parentItem): - """ - Public slot to handle the collapsed signal. - - @param parentItem reference to the item being collapsed - (QTreeWidgetItem) - """ - pathlist = self.__buildTreePath(parentItem) - self.openItems.remove(pathlist) - - try: - parentItem.collapse() - except AttributeError: - super(VariablesViewer, self).collapseItem(parentItem) - - def __sectionClicked(self): - """ - Private method handling a click onto a header section. - """ - self.__resort() - - def __resort(self, parent=None): - """ - Private method to resort the tree. - - @param parent reference to a parent item - @type QTreeWidgetItem - """ - if self.resortEnabled: - if parent is not None: - parent.sortChildren(self.sortColumn(), - self.header().sortIndicatorOrder()) - else: - self.sortItems(self.sortColumn(), - self.header().sortIndicatorOrder()) - - def handleResetUI(self): - """ - Public method to reset the VariablesViewer. - """ - self.clear() - self.openItems = [] - def __configure(self): """ Private method to open the configuration dialog. """ e5App().getObject("UserInterface")\ .showPreferences("debuggerGeneralPage") + + +# +# eflag: noqa = M822