Sun, 19 May 2019 12:30:02 +0200
Variables Viewer: merged the Variables Viewer extensions provided by Tobias into the default branch.
--- a/docs/changelog Thu May 16 18:58:12 2019 +0200 +++ b/docs/changelog Sun May 19 12:30:02 2019 +0200 @@ -24,6 +24,17 @@ - setup.py Wizard -- updated the Trove classifiers list -- added capability to retrieve the Trove classifiers list from PyPI +- Variables Viewer + -- reimplemented Variables Viewer using QTreeView with unlimited access to + big arrays, dicts, etc. because elements are lazy loaded + -- highlighting of still unloaded (default: yellow background) and last + changed variable(s) (default: green background) + -- colors for highlighting are configurable through Debugger->General + -- expand / collapse variables with children on double click on first column, + in all other cases display detail window + -- handling of dict views improved (can be expanded like lists) + -- show tooltips on all elements which don't fit into current column width + -- new options in the context menu, e.g. expand / collapse all child nodes - Third Party packages -- updated EditorConfig to 0.12.2
--- a/eric6/DebugClients/Python/DebugClientBase.py Thu May 16 18:58:12 2019 +0200 +++ b/eric6/DebugClients/Python/DebugClientBase.py Sun May 19 12:30:02 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 @@ -181,6 +181,10 @@ # keep these in sync with VariablesViewer.VariableItem.Indicators Indicators = ("()", "[]", "{:}", "{}") # __IGNORE_WARNING_M613__ + arrayTypes = { + 'list', 'tuple', 'dict', 'set', 'frozenset', "class 'dict_items'", + "class 'dict_keys'", "class 'dict_values'" + } def __init__(self): """ @@ -379,14 +383,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 +1431,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. @@ -1439,14 +1441,11 @@ @type int @param filterList the indices of variable types to be filtered @type list of int - @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 given. - @type int """ 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. @@ -1491,13 +1486,10 @@ @type list of strings @param frmnr distance of frame reported on. 0 is the current frame @type int - @param scope 1 to report global variables, 0 for local variables (int) + @param scope 1 to report global variables, 0 for local variables + @type int @param filterList the indices of variable types to be filtered @type list of int - @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 given. - @type int """ if self.currentThread is None: return @@ -1523,12 +1515,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 +1532,35 @@ else: break - + + idx = -3 # Requested variable doesn't exist anymore + # 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) - elif resolver: - varDict = resolver.getDictionary(variable) - vlist = self.__formatVariablesList( - list(varDict.keys()), varDict, scope, filterList, - maxSize=maxSize) - varlist.extend(vlist) + typeName, typeStr, resolver = DebugVariables.getType(variable) + if resolver: + varGen = resolver.getDictionary(variable) + self.resolverCache[scope][str(var)] = varGen + + idx, varDict = next(varGen) + varlist = self.__formatVariablesList( + varDict, scope, filterList) + else: + # 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 + + 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 +1707,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 +1720,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 +1737,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 +1748,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 +1760,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,95 +1778,80 @@ 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) if key == '__builtins__': rvalue = '<module __builtin__ (built-in)>' valtype = 'module' + if ConfigVarTypeStrings.index(valtype) in filterList: + continue else: - value = dict_[key] + isQt = False + # valtypestr, e.g. class 'PyQt5.QtCore.QPoint' valtypestr = str(type(value))[1:-1] _, valtype = valtypestr.split(' ', 1) + # valtype, e.g. PyQt5.QtCore.QPoint valtype = valtype[1:-1] + # Strip 'instance' to be equal with Python 3 + if valtype == "instancemethod": + valtype = "method" + elif valtype == "type" or valtype == "classobj": + valtype = "class" + + # valtypename, e.g. QPoint 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 == "sip.enumtype": + if ConfigVarTypeStrings.index('class') in filterList: + continue + elif (valtype == "sip.methoddescriptor" or + valtype == "method_descriptor"): + if ConfigVarTypeStrings.index('method') 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 + isQt = valtype.startswith(ConfigQtNames) if (not valtypestr.startswith('type ') and - valtypename not in - ["ndarray", "MultiValueDict", "array"]): + valtypename not in ("ndarray", "MultiValueDict", + "array", "defaultdict") and + not isQt): 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 self.arrayTypes: + 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 else: rvalue = repr(value) - if valtype.startswith('class') and \ - rvalue[0] in ['{', '(', '[']: + if valtype.startswith('class') and rvalue[0] in '{([': rvalue = "" - elif maxSize and len(rvalue) > maxSize: - rvalue = "@@TOO_BIG_TO_SHOW@@" + elif (isQt and rvalue.startswith("<class '")): + 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 Thu May 16 18:58:12 2019 +0200 +++ b/eric6/DebugClients/Python/DebugConfig.py Sun May 19 12:30:02 2019 +0200 @@ -18,11 +18,24 @@ 'slice', 'buffer', 'class', 'instance', 'method', 'property', 'generator', 'function', 'builtin_function_or_method', 'code', 'module', - 'ellipsis', 'traceback', 'frame', 'other', 'frozenset', + 'ellipsis', 'traceback', 'frame', 'other', 'frozenset', 'bytes', # Special case for Python 2: don't add 'instancemethod' to # ConfigVarTypeFilters and leave it always at last position 'instancemethod' ] +BatchSize = 200 +ConfigQtNames = ( + 'PyQt5.', 'PyQt4.', 'PySide2.', 'PySide.', 'Shiboken.EnumType' +) +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 Thu May 16 18:58:12 2019 +0200 +++ b/eric6/DebugClients/Python/DebugVariables.py Sun May 19 12:30:02 2019 +0200 @@ -7,14 +7,16 @@ Module implementing classes and functions to dump variable contents. """ +import sys + +from DebugConfig import ConfigQtNames, ConfigKnownQtTypes, 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,21 +225,59 @@ @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 + + while True: + yield -2, {} + + +############################################################ +## Resolver for dict_items, dict_keys and dict_values +############################################################ + + +class DictViewResolver(ListResolver): + """ + Class used to resolve from dict views. + """ + def resolve(self, var, attribute): + """ + Public method to get an attribute from a variable. - return d + @param var variable to extract an attribute or value from + @type tuple or list + @param attribute id of the value to extract + @type str + @return value of the attribute + @rtype any + """ + return super(DictViewResolver, self).resolve(list(var), attribute) + + 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 + """ + return super(DictViewResolver, self).getDictionary(list(var)) ############################################################ @@ -253,11 +300,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 +323,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 +378,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 +396,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 +413,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 +478,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 +501,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 +567,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,26 +584,37 @@ @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() dictResolver = DictResolver() listResolver = ListResolver() +dictViewResolver = DictViewResolver() setResolver = SetResolver() ndarrayResolver = NdArrayResolver() multiValueDictResolver = MultiValueDictResolver() @@ -598,29 +643,21 @@ (tuple, listResolver), (list, listResolver), (dict, dictResolver), + (set, setResolver), + (frozenset, setResolver), ] try: _TypeMap.append((long, None)) # __IGNORE_WARNING__ except Exception: - pass # not available on all python versions + pass # not available on all Python versions try: _TypeMap.append((unicode, None)) # __IGNORE_WARNING__ except Exception: - pass # not available on all python versions + 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,10 +671,18 @@ 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 + + try: + from collections.abc import ItemsView, KeysView, ValuesView + _TypeMap.append((ItemsView, dictViewResolver)) + _TypeMap.append((KeysView, dictViewResolver)) + _TypeMap.append((ValuesView, dictViewResolver)) + except ImportError: + pass # not available on all Python versions def getType(obj): @@ -646,27 +691,29 @@ @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) and + typeStr.endswith(ConfigKnownQtTypes)): 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/Config.py Thu May 16 18:58:12 2019 +0200 +++ b/eric6/Debugger/Config.py Sun May 19 12:30:02 2019 +0200 @@ -48,7 +48,7 @@ 'ellipsis': QT_TRANSLATE_NOOP('Variable Types', 'Ellipsis'), 'traceback': QT_TRANSLATE_NOOP('Variable Types', 'Traceback'), 'frame': QT_TRANSLATE_NOOP('Variable Types', 'Frame'), - 'other': QT_TRANSLATE_NOOP('Variable Types', 'Other'), + 'bytes': QT_TRANSLATE_NOOP('Variable Types', 'Bytes'), } @@ -84,6 +84,7 @@ 'ellipsis': 28, 'traceback': 29, 'frame': 30, - 'other': 31, + 'other': 31, # Not used anymore but keep to avoid reassignment 'frozenset': 32, + 'bytes': 33, }
--- a/eric6/Debugger/DebugUI.py Thu May 16 18:58:12 2019 +0200 +++ b/eric6/Debugger/DebugUI.py Sun May 19 12:30:02 2019 +0200 @@ -1305,7 +1305,7 @@ elif scope == 0: self.debugViewer.showVariables(variables, False) elif scope == -1: - vlist = [('None', '', '')] + vlist = [(self.tr('No locals available.'), '', '')] self.debugViewer.showVariables(vlist, False) def __clientVariable(self, scope, variables):
--- a/eric6/Debugger/DebugViewer.py Thu May 16 18:58:12 2019 +0200 +++ b/eric6/Debugger/DebugViewer.py Sun May 19 12:30:02 2019 +0200 @@ -45,8 +45,10 @@ the exception logger. Additionally a list of all threads is shown. @signal sourceFile(string, int) emitted to open a source file at a line + @signal preferencesChanged() emitted to react on changed preferences """ sourceFile = pyqtSignal(str, int) + preferencesChanged = pyqtSignal() def __init__(self, debugServer, parent=None): """ @@ -166,6 +168,10 @@ self.setLocalsFilterButton.clicked.connect(self.setLocalsFilter) self.localsFilterEdit.returnPressed.connect(self.setLocalsFilter) + self.preferencesChanged.connect(self.handlePreferencesChanged) + self.preferencesChanged.connect(self.globalsViewer.preferencesChanged) + self.preferencesChanged.connect(self.localsViewer.preferencesChanged) + from .CallStackViewer import CallStackViewer # add the call stack viewer self.callStackViewer = CallStackViewer(self.debugServer) @@ -244,7 +250,7 @@ self.__autoViewSource = Preferences.getDebugger("AutoViewSourceCode") self.sourceButton.setVisible(not self.__autoViewSource) - def preferencesChanged(self): + def handlePreferencesChanged(self): """ Public slot to handle the preferencesChanged signal. """
--- a/eric6/Debugger/VariablesViewer.py Thu May 16 18:58:12 2019 +0200 +++ b/eric6/Debugger/VariablesViewer.py Sun May 19 12:30:02 2019 +0200 @@ -4,33 +4,38 @@ # """ -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, 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 +43,910 @@ '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', + "class 'dict_items'", "class 'dict_keys'", "class 'dict_values'", + } + + nonExpandableTypes = ( + 'method_descriptor', 'wrapper_descriptor', '', 'getset_descriptor', + 'method-wrapper', 'member_descriptor', + ) + + def __init__(self, parent, dvar, dtype, dvalue): """ Constructor @param parent reference to the parent item - @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 + # Indicator that item was at least once fully populated + self.wasPopulated = 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() + self.changedItems = set() + # Name including its ID if it's a dict, set, etc. + self.nameWithId = dvar + + self.name = '' + self.sort = '' + self.type = '' + self.indicator = '' + self.value = None + self.valueShort = None + self.tooltip = '' + + self.__getName(dvar) + self.__getType(dtype) + self.__getValue(dtype, dvalue) + + def __getName(self, dvar): + """ + Private method to extract the variable name. + + @param dvar name of variable maybe with ID + @type str + """ + try: + idx = dvar.index(" (ID:") + dvar = dvar[:idx] + except 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] + # Qt related stuff? + elif (dtype.startswith(ConfigQtNames) and + dtype.endswith(ConfigKnownQtTypes)): + self.hasChilds = True + + elif dtype in ('instance', 'class'): + self.hasChilds = True + + vtype = ConfigVarTypeDispStrings.get(dtype, dtype) + # Unkown types should be expandable by default + if vtype is dtype and dtype not in self.nonExpandableTypes: + self.hasChilds = True + 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') # __IGNORE_WARNING__ + + 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() + self.rootNode.changedItems.clear() + self.rootNode.wasPopulated = False + if reset: + self.openItems = [] + self.closedItems = [] + self.endResetModel() + + def __findVariable(self, pathlist): + """ + Private method to get to the given variable. + + @param pathlist full path to the variable + @type list of str + @return the found variable or None if it doesn't exist + @rtype VariableItem or None + """ + node = self.rootNode + + for childName in pathlist or []: + for item in node.childs: + if item.nameWithId == childName: + node = item + break + else: + return None + + return node # __IGNORE_WARNING_M834__ + + def showVariables(self, vlist, frmnr, pathlist=None): + """ + Public method to update the data model of variable in pathlist. + + @param vlist the list of variables to be displayed. Each + list entry is a tuple of three values. + <ul> + <li>the variable name (string)</li> + <li>the variables type (string)</li> + <li>the variables value (string)</li> + </ul> + @type list of str + @param frmnr frame number (0 is the current frame) + @type int + @param pathlist full path to the variable + @type list of str + """ + if pathlist: + itemStartIndex = pathlist.pop(0) + else: + itemStartIndex = -1 + if self.framenr != frmnr: + self.clear() + self.framenr = frmnr + + parent = self.__findVariable(pathlist) + if parent is None: + return + + parent.pendigFetch = False + + if parent == self.rootNode: + parentIdx = QModelIndex() + parent.methodCount = len(vlist) + else: + row = parent.parent.childs.index(parent) + parentIdx = self.createIndex(row, 0, parent) + + if itemStartIndex == -3: + # Item doesn't exist any more + parentIdx = self.parent(parentIdx) + self.beginRemoveRows(parentIdx, row, row) + del parent.parent.childs[row] + self.endRemoveRows() + parent.parent.childCount -= 1 + return + + elif itemStartIndex == -2: + parent.wasPopulated = True + parent.currentCount = parent.absolutCount + # Remove items which are left over at the end of child list + self.__cleanupParentList(parent, parentIdx) + return + + elif itemStartIndex == -1: + parent.methodCount = len(vlist) + idx = parent.childCount = parent.currentCount + 1 + parent.currentCount += 1 + len(vlist) + else: + idx = parent.currentCount + 1 + parent.currentCount += len(vlist) - 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 + knownChildsCount = len(parent.childs) + 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) + if knownChildsCount <= idx and not parent.wasPopulated: + parent.newItems.add(newItem) + knownChildsCount += 1 + else: + parent.changedItems.add(newItem) + self.endInsertRows() + + idx += 1 + newChild = None + continue + + # Check if same name, type and afterwards value + elif sort == child.sort and child.type == newItem.type: + # Check if value has changed + if child.value != newItem.value: + child.value = newItem.value + child.valueShort = newItem.valueShort + child.tooltip = newItem.tooltip + + child.currentCount = -1 + child.childCount = newItem.childCount + + # Highlight item because it has changed + parent.changedItems.add(child) + + changedIndexStart = self.index(idx, 0, parentIdx) + changedIndexEnd = self.index(idx, 2, parentIdx) + self.dataChanged.emit(changedIndexStart, changedIndexEnd) + + newChild = None + idx += 1 + continue + + # Remove obsolete item + self.beginRemoveRows(parentIdx, idx, idx) + parent.childs.remove(child) + self.endRemoveRows() + # idx stay unchanged + knownChildsCount -= 1 + + # Remove items which are left over at the end of child list + if itemStartIndex == -1: + parent.wasPopulated = True + self.__cleanupParentList(parent, parentIdx) + + # Request data for any expanded node + self.getMore() + + def __cleanupParentList(self, parent, parentIdx): + """ + Private method to remove items which are left over at the end of the + child list. + + @param parent to clean up + @type VariableItem + @param parentIdx the parent index as QModelIndex + @type QModelIndex """ - 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 + + parent.newItems.clear() + parent.changedItems.clear() + + pll = len(pathlist) + posPaths = {x for x in self.openItems if len(x) > pll} + posPaths |= {x for x in self.closedItems if len(x) > pll} + posPaths = {x[pll] for x in posPaths if x[:pll] == pathlist} + + if posPaths: + for child in parent.childs: + if child.hasChilds and child.nameWithId in posPaths: + if child.currentCount >= 0: + # Discard loaded elements and refresh if still expanded + child.currentCount = -1 + row = parent.childs.index(child) + newParentIdx = self.index(row, 0, parentIdx) + self.resetModifiedMarker( + newParentIdx, pathlist + (child.nameWithId,)) + + self.closedItems = [] + + # Little quirk: Refresh all visible items to clear the changed marker + if parentIdx == QModelIndex(): + self.rootNode.currentCount = -1 + idxStart = self.index(0, 0, QModelIndex()) + idxEnd = self.index(0, 2, QModelIndex()) + self.dataChanged.emit(idxStart, idxEnd) + + def columnCount(self, parent=QModelIndex()): + """ + Public Qt slot to get the column count. + + @param parent the model parent + @type QModelIndex + @return number of columns + @rtype int + """ + return 3 + + def rowCount(self, parent=QModelIndex()): + """ + Public Qt slot to get the row count. + + @param parent the model parent + @type QModelIndex + @return number of rows + @rtype int + """ + if parent.isValid(): + node = parent.internalPointer() + else: + node = self.rootNode + + return len(node.childs) + + def flags(self, index): + """ + Public Qt slot to get the item flags. + + @param index of item + @type QModelIndex + @return item flags + @rtype QtCore.Qt.ItemFlag """ - 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. + + @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. - pathlist = [self._buildKey()] - par = self.parent() + @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.changedItems: + return self.__bgColorChanged + elif node in node.parent.newItems: + return self.__bgColorNew + + 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() - # step 1: get a pathlist up to the requested variable - while par is not None: - pathlist.insert(0, par._buildKey()) - par = par.parent() + return None + + def headerData(self, section, orientation, role=Qt.DisplayRole): + """ + Public Qt slot get the header names. + + @param section the header section (row/coulumn) + @type int + @param orientation the header's orientation + @type QtCore.Qt.Orientation + @param role the requested data role + @type QtCore.Qt.ItemDataRole + @return header name + @rtype str or None + """ + if role != Qt.DisplayRole or orientation != Qt.Horizontal: + return None + + if section == 0: + return self.rootNode.name + elif section == 1: + return self.rootNode.value + elif section == 2: + return self.rootNode.type + elif section == 3: + return self.rootNode.sort + + return None + + def __findPendingItem(self, parent=None, pathlist=()): + """ + Private method to find the next item to request data from debugger. + + @param parent the model parent + @type VariableItem + @param pathlist full path to the variable + @type list of str + @return next item index to request data from debugger + @rtype QModelIndex + """ + if parent is None: + parent = self.rootNode + + for child in parent.childs: + if not child.hasChilds: + continue + + if pathlist + (child.nameWithId,) in self.openItems: + if child.populated: + index = None + else: + idx = parent.childs.index(child) + index = self.createIndex(idx, 0, child) + self.expand.emit(index) + + if child.currentCount < 0: + return index + + possibleIndex = self.__findPendingItem( + child, pathlist + (child.nameWithId,)) + + if (possibleIndex or index) is None: + continue + + return possibleIndex or index - # step 2: request the variable from the debugger + 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) + + def handlePreferencesChanged(self): + """ + Public slot to handle the preferencesChanged signal. + """ + self.__bgColorNew = QBrush(Preferences.getDebugger("BgColorNew")) + self.__bgColorChanged = QBrush( + Preferences.getDebugger("BgColorChanged")) + + idxStart = self.index(0, 0, QModelIndex()) + idxEnd = self.index(0, 2, QModelIndex()) + self.dataChanged.emit(idxStart, idxEnd) -class 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. + + @signal preferencesChanged() to inform model about new background colours """ + preferencesChanged = pyqtSignal() + def __init__(self, viewer, globalScope, parent=None): """ Constructor - @param viewer reference to the debug viewer object (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.preferencesChanged.connect(self.varModel.handlePreferencesChanged) + self.preferencesChanged.emit() # Force initialization of colors + 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 +954,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 +963,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 +992,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 +1012,124 @@ <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) + self.menu.addAction( + QCoreApplication.translate('DebugUI', 'Varia&bles Type Filter...'), + self.__configureFilter) - 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) + self.backMenu.addAction( + QCoreApplication.translate('DebugUI', 'Varia&bles Type Filter...'), + self.__configureFilter) + + 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 +1140,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 +1184,26 @@ 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") + + def __configureFilter(self): + """ + Private method to open the variables filter dialog. + """ + e5App().getObject("DebugUI").dbgFilterAct.triggered.emit() + +# +# eflag: noqa = M822
--- a/eric6/Preferences/ConfigurationPages/ConfigurationPageBase.py Thu May 16 18:58:12 2019 +0200 +++ b/eric6/Preferences/ConfigurationPages/ConfigurationPageBase.py Sun May 19 12:30:02 2019 +0200 @@ -9,7 +9,7 @@ from __future__ import unicode_literals -from PyQt5.QtCore import pyqtSlot +from PyQt5.QtCore import pyqtSlot, pyqtSignal from PyQt5.QtGui import QIcon, QPixmap, QColor from PyQt5.QtWidgets import QWidget, QColorDialog, QFontDialog @@ -17,7 +17,11 @@ class ConfigurationPageBase(QWidget): """ Class implementing the base class for all configuration pages. + + @signal colourChanged(str, QColor) To inform about a new colour selection """ + colourChanged = pyqtSignal(str, QColor) + def __init__(self): """ Constructor @@ -68,6 +72,7 @@ button.setProperty("hasAlpha", hasAlpha) button.clicked.connect(lambda: self.__selectColourSlot(button)) self.__coloursDict[colourKey] = [colour, byName] + self.colourChanged.emit(colourKey, colour) @pyqtSlot() def __selectColourSlot(self, button): @@ -80,20 +85,26 @@ colorKey = button.property("colorKey") hasAlpha = button.property("hasAlpha") + colDlg = QColorDialog(self) if hasAlpha: - colour = QColorDialog.getColor( - self.__coloursDict[colorKey][0], self, "", - QColorDialog.ShowAlphaChannel) - else: - colour = QColorDialog.getColor(self.__coloursDict[colorKey][0], - self) - if colour.isValid(): + colDlg.setOptions(QColorDialog.ShowAlphaChannel) + # Set current colour last to avoid conflicts with alpha channel + colDlg.setCurrentColor(self.__coloursDict[colorKey][0]) + colDlg.currentColorChanged.connect( + lambda col: self.colourChanged.emit(colorKey, col)) + colDlg.exec_() + + if colDlg.result() == colDlg.Accepted: + colour = colDlg.selectedColor() size = button.iconSize() pm = QPixmap(size.width(), size.height()) pm.fill(colour) button.setIcon(QIcon(pm)) self.__coloursDict[colorKey][0] = colour + # Update colour selection + self.colourChanged.emit(colorKey, self.__coloursDict[colorKey][0]) + def saveColours(self, prefMethod): """ Public method to save the colour selections. @@ -129,4 +140,4 @@ "{0} {1}".format(font.family(), font.pointSize())) else: font = fontVar - return font + return font # __IGNORE_WARNING_M834__
--- a/eric6/Preferences/ConfigurationPages/DebuggerGeneralPage.py Thu May 16 18:58:12 2019 +0200 +++ b/eric6/Preferences/ConfigurationPages/DebuggerGeneralPage.py Sun May 19 12:30:02 2019 +0200 @@ -11,7 +11,8 @@ import socket -from PyQt5.QtCore import QRegExp, pyqtSlot +from PyQt5.QtCore import Qt, QAbstractItemModel, QModelIndex, QRegExp, pyqtSlot +from PyQt5.QtGui import QBrush, QColor from PyQt5.QtWidgets import QLineEdit, QInputDialog from PyQt5.QtNetwork import QNetworkInterface, QAbstractSocket, QHostAddress @@ -143,11 +144,20 @@ Preferences.getDebugger("BreakAlways")) self.exceptionShellCheckBox.setChecked( Preferences.getDebugger("ShowExceptionInShell")) + self.maxSizeSpinBox.setValue( + Preferences.getDebugger("MaxVariableSize")) + # Set the colours for debug viewer backgrounds + self.previewMdl = PreviewModel() + self.preView.setModel(self.previewMdl) + self.colourChanged.connect(self.previewMdl.setColor) + self.initColour("BgColorNew", self.backgroundNewButton, + Preferences.getDebugger, hasAlpha=True) + self.initColour("BgColorChanged", self.backgroundChangedButton, + Preferences.getDebugger, hasAlpha=True) + self.autoViewSourcecodeCheckBox.setChecked( Preferences.getDebugger("AutoViewSourceCode")) - self.maxSizeSpinBox.setValue( - Preferences.getDebugger("MaxVariableSize")) - + def save(self): """ Public slot to save the Debugger General (1) configuration. @@ -232,11 +242,14 @@ "ShowExceptionInShell", self.exceptionShellCheckBox.isChecked()) Preferences.setDebugger( + "MaxVariableSize", + self.maxSizeSpinBox.value()) + # Store background colors for debug viewer + self.saveColours(Preferences.setDebugger) + + Preferences.setDebugger( "AutoViewSourceCode", self.autoViewSourcecodeCheckBox.isChecked()) - Preferences.setDebugger( - "MaxVariableSize", - self.maxSizeSpinBox.value()) def on_allowedHostsList_currentItemChanged(self, current, previous): """ @@ -304,7 +317,123 @@ """ a valid IP v4 or IP v6 address.""" """ Aborting...</p>""") .format(allowedHost)) + + +class PreviewModel(QAbstractItemModel): + """ + Class to show an example of the selected background colours for the debug + viewer. + """ + def __init__(self): + """ + Constructor + """ + super(PreviewModel, self).__init__() + self.bgColorNew = QBrush(QColor('#FFFFFF')) + self.bgColorChanged = QBrush(QColor('#FFFFFF')) + def setColor(self, key, bgcolour): + """ + Public slot to update the background colour indexed by key. + + @param key the name of background + @type str + @param bgcolour the new background colour + @type QColor + """ + if key == 'BgColorNew': + self.bgColorNew = QBrush(bgcolour) + else: + self.bgColorChanged = QBrush(bgcolour) + + # Force update of preview view + idxStart = self.index(0, 0, QModelIndex()) + idxEnd = self.index(0, 2, QModelIndex()) + self.dataChanged.emit(idxStart, idxEnd) + + 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() + + return self.createIndex(row, column, None) + + def parent(self, child): + """ + Public Qt slot to get the parent of the given child. + + @param child the model child node + @type QModelIndex + @return new model index for parent + @rtype QModelIndex + """ + return QModelIndex() + + 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 1 + + 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 + """ + return 4 + + 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 + """ + return Qt.ItemIsEnabled | Qt.ItemIsSelectable + + 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 str, QBrush or None + """ + if role == Qt.DisplayRole: + return self.tr('Variable name') + elif role == Qt.BackgroundRole: + if index.row() >= 2: + return self.bgColorChanged + else: + return self.bgColorNew + + return None + def create(dlg): """ @@ -313,5 +442,7 @@ @param dlg reference to the configuration dialog @return reference to the instantiated page (ConfigurationPageBase) """ - page = DebuggerGeneralPage() - return page + return DebuggerGeneralPage() + +# +# eflag: noqa = M822
--- a/eric6/Preferences/ConfigurationPages/DebuggerGeneralPage.ui Thu May 16 18:58:12 2019 +0200 +++ b/eric6/Preferences/ConfigurationPages/DebuggerGeneralPage.ui Sun May 19 12:30:02 2019 +0200 @@ -572,6 +572,9 @@ <string>Variables Viewer</string> </property> <layout class="QVBoxLayout" name="verticalLayout_6"> + <property name="spacing"> + <number>9</number> + </property> <item> <layout class="QHBoxLayout" name="horizontalLayout"> <item> @@ -622,6 +625,124 @@ </layout> </item> <item> + <widget class="QGroupBox" name="groupBox_2"> + <property name="title"> + <string>Background Colours</string> + </property> + <layout class="QHBoxLayout" name="horizontalLayout_3" stretch="2,1"> + <item> + <layout class="QGridLayout" name="gridLayout_3"> + <property name="topMargin"> + <number>8</number> + </property> + <property name="bottomMargin"> + <number>8</number> + </property> + <property name="verticalSpacing"> + <number>16</number> + </property> + <item row="1" column="1"> + <widget class="QPushButton" name="backgroundChangedButton"> + <property name="minimumSize"> + <size> + <width>100</width> + <height>0</height> + </size> + </property> + <property name="maximumSize"> + <size> + <width>100</width> + <height>16777215</height> + </size> + </property> + <property name="toolTip"> + <string>Select the background colour for changed items.</string> + </property> + <property name="text"> + <string/> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="label_bgChangedItems"> + <property name="text"> + <string>Background colour of changed elements:</string> + </property> + </widget> + </item> + <item row="0" column="0"> + <widget class="QLabel" name="label_bgFirstLoaded"> + <property name="text"> + <string>Background colour of first opened elements:</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QPushButton" name="backgroundNewButton"> + <property name="minimumSize"> + <size> + <width>100</width> + <height>0</height> + </size> + </property> + <property name="maximumSize"> + <size> + <width>100</width> + <height>16777215</height> + </size> + </property> + <property name="toolTip"> + <string>Select the background colour for elements which are loaded for the first time.</string> + </property> + <property name="text"> + <string/> + </property> + </widget> + </item> + <item row="0" column="2"> + <spacer name="horizontalSpacer_4"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + <item> + <widget class="QListView" name="preView"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Ignored"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="focusPolicy"> + <enum>Qt::NoFocus</enum> + </property> + <property name="verticalScrollBarPolicy"> + <enum>Qt::ScrollBarAlwaysOff</enum> + </property> + <property name="horizontalScrollBarPolicy"> + <enum>Qt::ScrollBarAlwaysOff</enum> + </property> + <property name="editTriggers"> + <set>QAbstractItemView::NoEditTriggers</set> + </property> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> <widget class="QGroupBox" name="groupBox_10"> <property name="title"> <string>Local Variables Viewer</string> @@ -688,6 +809,8 @@ <tabstop>exceptionBreakCheckBox</tabstop> <tabstop>exceptionShellCheckBox</tabstop> <tabstop>maxSizeSpinBox</tabstop> + <tabstop>backgroundNewButton</tabstop> + <tabstop>backgroundChangedButton</tabstop> <tabstop>autoViewSourcecodeCheckBox</tabstop> </tabstops> <resources/>
--- a/eric6/Preferences/__init__.py Thu May 16 18:58:12 2019 +0200 +++ b/eric6/Preferences/__init__.py Sun May 19 12:30:02 2019 +0200 @@ -116,6 +116,8 @@ "NetworkInterface": "127.0.0.1", "AutoViewSourceCode": False, "MaxVariableSize": 0, # Bytes, 0 = no limit + "BgColorNew": QColor("#28FFEEAA"), + "BgColorChanged": QColor("#2870FF66"), } debuggerDefaults["AllowedHosts"] = ["127.0.0.1", "::1%0"] if sys.version_info[0] == 2: