eric6/Debugger/VariablesViewer.py

Sat, 27 Apr 2019 22:16:35 +0200

author
T.Rzepka <Tobias.Rzepka@gmail.com>
date
Sat, 27 Apr 2019 22:16:35 +0200
branch
Variables Viewer
changeset 6979
c272ec33ea11
parent 6978
720247f98e1f
child 6988
87ad87ef1910
permissions
-rw-r--r--

Handling of no more existing items improved, e.g. list items which has as last child
an open list.

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

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

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

from __future__ import unicode_literals

try:
    str = unicode
except NameError:
    pass

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 Utilities

SORT_ROLE = Qt.UserRole


class VariableItem(object):
    """
    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 = 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
        @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):
        """
        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]
            if dtype not in ('method_descriptor', 'wrapper_descriptor'):
                self.hasChilds = True
        # Qt related stuff?
        elif (dtype.startswith(ConfigQtNames) and
                dtype.endswith(ConfigKnownQtTypes)):
            self.hasChilds = True
            
        elif dtype in ('instance', 'class'):
            self.hasChilds = True
        
        vtype = ConfigVarTypeDispStrings.get(dtype, dtype)
        # Unkown types should be expandable by default
        if (vtype is dtype and
                dtype not in ('method_descriptor', 'wrapper_descriptor')):
            self.hasChilds = 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 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')  # __IGNORE_WARNING__
        
        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 childs.
        
        @return total number of childs
        @rtype int
        """
        return self.childCount + self.methodCount
    
    @property
    def populated(self):
        """
        Public property returning a flag indicating if item is fully populated.
        
        @return item is fully populated
        @rtype bool
        """
        return self.currentCount >= (self.childCount + self.methodCount)


class VariableModel(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(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 == -3:
            # Item doesn't exist any more
            parentIdx = self.parent(parentIdx)
            self.beginRemoveRows(parentIdx, row, row)
            del parent.parent.childs[row]
            self.endRemoveRows()
            parent.parent.childCount -= 1
            return
            
        elif 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)
            idx = parent.childCount = parent.currentCount + 1
            parent.currentCount += 1 + len(vlist)
        else:
            idx = parent.currentCount + 1
            parent.currentCount += 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
        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
        """
        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():
            self.rootNode.currentCount = -1
            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
        """
        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.
        
        @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)
        
        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
        
        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)
        e5App().getObject("DebugServer").remoteClientVariable(
            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 ProxyModel(QSortFilterProxyModel):
    """
    Class for handling the sort operations.
    """
    def __init__(self, parent=None):
        """
        Constructor
        
        @param parent the parent model index
        @type QModelIndex
        """
        super(ProxyModel, self).__init__(parent)
        self.setSortRole(SORT_ROLE)
    
    def hasChildren(self, parent):
        """
        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
        """
        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 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 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 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
        @type DebugViewer
        @param globalScope flag indicating global (True) or local (False)
            variables
        @type bool
        @param parent the parent
        @type QWidget
        """
        super(VariablesViewer, self).__init__(parent)
        
        self.__debugViewer = viewer
        self.__globalScope = globalScope
        self.framenr = 0
        
        # 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.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.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.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.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
                listentry 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 listentry 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 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.
        
        @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.
        
        @param index the double clicked item
        @type QModelIndex
        """
        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):
        """
        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 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)
        
        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
        @type QPoint
        """
        gcoord = self.mapToGlobal(coord)
        if self.indexAt(coord).isValid():
            self.menu.popup(gcoord)
        else:
            self.backMenu.popup(gcoord)
    
    def __expandChilds(self):
        """
        Private slot to expand all childs of current parent.
        """
        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):
        """
        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):
        """
        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")


#
# eflag: noqa = M822

eric ide

mercurial