Reimplemented Variables Viewer using QTreeView. Variables Viewer

Sun, 21 Apr 2019 21:20:24 +0200

author
T.Rzepka <Tobias.Rzepka@gmail.com>
date
Sun, 21 Apr 2019 21:20:24 +0200
branch
Variables Viewer
changeset 6969
fd7af2312383
parent 6968
c634f51e40ec
child 6971
77cf9a30562d

Reimplemented Variables Viewer using QTreeView.

docs/changelog file | annotate | diff | comparison | revisions
eric6/DebugClients/Python/DebugClientBase.py file | annotate | diff | comparison | revisions
eric6/DebugClients/Python/DebugConfig.py file | annotate | diff | comparison | revisions
eric6/DebugClients/Python/DebugVariables.py file | annotate | diff | comparison | revisions
eric6/Debugger/VariablesViewer.py file | annotate | diff | comparison | revisions
--- a/docs/changelog	Sun Apr 21 17:27:52 2019 +0200
+++ b/docs/changelog	Sun Apr 21 21:20:24 2019 +0200
@@ -1,5 +1,16 @@
 Change Log
 ----------
+Version 19.xx:
+- Variables Viewer
+  -- reimplemented Variables Viewer using QTreeView, with unlimited access to
+     big arrays, dicts, etc. because elements are lazy loaded
+  -- highlighting of last changed variable(s)
+  -- expand / collapse variables with childs on double click on first column,
+     in all other cases display detail window
+  -- show tooltips on all elements which doesn't fit into current column width
+  -- new options in the context menu, e.g. expand / collapse all child nodes
+  -- bug fixes, e.g. sort order of mixed list elements, unicode handling, ...
+
 Version 19.05:
 - bug fixes
 - General
--- a/eric6/DebugClients/Python/DebugClientBase.py	Sun Apr 21 17:27:52 2019 +0200
+++ b/eric6/DebugClients/Python/DebugClientBase.py	Sun Apr 21 21:20:24 2019 +0200
@@ -26,7 +26,7 @@
 import DebugVariables
 from DebugBase import setRecursionLimit, printerr   # __IGNORE_WARNING__
 from AsyncFile import AsyncFile, AsyncPendingWrite
-from DebugConfig import ConfigVarTypeStrings
+from DebugConfig import ConfigQtNames, ConfigVarTypeStrings
 from FlexCompleter import Completer
 from DebugUtilities import prepareJsonCommand
 from BreakpointWatch import Breakpoint, Watch
@@ -379,14 +379,12 @@
         
         if method == "RequestVariables":
             self.__dumpVariables(
-                params["frameNumber"], params["scope"], params["filters"],
-                params["maxSize"])
+                params["frameNumber"], params["scope"], params["filters"])
         
         elif method == "RequestVariable":
             self.__dumpVariable(
                 params["variable"], params["frameNumber"],
-                params["scope"], params["filters"],
-                params["maxSize"])
+                params["scope"], params["filters"])
         
         elif method == "RequestThreadList":
             self.dumpThreadList()
@@ -1429,7 +1427,7 @@
         # reset coding
         self.__coding = self.defaultCoding
 
-    def __dumpVariables(self, frmnr, scope, filterList, maxSize):
+    def __dumpVariables(self, frmnr, scope, filterList):
         """
         Private method to return the variables of a frame to the debug server.
         
@@ -1447,6 +1445,7 @@
         if self.currentThread is None:
             return
         
+        self.resolverCache = [{}, {}]
         frmnr += self.currentThread.skipFrames
         if scope == 0:
             self.framenr = frmnr
@@ -1468,22 +1467,18 @@
             scope = -1
         else:
             varDict = f.f_locals
-            
-        varlist = []
         
-        if scope != -1:
-            keylist = varDict.keys()
-            
-            vlist = self.__formatVariablesList(
-                keylist, varDict, scope, filterList, maxSize=maxSize)
-            varlist.extend(vlist)
+        if scope == -1:
+            varlist = []
+        else:
+            varlist = self.__formatVariablesList(varDict, scope, filterList)
         
         self.sendJsonCommand("ResponseVariables", {
             "scope": scope,
             "variables": varlist,
         })
     
-    def __dumpVariable(self, var, frmnr, scope, filterList, maxSize):
+    def __dumpVariable(self, var, frmnr, scope, filterList):
         """
         Private method to return the variables of a frame to the debug server.
         
