eric6/Debugger/VariablesViewer.py

Sat, 03 Jul 2021 11:47:24 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Sat, 03 Jul 2021 11:47:24 +0200
branch
maintenance
changeset 8450
be7369a19dc9
parent 8273
698ae46f40a4
parent 8434
a105f31ba10f
child 8576
fe1957c69854
permissions
-rw-r--r--

Merged with default branch to prepare release 21.7.

# -*- coding: utf-8 -*-

# Copyright (c) 2002 - 2021 Detlev Offenbach <detlev@die-offenbachs.de>
#

"""
Module implementing the variables viewer view based on QTreeView.
"""

import ast
import re
import contextlib

from PyQt5.QtCore import (
    Qt, QAbstractItemModel, QModelIndex, QCoreApplication,
    QSortFilterProxyModel, pyqtSignal
)
from PyQt5.QtGui import QBrush, 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

SORT_ROLE = Qt.ItemDataRole.UserRole


class VariableItem:
    """
    Class implementing the data structure for all variable items.
    """
    Type2Indicators = {
        # Python types
        'list': '[]',
        'tuple': '()',
        'dict': '{:}',                          # __IGNORE_WARNING_M613__
        'set': '{}',                            # __IGNORE_WARNING_M613__
        'frozenset': '{}',                      # __IGNORE_WARNING_M613__
        'numpy.ndarray': '[ndarray]',           # __IGNORE_WARNING_M613__
    }
    
    # Initialize regular expression for unprintable strings
    rx_nonprintable = re.compile(r"""(\\x\d\d)+""")
    
    noOfItemsStr = QCoreApplication.translate("VariablesViewer", "{0} items")
    unsized = QCoreApplication.translate("VariablesViewer", "unsized")
    
    arrayTypes = {
        'list', 'tuple', 'dict', 'set', 'frozenset', 'numpy.ndarray',
        'django.MultiValueDict', 'array.array', 'collections.defaultdict',
        "class 'dict_items'", "class 'dict_keys'", "class 'dict_values'",
    }
    
    nonExpandableTypes = (
        'method_descriptor', 'wrapper_descriptor', '', 'getset_descriptor',
        'method-wrapper', 'member_descriptor',
    )
    
    def __init__(self, parent, dvar, dtype, dvalue):
        """
        Constructor
        
        @param parent reference to the parent item
        @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 children
        # Indicator that there are children
        self.hasChildren = False
        self.populated = False
        # Indicator that item was at least once fully populated
        self.wasPopulated = False
        
        self.children = []
        # Flag to prevent endless reloading of current item while waiting on
        # a response from debugger
        self.pendigFetch = False
        
        # Set of child items, which are displayed the first time or changed
        self.newItems = set()
        self.changedItems = 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 AttributeError:
            idx = dvar
            dvar = str(dvar)
        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):
        """
        Private method to process the type of the variable.
        
        If type is known to have children, the corresponding flag is set.
        
        @param dtype type string
        @type str
        """
        # Python class?
        if dtype.startswith('class '):
            dtype = dtype[7:-1]
        # Qt related stuff?
        elif (
            (dtype.startswith(ConfigQtNames) and
             dtype.endswith(ConfigKnownQtTypes)) or
            dtype in ('instance', 'class')
        ):
            self.hasChildren = True
        
        # Special Qt types should not be expanded infinite
        elif ".{0}".format(dtype) in ConfigKnownQtTypes:
            self.type = dtype  # It's a Qt type, so skipping translation is ok
            return
        
        vtype = ConfigVarTypeDispStrings.get(dtype, dtype)
        # Unkown types should be expandable by default
        if vtype is dtype and dtype not in self.nonExpandableTypes:
            self.hasChildren = True
        self.type = QCoreApplication.translate("VariablesViewer", vtype)
    
    def __getValue(self, dtype, dvalue):
        """
        Private method to process the variables value.
        
        Define and limit value, set tooltip text. If type is known to have
        children, 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)
        elif dtype == 'array.array':
            dvalue, typecode = dvalue.split('|')
            self.indicator = '[<{0}>]'.format(typecode)
        else:
            self.indicator = VariableItem.Type2Indicators.get(dtype, '')
        
        if dtype == 'numpy.ndarray':
            if dvalue:
                self.childCount = int(dvalue.split('x')[0])
                dvalue = VariableItem.noOfItemsStr.format(dvalue)
            else:
                dvalue = VariableItem.unsized
            self.hasChildren = True
        elif dtype in VariableItem.arrayTypes:
            self.childCount = int(dvalue)
            dvalue = VariableItem.noOfItemsStr.format(dvalue)
            self.hasChildren = True
        
        elif dtype == "Shiboken.EnumType":
            self.hasChildren = True
        
        elif dtype == 'str':
            if VariableItem.rx_nonprintable.search(dvalue) is None:
                with contextlib.suppress(Exception):
                    dvalue = ast.literal_eval(dvalue)
            dvalue = str(dvalue)
        
        elif (
            dvalue.startswith(("{", "(", "[")) and
            dvalue.endswith(("}", ")", "]"))
        ):
            # it is most probably a dict, tuple or list derived class
            value = ast.literal_eval(dvalue)
            valueTypeStr = str(type(value))[8:-2]
            if valueTypeStr in VariableItem.arrayTypes:
                self.childCount = len(value)
                self.hasChildren = True
        
        elif (
            (dvalue.endswith("})") and "({" in dvalue) or
            (dvalue.endswith("])") and "([" in dvalue)
        ):
            # that is probably a set derived class
            value = ast.literal_eval(dvalue.split("(", 1)[1][:-1])
            valueTypeStr = str(type(value))[8:-2]
            if valueTypeStr in VariableItem.arrayTypes:
                self.childCount = len(value)
                self.hasChildren = True
        
        self.value = dvalue
        
        if len(dvalue) > 2048:     # 2 kB
            self.tooltip = dvalue[:2048]
            dvalue = QCoreApplication.translate(
                "VariableItem", "<double click to show value>")
        else:
            self.tooltip = dvalue
        
        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 += "<...>"
        
        self.valueShort = dvalue
    
    @property
    def absolutCount(self):
        """
        Public property to get the total number of children.
        
        @return total number of children
        @rtype int
        """
        return self.childCount + self.methodCount