@@ -1523,12 +1518,16 @@
         
         varlist = []
         
-        if scope != -1:
+        if scope != -1 and str(var) in self.resolverCache[scope]:
+            varGen = self.resolverCache[scope][str(var)]
+            idx, varDict = next(varGen)
+            var.insert(0, idx)
+            varlist = self.__formatVariablesList(varDict, scope, filterList)
+        elif scope != -1:
             variable = varDict
+            # Lookup the wanted attribute
             for attribute in var:
-                attribute = self.__extractIndicators(attribute)[0]
-                typeObject, typeName, typeStr, resolver = \
-                    DebugVariables.getType(variable)
+                _, _, resolver = DebugVariables.getType(variable)
                 if resolver:
                     variable = resolver.resolve(variable, attribute)
                     if variable is None:
@@ -1536,26 +1535,35 @@
                     
                 else:
                     break
-                
+            
+            idx = -2
+            # If found, get the details of attribute
             if variable is not None:
-                typeObject, typeName, typeStr, resolver = \
-                    DebugVariables.getType(variable)
-                if typeStr.startswith(("PyQt5.", "PyQt4.")):
-                    vlist = self.__formatQtVariable(variable, typeName)
-                    varlist.extend(vlist)
+                typeName, typeStr, resolver = DebugVariables.getType(variable)
+                if typeStr.startswith(ConfigQtNames):
+                    # Gently handle exception which could occure as special
+                    # cases, e.g. already deleted C++ objects, str conversion..
+                    try:
+                        varlist = self.__formatQtVariable(variable, typeName)
+                    except Exception:
+                        varlist = []
+                    idx = -1
                 elif resolver:
-                    varDict = resolver.getDictionary(variable)
-                    vlist = self.__formatVariablesList(
-                        list(varDict.keys()), varDict, scope, filterList,
-                        maxSize=maxSize)
-                    varlist.extend(vlist)
+                    varGen = resolver.getDictionary(variable)
+                    self.resolverCache[scope][str(var)] = varGen
+                    
+                    idx, varDict = next(varGen)
+                    varlist = self.__formatVariablesList(
+                        varDict, scope, filterList)
+            
+            var.insert(0, idx)
         
         self.sendJsonCommand("ResponseVariable", {
             "scope": scope,
             "variable": var,
             "variables": varlist,
         })
-        
+    
     def __extractIndicators(self, var):
         """
         Private method to extract the indicator string from a variable text.
@@ -1702,7 +1710,7 @@
             varlist.append(("data", "str", "{0}".format(value.data())))
         elif qttype == 'QDomComment':
             varlist.append(("data", "str", "{0}".format(value.data())))
-        elif qttype == "QDomDocument":
+        elif qttype == 'QDomDocument':
             varlist.append(("text", "str", "{0}".format(value.toString())))
         elif qttype == 'QDomElement':
             varlist.append(("tagName", "str", "{0}".format(value.tagName())))
@@ -1715,10 +1723,14 @@
             varlist.append(
                 ("address", "QHostAddress", "{0}".format(value.toString())))
             
+        # PySide specific
+        elif qttype == 'EnumType':  # Not in PyQt possible
+            for key, value in value.values.items():
+                varlist.append((key, qttype, "{0}".format(int(value))))
+        
         return varlist
     
-    def __formatVariablesList(self, keylist, dict_, scope, filterList=None,
-                              formatSequences=False, maxSize=0):
+    def __formatVariablesList(self, dict_, scope, filterList=None):
         """
         Private method to produce a formated variables list.
         
@@ -1728,8 +1740,6 @@
         expressions. The formated variables list (a list of tuples of 3
         values) is returned.
         
-        @param keylist keys of the dictionary to be formatted
-        @type list of str
         @param dict_ the dictionary to be scanned
         @type dict
         @param scope 1 to filter using the globals filter, 0 using the locals
@@ -1741,14 +1751,6 @@
             Variables are only added to the list, if their type is not
             contained in the filter list.
         @type list of int
-        @param formatSequences flag indicating, that sequence or dictionary
-            variables should be formatted. If it is 0 (or false), just the
-            number of items contained in these variables is returned.
-        @type bool
-        @param maxSize maximum size the formatted value of a variable will
-            be shown. If it is bigger than that, a 'too big' indication will
-            be placed in the value field.
-        @type int
         @return A tuple consisting of a list of formatted variables. Each
             variable entry is a tuple of three elements, the variable name,
             its type and value.
@@ -1761,8 +1763,14 @@
             patternFilterObjects = self.globalsFilterObjects
         else:
             patternFilterObjects = self.localsFilterObjects
+        if type(dict_) == dict:
+            dict_ = dict_.items()
         
-        for key in keylist:
+        for key, value in dict_:
+            # no more elements available
+            if key == -2:
+                break
+            
             # filter based on the filter pattern
             matched = False
             for pat in patternFilterObjects:
@@ -1773,9 +1781,7 @@
                 continue
             
             # filter hidden attributes (filter #0)
-            if 0 in filterList and str(key)[:2] == '__' and not (
-                key == "___len___" and
-                    DebugVariables.TooLargeAttribute in keylist):
+            if 0 in filterList and str(key)[:2] == '__':
                 continue
             
             # special handling for '__builtins__' (it's way too big)
@@ -1783,67 +1789,57 @@
                 rvalue = '<module __builtin__ (built-in)>'
                 valtype = 'module'
             else:
-                value = dict_[key]
                 valtypestr = str(type(value))[1:-1]
                 _, valtype = valtypestr.split(' ', 1)
                 valtype = valtype[1:-1]
+                # Strip 'instance' to be equal with Python 3
+                if valtype == "instancemethod":
+                    valtype = "method"
+                
                 valtypename = type(value).__name__
-                if valtype not in ConfigVarTypeStrings:
-                    if valtype in ["numpy.ndarray", "array.array"]:
+                try:
+                    if ConfigVarTypeStrings.index(valtype) in filterList:
+                        continue
+                except ValueError:
+                    if valtype == "classobj":
+                        if ConfigVarTypeStrings.index(
+                                'instance') in filterList:
+                            continue
+                    elif valtype == "sip.methoddescriptor":
+                        if ConfigVarTypeStrings.index('method') in filterList:
+                            continue
+                    elif valtype == "sip.enumtype":
+                        if ConfigVarTypeStrings.index('class') in filterList:
+                            continue
+                    elif not valtype.startswith(("PySide.", "PySide2.")) and \
+                        (ConfigVarTypeStrings.index('other') in
+                         filterList):
+                        continue
+                    elif valtype in ["numpy.ndarray", "array.array"]:
                         if ConfigVarTypeStrings.index('list') in filterList:
                             continue
                     elif valtypename == "MultiValueDict":
                         if ConfigVarTypeStrings.index('dict') in filterList:
                             continue
-                    elif valtype == "sip.methoddescriptor":
-                        if ConfigVarTypeStrings.index(
-                                'method') in filterList:
-                            continue
-                    elif valtype == "sip.enumtype":
-                        if ConfigVarTypeStrings.index('class') in filterList:
-                            continue
-                    elif ConfigVarTypeStrings.index('instance') in filterList:
-                        continue
                     
                     if (not valtypestr.startswith('type ') and
                             valtypename not in
-                            ["ndarray", "MultiValueDict", "array"]):
+                            ["ndarray", "MultiValueDict", "array"] and
+                            not valtype.startswith(('PyQt5.', 'PyQt4.'))):
                         valtype = valtypestr