class VariablesModel(QAbstractItemModel):
    """
    Class implementing the data model for QTreeView.
    
    @signal expand trigger QTreeView to expand given index
    """
    expand = pyqtSignal(QModelIndex)
    
    def __init__(self, treeView, globalScope):
        """
        Constructor
        
        @param treeView QTreeView showing the data
        @type VariablesViewer
        @param globalScope flag indicating global (True) or local (False)
            variables
        @type bool
        """
        super().__init__()
        self.treeView = treeView
        self.proxyModel = treeView.proxyModel
        
        self.framenr = -1
        self.openItems = []
        self.closedItems = []
        
        visibility = self.tr("Globals") if globalScope else 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.children = []
        self.rootNode.newItems.clear()
        self.rootNode.changedItems.clear()
        self.rootNode.wasPopulated = False
        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.children:
                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.children.index(parent)
            parentIdx = self.createIndex(row, 0, parent)
        
        if itemStartIndex == -3:
            # Item doesn't exist any more
            parentIdx = self.parent(parentIdx)
            self.beginRemoveRows(parentIdx, row, row)
            del parent.parent.children[row]
            self.endRemoveRows()
            parent.parent.childCount -= 1
            return
            
        elif itemStartIndex == -2:
            parent.wasPopulated = True
            parent.currentCount = parent.absolutCount
            parent.populated = True
            # Remove items which are left over at the end of child list
            self.__cleanupParentList(parent, parentIdx)
            return
            
        elif itemStartIndex == -1:
            parent.methodCount = len(vlist)
            idx = max(parent.currentCount, 0)
            parent.currentCount = idx + len(vlist)
            parent.populated = True
        else:
            idx = itemStartIndex
            parent.currentCount = idx + len(vlist)
        
        # 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
        knownChildrenCount = len(parent.children)
        while idx < endIndex:
            # Fetch next old item from last cycle
            try:
                child = parent.children[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.children.insert(idx, newItem)
                if knownChildrenCount <= idx and not parent.wasPopulated:
                    parent.newItems.add(newItem)
                    knownChildrenCount += 1
                else:
                    parent.changedItems.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.populated = False
                    child.childCount = newItem.childCount
                    
                    # Highlight item because it has changed
                    parent.changedItems.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.children.remove(child)
            self.endRemoveRows()
            # idx stay unchanged
            knownChildrenCount -= 1
        
        # Remove items which are left over at the end of child list
        if itemStartIndex == -1:
            parent.wasPopulated = True
            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
        """
        end = len(parent.children)
        if end > parent.absolutCount:
            self.beginRemoveRows(parentIdx, parent.absolutCount, end)
            del parent.children[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
        """
        parent = (parentIdx.internalPointer() if parentIdx.isValid()
                  else self.rootNode)
        
        parent.newItems.clear()
        parent.changedItems.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.children:
                if (
                    child.hasChildren and
                    child.nameWithId in posPaths and
                    child.currentCount >= 0
                ):
                    # Discard loaded elements and refresh if still expanded
                    child.currentCount = -1
                    child.populated = False
                    row = parent.children.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():
            self.rootNode.currentCount = -1
            self.rootNode.populated = False
            idxStart = self.index(0, 0, QModelIndex())
            idxEnd = self.index(0, 2, QModelIndex())
            self.dataChanged.emit(idxStart, idxEnd)
    
    def columnCount(self, parent=QModelIndex()):
        """
        Public method 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 method to get the row count.
        
        @param parent the model parent
        @type QModelIndex
        @return number of rows
        @rtype int
        """
        node = parent.internalPointer() if parent.isValid() else self.rootNode
        
        return len(node.children)
    
    def flags(self, index):
        """
        Public method to get the item flags.
        
        @param index of item
        @type QModelIndex
        @return item flags
        @rtype QtCore.Qt.ItemFlag
        """
        if not index.isValid():
            return Qt.ItemFlag.NoItemFlags

        return Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsSelectable
    
    def hasChildren(self, parent=QModelIndex()):
        """
        Public method to get a flag if parent has children.
        
        @param parent the model parent
        @type QModelIndex
        @return flag indicating parent has children
        @rtype bool
        """
        if not parent.isValid():
            return self.rootNode.children != []
        
        return parent.internalPointer().hasChildren
    
    def index(self, row, column, parent=QModelIndex()):
        """
        Public method to get the index of item at row:column of parent.
        
        @param row number of rows
        @type 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()
        
        node = parent.internalPointer() if parent.isValid() else self.rootNode
        
        return self.createIndex(row, column, node.children[row])
    
    def parent(self, child):
        """
        Public method to get the parent of the given child.
        
        @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.children.index(parentNode)
        return self.createIndex(row, 0, parentNode)
    
    def data(self, index, role=Qt.ItemDataRole.DisplayRole):
        """
        Public method 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.ItemDataRole.DisplayRole, SORT_ROLE, Qt.ItemDataRole.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
                else:
                    return {
                        1: node.valueShort,
                        2: node.type,
                        3: node.sort
                    }.get(column)
            except AttributeError:
                return ['None', '', '', ''][column]
        
        elif role == Qt.ItemDataRole.BackgroundRole:
            if node in node.parent.changedItems:
                return self.__bgColorChanged
            elif node in node.parent.newItems:
                return self.__bgColorNew
        
        elif role == Qt.ItemDataRole.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())
            try:
                textSize = fontMetrics.horizontalAdvance(tooltip)
            except AttributeError:
                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.ItemDataRole.DisplayRole):
        """
        Public method 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.ItemDataRole.DisplayRole or
            orientation != Qt.Orientation.Horizontal
        ):
            return None
        
        return {
            0: self.rootNode.name,
            1: self.rootNode.value,
            2: self.rootNode.type,
            3: self.rootNode.sort
        }.get(section)
    
    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
        
        for child in parent.children:
            if not child.hasChildren:
                continue
            
            if pathlist + (child.nameWithId,) in self.openItems:
                if child.populated:
                    index = None
                else:
                    idx = parent.children.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)
        e5App().getObject("DebugServer").remoteClientVariable(
            e5App().getObject("DebugUI").getSelectedDebuggerId(),
            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:
            if pathlist in self.openItems:
                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)
    
    def handlePreferencesChanged(self):
        """
        Public slot to handle the preferencesChanged signal.
        """
        self.__bgColorNew = QBrush(Preferences.getDebugger("BgColorNew"))
        self.__bgColorChanged = QBrush(
            Preferences.getDebugger("BgColorChanged"))
        
        idxStart = self.index(0, 0, QModelIndex())
        idxEnd = self.index(0, 2, QModelIndex())
        self.dataChanged.emit(idxStart, idxEnd)