-                else:
-                    try:
-                        # Strip 'instance' to be equal with Python 3
-                        if valtype == "instancemethod":
-                            valtype = "method"
-                        
-                        if ConfigVarTypeStrings.index(valtype) in filterList:
-                            continue
-                    except ValueError:
-                        if valtype == "classobj":
-                            if ConfigVarTypeStrings.index(
-                                    'instance') in filterList:
-                                continue
-                        elif valtype == "sip.methoddescriptor":
-                            if ConfigVarTypeStrings.index(
-                                    'method') in filterList:
-                                continue
-                        elif valtype == "sip.enumtype":
-                            if ConfigVarTypeStrings.index('class') in \
-                                    filterList:
-                                continue
-                        elif not valtype.startswith("PySide") and \
-                            (ConfigVarTypeStrings.index('other') in
-                             filterList):
-                            continue
                 
                 try:
-                    if valtype in ['list', 'tuple', 'dict', 'set',
-                                   'frozenset', 'array.array']:
-                        if valtype == 'dict':
-                            rvalue = "{0:d}".format(len(value.keys()))
-                        else:
-                            rvalue = "{0:d}".format(len(value))
+                    if valtype in ['list', 'tuple', 'dict', 'set', 'frozenset'
+                                   ]:
+                        rvalue = "{0:d}".format(len(value))
+                    elif valtype == 'array.array':
+                        rvalue = "{0:d}|{1}".format(
+                            len(value), value.typecode)
+                    elif valtype == 'collections.defaultdict':
+                        rvalue = "{0:d}|{1}".format(
+                            len(value), value.default_factory)
                     elif valtype == "numpy.ndarray":
-                        rvalue = "{0:d}".format(value.size)
+                        rvalue = "x".join(str(x) for x in value.shape)
                     elif valtypename == "MultiValueDict":
                         rvalue = "{0:d}".format(len(value.keys()))
                         valtype = "django.MultiValueDict"  # shortened type
@@ -1852,16 +1848,12 @@
                         if valtype.startswith('class') and \
                            rvalue[0] in ['{', '(', '[']:
                             rvalue = ""
-                        elif maxSize and len(rvalue) > maxSize:
-                            rvalue = "@@TOO_BIG_TO_SHOW@@"
+                        elif (rvalue.startswith("<class '") and
+                                valtype.startswith(ConfigQtNames)):
+                            rvalue = rvalue[8:-2]
                 except Exception:
                     rvalue = ''
             
-            if formatSequences:
-                if str(key) == key:
-                    key = "'{0!s}'".format(key)
-                else:
-                    key = str(key)
             varlist.append((key, valtype, rvalue))
         
         return varlist
--- a/eric6/DebugClients/Python/DebugConfig.py	Sun Apr 21 17:27:52 2019 +0200
+++ b/eric6/DebugClients/Python/DebugConfig.py	Sun Apr 21 21:20:24 2019 +0200
@@ -24,5 +24,15 @@
     'instancemethod'
 ]
 
+BatchSize = 200
+ConfigQtNames = ('PyQt5.', 'PyQt4.', 'PySide2.', 'PySide.', 'Shiboken.')
+ConfigKnownQtTypes = ('.QChar', '.QByteArray', '.QString', '.QStringList',
+    '.QPoint', '.QPointF', '.QRect', '.QRectF', '.QSize', '.QSizeF', '.QColor',
+    '.QDate', '.QTime', '.QDateTime', '.QDir', '.QFile', '.QFont', '.QUrl',
+    '.QModelIndex', '.QRegExp', '.QAction', '.QKeySequence', '.QDomAttr',
+    '.QDomCharacterData', '.QDomComment', '.QDomDocument', '.QDomElement',
+    '.QDomText', '.QHostAddress', '.EnumType'
+    )
+
 #
 # eflag: noqa = M702
--- a/eric6/DebugClients/Python/DebugVariables.py	Sun Apr 21 17:27:52 2019 +0200
+++ b/eric6/DebugClients/Python/DebugVariables.py	Sun Apr 21 21:20:24 2019 +0200
@@ -7,14 +7,16 @@
 Module implementing classes and functions to dump variable contents.
 """
 
+import sys
+
+from DebugConfig import ConfigQtNames, BatchSize
+
 #
 # This code was inspired by pydevd.
 #
 
-MaxItemsToHandle = 300
-TooLargeMessage = ("Too large to show contents. Max items to show: " +
-                   str(MaxItemsToHandle))
-TooLargeAttribute = "Too large to be handled."
+if sys.version_info[0] > 2:
+    basestring = str
 
 ############################################################
 ## Classes implementing resolvers for various compund types
@@ -35,44 +37,6 @@
         @type str
         @return value of the attribute
         @rtype any
-        @exception NotImplementedError raised to indicate a missing
-            implementation
-        """     # __IGNORE_WARNING_D235__
-        raise NotImplementedError
-    
-    def getDictionary(self, var):
-        """
-        Public method to get the attributes of a variable as a dictionary.
-        
-        @param var variable to be converted
-        @type any
-        @return dictionary containing the variable attributes
-        @rtype dict
-        @exception NotImplementedError raised to indicate a missing
-            implementation
-        """     # __IGNORE_WARNING_D235__
-        raise NotImplementedError
-
-
-############################################################
-## Default Resolver
-############################################################
-
-
-class DefaultResolver(BaseResolver):
-    """
-    Class used to resolve the default way.
-    """
-    def resolve(self, var, attribute):
-        """
-        Public method to get an attribute from a variable.
-        
-        @param var variable to extract an attribute or value from
-        @type any
-        @param attribute name of the attribute to extract
-        @type str
-        @return value of the attribute
-        @rtype any
         """
         return getattr(var, attribute, None)
     
@@ -101,6 +65,41 @@
 
 
 ############################################################
+## Default Resolver
+############################################################
+
+
+class DefaultResolver(BaseResolver):
+    """
+    Class used to resolve the default way.
+    """
+    def getDictionary(self, var):
+        """
+        Public method to get the attributes of a variable as a dictionary.
+        
+        @param var variable to be converted
+        @type any
+        @return dictionary containing the variable attributes
+        @rtype dict
+        """
+        names = dir(var)
+        if not names and hasattr(var, "__members__"):
+            names = var.__members__
+        
+        d = {}
+        for name in names:
+            try:
+                attribute = getattr(var, name)
+                d[name] = attribute
+            except Exception:
+                pass    # if we can't get it, simply ignore it
+        
+        yield -1, d
+        while True:
+            yield -2, {}
+
+
+############################################################
 ## Resolver for Dictionaries
 ############################################################
 
@@ -120,16 +119,13 @@
         @return value of the attribute
         @rtype any
         """
-        if attribute in ('___len___', TooLargeAttribute):
-            return None
-        
-        if "(ID:" not in attribute:
+        if " (ID:" not in attribute:
             try:
                 return var[attribute]
             except Exception:
                 return getattr(var, attribute, None)
         
-        expectedID = int(attribute.split("(ID:")[-1][:-1])
+        expectedID = int(attribute.split(" (ID:")[-1][:-1])
         for key, value in var.items():
             if id(key) == expectedID:
                 return value
@@ -145,10 +141,14 @@
         @return string representation of the given key
         @rtype str
         """
-        if isinstance(key, str):
-            return repr(key)
-        else:
-            return key
+        if isinstance(key, basestring):
+            key = repr(key)
+            # Special handling for Python2 unicode strings and bytes object
+            # Raw and f-Strings are always converted to (unicode) str
+            if key[0] in 'ub':
+                key = key[1:]
+
+        return key  # __IGNORE_WARNING_M834__
     
     def getDictionary(self, var):
         """
@@ -160,22 +160,34 @@
         @rtype dict
         """
         d = {}
-        count = 0
-        for key, value in var.items():
-            count += 1
+        start = count = 0
+        allItems = list(var.items())
+        try:
+            # Fast path: all items from same type
+            allItems.sort(key=lambda x: x[0])
+        except TypeError:
+            # Slow path: only sort items with same type (Py3 only)
+            allItems.sort(key=lambda x: (str(x[0]), x[0]))
+        
+        for key, value in allItems:
             key = "{0} (ID:{1})".format(self.keyToStr(key), id(key))
             d[key] = value
-            if count > MaxItemsToHandle:
-                d[TooLargeAttribute] = TooLargeMessage
-                break
+            count += 1
+            if count >= BatchSize:
+                yield start, d
+                start += count
+                count = 0
+                d = {}
         