class VariablesProxyModel(QSortFilterProxyModel):
    """
    Class for handling the sort operations.
    """
    def __init__(self, parent=None):
        """
        Constructor
        
        @param parent the parent model index
        @type QModelIndex
        """
        super().__init__(parent)
        self.setSortRole(SORT_ROLE)
    
    def hasChildren(self, parent):
        """
        Public method to get a flag if parent has children.
        
        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 children
        @rtype bool
        """
        return self.sourceModel().hasChildren(self.mapToSource(parent))
    
    def setExpanded(self, index, state):
        """
        Public slot to get a flag if parent has children.
        
        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 VariablesViewer(QTreeView):
    """
    Class implementing the variables viewer view.
    
    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 children 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 view has two modes for displaying the global and the local
    variables.
    
    @signal preferencesChanged() to inform model about new background colours
    """
    preferencesChanged = pyqtSignal()
    
    def __init__(self, viewer, globalScope, parent=None):
        """
        Constructor
        
        @param viewer reference to the debug viewer object
        @type DebugViewer
        @param globalScope flag indicating global (True) or local (False)
            variables
        @type bool
        @param parent the parent
        @type QWidget
        """
        super().__init__(parent)
        
        self.__debugViewer = viewer
        self.__globalScope = globalScope
        self.framenr = 0
        
        # Massive performance gain
        self.setUniformRowHeights(True)
        
        # Implements sorting and filtering
        self.proxyModel = VariablesProxyModel()
        # Variable model implements the underlying data model
        self.varModel = VariablesModel(self, globalScope)
        self.proxyModel.setSourceModel(self.varModel)
        self.setModel(self.proxyModel)
        self.preferencesChanged.connect(self.varModel.handlePreferencesChanged)
        self.preferencesChanged.emit()  # Force initialization of colors
        
        self.expanded.connect(
            lambda idx: self.proxyModel.setExpanded(idx, True))
        self.collapsed.connect(
            lambda idx: self.proxyModel.setExpanded(idx, False))
        
        self.setExpandsOnDoubleClick(False)
        self.doubleClicked.connect(self.__itemDoubleClicked)
        
        self.varModel.expand.connect(self.__mdlRequestExpand)
        
        self.setSortingEnabled(True)
        self.setAlternatingRowColors(True)
        self.setSelectionBehavior(
            QAbstractItemView.SelectionBehavior.SelectRows)
        
        if self.__globalScope:
            self.setWindowTitle(self.tr("Global Variables"))
            self.setWhatsThis(self.tr(
                """<b>The Global Variables Viewer Window</b>"""
                """<p>This window displays the global variables"""
                """ of the debugged program.</p>"""
            ))
        else:
            self.setWindowTitle(self.tr("Local Variables"))
            self.setWhatsThis(self.tr(
                """<b>The Local Variables Viewer Window</b>"""
                """<p>This window displays the local variables"""
                """ of the debugged program.</p>"""
            ))
        
        header = self.header()
        header.setSortIndicator(0, Qt.SortOrder.AscendingOrder)
        header.setSortIndicatorShown(True)
        
        try:
            header.setSectionsClickable(True)
        except Exception:
            header.setClickable(True)
        
        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.ContextMenuPolicy.CustomContextMenu)
        self.customContextMenuRequested.connect(self.__showContextMenu)
        
        self.resortEnabled = True
    
    def showVariables(self, vlist, frmnr):
        """
        Public method to show variables in a list.
        
        @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
        @param frmnr frame number (0 is the current frame)
        @type int
        """
        self.varModel.resetModifiedMarker()
        self.varModel.showVariables(vlist, frmnr)
    
    def showVariable(self, vlist):
        """
        Public method to show variables in a list.
        
        @param vlist the list of subitems to be displayed.
                The first element gives the path of the
                parent variable. Each other 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
        """
        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 slot informing about the scrollbar change.
        
        @param value current value of the vertical scrollbar
        @type int
        """
        self.varModel.getMore()
        super().verticalScrollbarValueChanged(value)
    
    def resizeEvent(self, event):
        """
        Protected slot informing about the widget size change.
        
        @param event information
        @type QResizeEvent
        """
        self.varModel.getMore()
        super().resizeEvent(event)
    
    def __itemDoubleClicked(self, index):
        """
        Private method called if an item was double clicked.
        
        @param index the double clicked item
        @type QModelIndex
        """
        node = self.proxyModel.mapToSource(index).internalPointer()
        if node.hasChildren and index.column() == 0:
            state = self.isExpanded(index)
            self.setExpanded(index, not state)
        else:
            self.__showVariableDetails(index)
    
    def __mdlRequestExpand(self, modelIndex):
        """
        Private method to inform the view about items to be expand.
        
        @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"), self.__expandChildren)
        self.menu.addAction(self.tr("Collapse"), self.__collapseChildren)
        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)
        self.menu.addAction(self.tr("Variables Type Filter..."),
                            self.__configureFilter)
        
        self.backMenu = QMenu()
        self.backMenu.addAction(self.tr("Refresh"), self.__refreshView)
        self.backMenu.addSeparator()
        self.backMenu.addAction(self.tr("Configure..."), self.__configure)
        self.backMenu.addAction(self.tr("Variables Type Filter..."),
                                self.__configureFilter)
    
    def __showContextMenu(self, coord):
        """
        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:
            self.backMenu.popup(gcoord)
    
    def __expandChildren(self):
        """
        Private slot to expand all child items of current parent.
        """
        index = self.currentIndex()
        node = self.proxyModel.mapToSource(index).internalPointer()
        for child in node.children:
            if child.hasChildren:
                row = node.children.index(child)
                idx = self.varModel.createIndex(row, 0, child)
                idx = self.proxyModel.mapFromSource(idx)
                self.expand(idx)
    
    def __collapseChildren(self):
        """
        Private slot to collapse all child items of current parent.
        """
        index = self.currentIndex()
        node = self.proxyModel.mapToSource(index).internalPointer()
        for child in node.children:
            row = node.children.index(child)
            idx = self.varModel.createIndex(row, 0, child)
            idx = self.proxyModel.mapFromSource(idx)
            if self.isExpanded(idx):
                self.collapse(idx)
    
    def __refreshView(self):
        """
        Private slot to refresh the view.
        """
        if self.__globalScope:
            self.__debugViewer.setGlobalsFilter()
        else:
            self.__debugViewer.setLocalsFilter()
    
    def __showDetails(self):
        """
        Private slot to show details about the selected variable.
        """
        idx = self.currentIndex()
        self.__showVariableDetails(idx)
    
    def __showVariableDetails(self, index):
        """
        Private method to show details about a variable.
        
        @param index reference to the variable item
        @type QModelIndex
        """
        node = self.proxyModel.mapToSource(index).internalPointer()
        
        val = node.value
        vtype = node.type
        name = node.name
        
        par = node.parent
        nlist = [name]
        
        # build up the fully qualified name
        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])
                nlist.insert(0, pname)
            else:
                if par.type == "django.MultiValueDict":
                    nlist[0] = 'getlist({0})'.format(nlist[0])
                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])
                        else:
                            nlist[0] = ".{0}".format(nlist[0])
                    nlist.insert(0, pname)
                else:
                    nlist.insert(0, '{0}.'.format(pname))
            par = par.parent
        
        name = ''.join(nlist)
        # now show the dialog
        from .VariableDetailDialog import VariableDetailDialog
        dlg = VariableDetailDialog(name, vtype, val)
        dlg.exec()
    
    def __configure(self):
        """
        Private method to open the configuration dialog.
        """
        e5App().getObject("UserInterface").showPreferences(
            "debuggerGeneralPage")
    
    def __configureFilter(self):
        """
        Private method to open the variables filter dialog.
        """
        e5App().getObject("DebugUI").dbgFilterAct.triggered.emit()

#
# eflag: noqa = M822

eric ide

mercurial