-        d["___len___"] = len(var)
+        if d:
+            yield start, d
         
         # in case it has additional fields
-        additionals = defaultResolver.getDictionary(var)
-        d.update(additionals)
+        d = super(DictResolver, self).getDictionary(var)
+        yield -1, d
         
-        return d
+        while True:
+            yield -2, {}
 
 
 ############################################################
@@ -198,9 +210,6 @@
         @return value of the attribute
         @rtype any
         """
-        if attribute in ('___len___', TooLargeAttribute):
-            return None
-
         try:
             return var[int(attribute)]
         except Exception:
@@ -216,22 +225,26 @@
         @rtype dict
         """
         d = {}
-        count = 0
-        for value in var:
-            d[str(count)] = value
+        start = count = 0
+        for idx, value in enumerate(var):
+            d[str(idx)] = value
             count += 1
-            if count > MaxItemsToHandle:
-                d[TooLargeAttribute] = TooLargeMessage
-                break
+            if count >= BatchSize:
+                yield start, d
+                start = idx + 1
+                count = 0
+                d = {}
         
-        d["___len___"] = len(var)
+        if d:
+            yield start, d
         
         # in case it has additional fields
-        additionals = defaultResolver.getDictionary(var)
-        d.update(additionals)
+        d = super(ListResolver, self).getDictionary(var)
+        yield -1, d
         
-        return d
-
+        while True:
+            yield -2, {}
+    
 
 ############################################################
 ## Resolver for Sets and Frozensets
@@ -253,11 +266,8 @@
         @return value of the attribute
         @rtype any
         """
-        if attribute in ('___len___', TooLargeAttribute):
-            return None
-
-        if attribute.startswith("ID: "):
-            attribute = attribute.split(None, 1)[1]
+        if attribute.startswith("'ID: "):
+            attribute = attribute.split(None, 1)[1][:-1]
         try:
             attribute = int(attribute)
         except Exception:
@@ -279,22 +289,26 @@
         @rtype dict
         """
         d = {}
-        count = 0
+        start = count = 0
         for value in var:
             count += 1
-            d["ID: " + str(id(value))] = value
-            if count > MaxItemsToHandle:
-                d[TooLargeAttribute] = TooLargeMessage
-                break
-
-        d["___len___"] = len(var)
+            d["'ID: {0}'".format(id(value))] = value
+            if count >= BatchSize:
+                yield start, d
+                start += count
+                count = 0
+                d = {}
+        
+        if d:
+            yield start, d
         
         # in case it has additional fields
-        additionals = defaultResolver.getDictionary(var)
-        d.update(additionals)
+        additionals = super(SetResolver, self).getDictionary(var)
+        yield -1, additionals
         
-        return d
-
+        while True:
+            yield -2, {}
+    
 
 ############################################################
 ## Resolver for Numpy Arrays
@@ -330,9 +344,6 @@
         @return value of the attribute
         @rtype any
         """
-        if attribute == '__internals__':
-            return defaultResolver.getDictionary(var)
-        
         if attribute == 'min':
             if self.__isNumeric(var):
                 return var.min()
@@ -351,25 +362,10 @@
             else:
                 return None
         
-        if attribute == 'shape':
-            return var.shape
-        
-        if attribute == 'dtype':
-            return var.dtype
-        
-        if attribute == 'size':
-            return var.size
-        
-        if attribute.startswith('['):
-            container = NdArrayItemsContainer()
-            count = 0
-            for element in var:
-                setattr(container, str(count), element)
-                count += 1
-                if count > MaxItemsToHandle:
-                    setattr(container, TooLargeAttribute, TooLargeMessage)
-                    break
-            return container
+        try:
+            return var[int(attribute)]
+        except Exception:
+            return getattr(var, attribute, None)
         
         return None
     
@@ -383,38 +379,49 @@
         @rtype dict
         """
         d = {}
-        d['__internals__'] = defaultResolver.getDictionary(var)
+        start = count = 0
+        allItems = var.tolist()
+        
+        for idx, value in enumerate(allItems):
+            d[str(idx)] = value
+            count += 1
+            if count >= BatchSize:
+                yield start, d
+                start += count
+                count = 0
+                d = {}
+        
+        if d:
+            yield start, d
+        
+        # in case it has additional fields
+        d = super(NdArrayResolver, self).getDictionary(var)
+        
         if var.size > 1024 * 1024:
             d['min'] = 'ndarray too big, calculating min would slow down' \
                        ' debugging'
             d['max'] = 'ndarray too big, calculating max would slow down' \
                        ' debugging'
-        else:
-            if self.__isNumeric(var):
-                if var.size == 0:
-                    d['min'] = 'empty array'
-                    d['max'] = 'empty array'
-                    d['mean'] = 'empty array'
-                else:
-                    d['min'] = var.min()
-                    d['max'] = var.max()
-                    d['mean'] = var.mean()
+            d['mean'] = 'ndarray too big, calculating mean would slow down' \
+                        ' debugging'
+        elif self.__isNumeric(var):
+            if var.size == 0:
+                d['min'] = 'empty array'
+                d['max'] = 'empty array'
+                d['mean'] = 'empty array'
             else:
-                d['min'] = 'not a numeric object'
-                d['max'] = 'not a numeric object'
-                d['mean'] = 'not a numeric object'
-        d['shape'] = var.shape
-        d['dtype'] = var.dtype
-        d['size'] = var.size
-        d['[0:{0}]'.format(len(var) - 1)] = list(var[0:MaxItemsToHandle])
-        return d
-
-
-class NdArrayItemsContainer:
-    """
-    Class to store ndarray items.
-    """
-    pass
+                d['min'] = var.min()
+                d['max'] = var.max()
+                d['mean'] = var.mean()
+        else:
+            d['min'] = 'not a numeric object'
+            d['max'] = 'not a numeric object'
+            d['mean'] = 'not a numeric object'
+        
+        yield -1, d
+        
+        while True:
+            yield -2, {}
 
 
 ############################################################
@@ -437,20 +444,16 @@
         @return value of the attribute
         @rtype any
         """
-        if attribute in ('___len___', TooLargeAttribute):
-            return None
-        
-        if "(ID:" not in attribute:
+        if " (ID:" not in attribute:
             try:
                 return var[attribute]
             except Exception:
                 return getattr(var, attribute, None)
         
-        expectedID = int(attribute.split("(ID:")[-1][:-1])
+        expectedID = int(attribute.split(" (ID:")[-1][:-1])
         for key in var.keys():
             if id(key) == expectedID:
-                value = var.getlist(key)
-                return value
+                return var.getlist(key)
         
         return None
     
@@ -464,20 +467,35 @@
         @rtype dict
         """
         d = {}
-        count = 0
-        for key in var.keys():
+        start = count = 0
+        allKeys = list(var.keys())
+        try:
+            # Fast path: all items from same type
+            allKeys.sort()
+        except TypeError:
+            # Slow path: only sort items with same type (Py3 only)
+            allKeys.sort(key=lambda x: (str(x), x))
+        
+        for key in allKeys:
+            dkey = "{0} (ID:{1})".format(self.keyToStr(key), id(key))
+            d[dkey] = var.getlist(key)
             count += 1
-            value = var.getlist(key)
-            key = "{0} (ID:{1})".format(self.keyToStr(key), id(key))
-            d[key] = value
-            if count > MaxItemsToHandle:
-                d[TooLargeAttribute] = TooLargeMessage
-                break
+            if count >= BatchSize:
+                yield start, d
+                start += count
+                count = 0
+                d = {}
         
-        d["___len___"] = len(var)
+        if d:
+            yield start, d
         
-        return d
-
+        # in case it has additional fields
+        d = super(DictResolver, self).getDictionary(var)
+        yield -1, d
+        
+        while True:
+            yield -2, {}
+    
 
 ############################################################
 ## Resolver for array.array
@@ -515,28 +533,10 @@
         @return value of the attribute
         @rtype any
         """
-        if attribute == 'itemsize':
-            return var.itemsize
-        
-        if attribute == 'typecode':
-            return var.typecode
-        
-        if attribute == 'type':
-            if var.typecode in ArrayResolver.TypeCodeMap:
-                return ArrayResolver.TypeCodeMap[var.typecode]
-            else:
-                return 'illegal type'
-        
-        if attribute.startswith('['):
-            container = ArrayItemsContainer()
-            count = 0
-            for element in var:
-                setattr(container, str(count), element)
-                count += 1
-                if count > MaxItemsToHandle:
-                    setattr(container, TooLargeAttribute, TooLargeMessage)
-                    break
-            return container
+        try:
+            return var[int(attribute)]
+        except Exception:
+            return getattr(var, attribute, None)
         
         return None
     
@@ -550,21 +550,31 @@
         @rtype dict
         """
         d = {}
-        d['typecode'] = var.typecode
-        if var.typecode in ArrayResolver.TypeCodeMap:
-            d['type'] = ArrayResolver.TypeCodeMap[var.typecode]
-        else:
-            d['type'] = 'illegal type'
-        d['itemsize'] = var.itemsize
-        d['[0:{0}]'.format(len(var) - 1)] = var.tolist()[0:MaxItemsToHandle]
-        return d
-
-
-class ArrayItemsContainer:
-    """
-    Class to store array.array items.
-    """
-    pass
+        start = count = 0
+        allItems = var.tolist()
+        
+        for idx, value in enumerate(allItems):
+            d[str(idx)] = value
+            count += 1
+            if count >= BatchSize:
+                yield start, d
+                start += count
+                count = 0
+                d = {}
+        
+        if d:
+            yield start, d
+        
+        # in case it has additional fields
+        d = super(ArrayResolver, self).getDictionary(var)
+        
+        # Special data for array type: convert typecode to readable text
+        d['type'] = self.TypeCodeMap.get(var.typecode, 'illegal type')
+        
+        yield -1, d
+        
+        while True:
+            yield -2, {}
 
 
 defaultResolver = DefaultResolver()
@@ -598,6 +608,8 @@
         (tuple, listResolver),
         (list, listResolver),
         (dict, dictResolver),
+        (set, setResolver),
+        (frozenset, setResolver),
     ]
     
     try:
@@ -611,16 +623,6 @@
         pass    # not available on all python versions
 
     try:
-        _TypeMap.append((set, setResolver))     # __IGNORE_WARNING__
-    except Exception:
-        pass    # not available on all python versions
-
-    try:
-        _TypeMap.append((frozenset, setResolver))     # __IGNORE_WARNING__
-    except Exception:
-        pass    # not available on all python versions
-    
-    try:
         import array
         _TypeMap.append((array.array, arrayResolver))
     except ImportError:
@@ -634,8 +636,8 @@
     
     try:
         from django.utils.datastructures import MultiValueDict
+        # it should go before dict
         _TypeMap.insert(0, (MultiValueDict, multiValueDictResolver))
-        # it should go before dict
     except ImportError:
         pass  # django may not be installed
 
@@ -646,27 +648,28 @@
     
     @param obj object to get type information for
     @type any
-    @return tuple containing the type, type name, type string and resolver
-    @rtype tuple of type, str, str, BaseResolver
+    @return tuple containing the type name, type string and resolver
+    @rtype tuple of str, str, BaseResolver
     """
     typeObject = type(obj)
     typeName = typeObject.__name__
-    typeStr = str(typeObject)[8:-2]
+    # Between PyQt and PySide the returned type is different (class vs. type)
+    typeStr = str(typeObject).split(' ', 1)[-1]
+    typeStr = typeStr[1:-2]
     
-    if typeStr.startswith(("PyQt5.", "PyQt4.")):
+    if typeStr.startswith(ConfigQtNames):
         resolver = None
     else:
         if _TypeMap is None:
             _initTypeMap()
         
-        for typeData in _TypeMap:
-            if isinstance(obj, typeData[0]):
-                resolver = typeData[1]
+        for typeData, resolver in _TypeMap:  # __IGNORE_WARNING_M507__
+            if isinstance(obj, typeData):
                 break
         else:
             resolver = defaultResolver
     
-    return typeObject, typeName, typeStr, resolver
+    return typeName, typeStr, resolver
 
 #
 # eflag: noqa = M702
--- 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

eric ide

mercurial