1# -*- coding: utf-8 -*-
2
3# Copyright (c) 2002 - 2021 Detlev Offenbach <detlev@die-offenbachs.de>
4#
5
6"""
7Module implementing the variables viewer view based on QTreeView.
8"""
9
10import ast
11import re
12import contextlib
13
14from PyQt5.QtCore import (
15    Qt, QAbstractItemModel, QModelIndex, QCoreApplication,
16    QSortFilterProxyModel, pyqtSignal
17)
18from PyQt5.QtGui import QBrush, QFontMetrics
19from PyQt5.QtWidgets import QTreeView, QAbstractItemView, QToolTip, QMenu
20
21from E5Gui.E5Application import e5App
22
23from .Config import ConfigVarTypeDispStrings
24from DebugClients.Python.DebugConfig import ConfigQtNames, ConfigKnownQtTypes
25
26import Preferences
27import Utilities
28
29SORT_ROLE = Qt.ItemDataRole.UserRole
30
31
32class VariableItem:
33    """
34    Class implementing the data structure for all variable items.
35    """
36    Type2Indicators = {
37        # Python types
38        'list': '[]',
39        'tuple': '()',
40        'dict': '{:}',                          # __IGNORE_WARNING_M613__
41        'set': '{}',                            # __IGNORE_WARNING_M613__
42        'frozenset': '{}',                      # __IGNORE_WARNING_M613__
43        'numpy.ndarray': '[ndarray]',           # __IGNORE_WARNING_M613__
44    }
45
46    # Initialize regular expression for unprintable strings
47    rx_nonprintable = re.compile(r"""(\\x\d\d)+""")
48
49    noOfItemsStr = QCoreApplication.translate("VariablesViewer", "{0} items")
50    unsized = QCoreApplication.translate("VariablesViewer", "unsized")
51
52    arrayTypes = {
53        'list', 'tuple', 'dict', 'set', 'frozenset', 'numpy.ndarray',
54        'django.MultiValueDict', 'array.array', 'collections.defaultdict',
55        "class 'dict_items'", "class 'dict_keys'", "class 'dict_values'",
56    }
57
58    nonExpandableTypes = (
59        'method_descriptor', 'wrapper_descriptor', '', 'getset_descriptor',
60        'method-wrapper', 'member_descriptor',
61    )
62
63    def __init__(self, parent, dvar, dtype, dvalue):
64        """
65        Constructor
66
67        @param parent reference to the parent item
68        @type VariableItem
69        @param dvar variable name
70        @type str
71        @param dtype type string
72        @type str
73        @param dvalue value string
74        @type str
75        """
76        self.parent = parent
77        # Take the additional methods into account for childCount
78        self.methodCount = 0
79        self.childCount = 0
80        self.currentCount = -1  # -1 indicates to (re)load children
81        # Indicator that there are children
82        self.hasChildren = False
83        self.populated = False
84        # Indicator that item was at least once fully populated
85        self.wasPopulated = False
86
87        self.children = []
88        # Flag to prevent endless reloading of current item while waiting on
89        # a response from debugger
90        self.pendigFetch = False
91
92        # Set of child items, which are displayed the first time or changed
93        self.newItems = set()
94        self.changedItems = set()
95        # Name including its ID if it's a dict, set, etc.
96        self.nameWithId = dvar
97
98        self.name = ''
99        self.sort = ''
100        self.type = ''
101        self.indicator = ''
102        self.value = None
103        self.valueShort = None
104        self.tooltip = ''
105
106        self.__getName(dvar)
107        self.__getType(dtype)
108        self.__getValue(dtype, dvalue)
109
110    def __getName(self, dvar):
111        """
112        Private method to extract the variable name.
113
114        @param dvar name of variable maybe with ID
115        @type str
116        """
117        try:
118            idx = dvar.index(" (ID:")
119            dvar = dvar[:idx]
120        except AttributeError:
121            idx = dvar
122            dvar = str(dvar)
123        except ValueError:
124            pass
125
126        self.name = dvar
127        try:
128            # Convert numbers to strings with preceding zeros
129            sort = int(dvar)
130            sort = "{0:06}".format(sort)
131        except ValueError:
132            sort = dvar.lower()
133
134        self.sort = sort
135
136    def __getType(self, dtype):
137        """
138        Private method to process the type of the variable.
139
140        If type is known to have children, the corresponding flag is set.
141
142        @param dtype type string
143        @type str
144        """
145        # Python class?
146        if dtype.startswith('class '):
147            dtype = dtype[7:-1]
148        # Qt related stuff?
149        elif (
150            (dtype.startswith(ConfigQtNames) and
151             dtype.endswith(ConfigKnownQtTypes)) or
152            dtype in ('instance', 'class')
153        ):
154            self.hasChildren = True
155
156        # Special Qt types should not be expanded infinite
157        elif ".{0}".format(dtype) in ConfigKnownQtTypes:
158            self.type = dtype  # It's a Qt type, so skipping translation is ok
159            return
160
161        vtype = ConfigVarTypeDispStrings.get(dtype, dtype)
162        # Unkown types should be expandable by default
163        if vtype is dtype and dtype not in self.nonExpandableTypes:
164            self.hasChildren = True
165        self.type = QCoreApplication.translate("VariablesViewer", vtype)
166
167    def __getValue(self, dtype, dvalue):
168        """
169        Private method to process the variables value.
170
171        Define and limit value, set tooltip text. If type is known to have
172        children, the corresponding flag is set.
173
174        @param dtype type string
175        @type str
176        @param dvalue value of variable encoded as utf-8
177        @type str
178        """
179        if dtype == 'collections.defaultdict':
180            dvalue, default_factory = dvalue.split('|')
181            self.indicator = '{{:<{0}>}}'.format(default_factory)
182        elif dtype == 'array.array':
183            dvalue, typecode = dvalue.split('|')
184            self.indicator = '[<{0}>]'.format(typecode)
185        else:
186            self.indicator = VariableItem.Type2Indicators.get(dtype, '')
187
188        if dtype == 'numpy.ndarray':
189            if dvalue:
190                self.childCount = int(dvalue.split('x')[0])
191                dvalue = VariableItem.noOfItemsStr.format(dvalue)
192            else:
193                dvalue = VariableItem.unsized
194            self.hasChildren = True
195
196        elif dtype in VariableItem.arrayTypes:
197            self.childCount = int(dvalue)
198            dvalue = VariableItem.noOfItemsStr.format(dvalue)
199            self.hasChildren = True
200
201        elif dtype == "Shiboken.EnumType":
202            self.hasChildren = True
203
204        elif dtype == 'str':
205            if VariableItem.rx_nonprintable.search(dvalue) is None:
206                with contextlib.suppress(Exception):
207                    dvalue = ast.literal_eval(dvalue)
208            dvalue = str(dvalue)
209
210        elif (
211            dvalue.startswith(("{", "(", "[")) and
212            dvalue.endswith(("}", ")", "]"))
213        ):
214            # it is most probably a dict, tuple or list derived class
215            with contextlib.suppress(Exception):
216                value = ast.literal_eval(dvalue)
217                valueTypeStr = str(type(value))[8:-2]
218                if valueTypeStr in VariableItem.arrayTypes:
219                    self.childCount = len(value)
220                    self.hasChildren = True
221
222        elif (
223            (dvalue.endswith("})") and "({" in dvalue) or
224            (dvalue.endswith("])") and "([" in dvalue)
225        ):
226            # that is probably a set derived class
227            with contextlib.suppress(Exception):
228                value = ast.literal_eval(dvalue.split("(", 1)[1][:-1])
229                valueTypeStr = str(type(value))[8:-2]
230                if valueTypeStr in VariableItem.arrayTypes:
231                    self.childCount = len(value)
232                    self.hasChildren = True
233
234        self.value = dvalue
235
236        if len(dvalue) > 2048:     # 2 kB
237            self.tooltip = dvalue[:2048]
238            dvalue = QCoreApplication.translate(
239                "VariableItem", "<double click to show value>")
240        else:
241            self.tooltip = dvalue
242
243        lines = dvalue[:2048].splitlines()
244        if len(lines) > 1:
245            # only show the first non-empty line;
246            # indicate skipped lines by <...> at the
247            # beginning and/or end
248            index = 0
249            while index < len(lines) - 1 and lines[index].strip(' \t') == "":
250                index += 1
251
252            dvalue = ""
253            if index > 0:
254                dvalue += "<...>"
255            dvalue += lines[index]
256            if index < len(lines) - 1 or len(dvalue) > 2048:
257                dvalue += "<...>"
258
259        self.valueShort = dvalue
260
261    @property
262    def absolutCount(self):
263        """
264        Public property to get the total number of children.
265
266        @return total number of children
267        @rtype int
268        """
269        return self.childCount + self.methodCount
270
271
272class VariablesModel(QAbstractItemModel):
273    """
274    Class implementing the data model for QTreeView.
275
276    @signal expand trigger QTreeView to expand given index
277    """
278    expand = pyqtSignal(QModelIndex)
279
280    def __init__(self, treeView, globalScope):
281        """
282        Constructor
283
284        @param treeView QTreeView showing the data
285        @type VariablesViewer
286        @param globalScope flag indicating global (True) or local (False)
287            variables
288        @type bool
289        """
290        super().__init__()
291        self.treeView = treeView
292        self.proxyModel = treeView.proxyModel
293
294        self.framenr = -1
295        self.openItems = []
296        self.closedItems = []
297
298        visibility = self.tr("Globals") if globalScope else self.tr("Locals")
299        self.rootNode = VariableItem(None, visibility, self.tr("Type"),
300                                     self.tr("Value"))
301
302        self.__globalScope = globalScope
303
304    def clear(self, reset=False):
305        """
306        Public method to clear the complete data model.
307
308        @param reset flag to clear the expanded keys also
309        @type bool
310        """
311        self.beginResetModel()
312        self.rootNode.children = []
313        self.rootNode.newItems.clear()
314        self.rootNode.changedItems.clear()
315        self.rootNode.wasPopulated = False
316        if reset:
317            self.openItems = []
318            self.closedItems = []
319        self.endResetModel()
320
321    def __findVariable(self, pathlist):
322        """
323        Private method to get to the given variable.
324
325        @param pathlist full path to the variable
326        @type list of str
327        @return the found variable or None if it doesn't exist
328        @rtype VariableItem or None
329        """
330        node = self.rootNode
331
332        for childName in pathlist or []:
333            for item in node.children:
334                if item.nameWithId == childName:
335                    node = item
336                    break
337            else:
338                return None
339
340        return node  # __IGNORE_WARNING_M834__
341
342    def showVariables(self, vlist, frmnr, pathlist=None):
343        """
344        Public method to update the data model of variable in pathlist.
345
346        @param vlist the list of variables to be displayed. Each
347                list entry is a tuple of three values.
348                <ul>
349                <li>the variable name (string)</li>
350                <li>the variables type (string)</li>
351                <li>the variables value (string)</li>
352                </ul>
353        @type list of str
354        @param frmnr frame number (0 is the current frame)
355        @type int
356        @param pathlist full path to the variable
357        @type list of str
358        """
359        if pathlist:
360            itemStartIndex = pathlist.pop(0)
361        else:
362            itemStartIndex = -1
363            if self.framenr != frmnr:
364                self.clear()
365                self.framenr = frmnr
366
367        parent = self.__findVariable(pathlist)
368        if parent is None:
369            return
370
371        parent.pendigFetch = False
372
373        if parent == self.rootNode:
374            parentIdx = QModelIndex()
375            parent.methodCount = len(vlist)
376        else:
377            row = parent.parent.children.index(parent)
378            parentIdx = self.createIndex(row, 0, parent)
379
380        if itemStartIndex == -3:
381            # Item doesn't exist any more
382            parentIdx = self.parent(parentIdx)
383            self.beginRemoveRows(parentIdx, row, row)
384            del parent.parent.children[row]
385            self.endRemoveRows()
386            parent.parent.childCount -= 1
387            return
388
389        elif itemStartIndex == -2:
390            parent.wasPopulated = True
391            parent.currentCount = parent.absolutCount
392            parent.populated = True
393            # Remove items which are left over at the end of child list
394            self.__cleanupParentList(parent, parentIdx)
395            return
396
397        elif itemStartIndex == -1:
398            parent.methodCount = len(vlist)
399            idx = max(parent.currentCount, 0)
400            parent.currentCount = idx + len(vlist)
401            parent.populated = True
402        else:
403            idx = itemStartIndex
404            parent.currentCount = idx + len(vlist)
405
406        # Sort items for Python versions where dict doesn't retain order
407        vlist.sort(key=lambda x: x[0])
408        # Now update the table
409        endIndex = idx + len(vlist)
410        newChild = None
411        knownChildrenCount = len(parent.children)
412        while idx < endIndex:
413            # Fetch next old item from last cycle
414            try:
415                child = parent.children[idx]
416            except IndexError:
417                child = None
418
419            # Fetch possible new item
420            if not newChild and vlist:
421                newChild = vlist.pop(0)
422
423                # Process parameters of new item
424                newItem = VariableItem(parent, *newChild)
425                sort = newItem.sort
426
427            # Append or insert before already existing item
428            if child is None or newChild and sort < child.sort:
429                self.beginInsertRows(parentIdx, idx, idx)
430                parent.children.insert(idx, newItem)
431                if knownChildrenCount <= idx and not parent.wasPopulated:
432                    parent.newItems.add(newItem)
433                    knownChildrenCount += 1
434                else:
435                    parent.changedItems.add(newItem)
436                self.endInsertRows()
437
438                idx += 1
439                newChild = None
440                continue
441
442            # Check if same name, type and afterwards value
443            elif sort == child.sort and child.type == newItem.type:
444                # Check if value has changed
445                if child.value != newItem.value:
446                    child.value = newItem.value
447                    child.valueShort = newItem.valueShort
448                    child.tooltip = newItem.tooltip
449                    child.nameWithId = newItem.nameWithId
450
451                    child.currentCount = -1
452                    child.populated = False
453                    child.childCount = newItem.childCount
454
455                    # Highlight item because it has changed
456                    parent.changedItems.add(child)
457
458                    changedIndexStart = self.index(idx, 0, parentIdx)
459                    changedIndexEnd = self.index(idx, 2, parentIdx)
460                    self.dataChanged.emit(changedIndexStart, changedIndexEnd)
461
462                newChild = None
463                idx += 1
464                continue
465
466            # Remove obsolete item
467            self.beginRemoveRows(parentIdx, idx, idx)
468            parent.children.remove(child)
469            self.endRemoveRows()
470            # idx stay unchanged
471            knownChildrenCount -= 1
472
473        # Remove items which are left over at the end of child list
474        if itemStartIndex == -1:
475            parent.wasPopulated = True
476            self.__cleanupParentList(parent, parentIdx)
477
478        # Request data for any expanded node
479        self.getMore()
480
481    def __cleanupParentList(self, parent, parentIdx):
482        """
483        Private method to remove items which are left over at the end of the
484        child list.
485
486        @param parent to clean up
487        @type VariableItem
488        @param parentIdx the parent index as QModelIndex
489        @type QModelIndex
490        """
491        end = len(parent.children)
492        if end > parent.absolutCount:
493            self.beginRemoveRows(parentIdx, parent.absolutCount, end)
494            del parent.children[parent.absolutCount:]
495            self.endRemoveRows()
496
497    def resetModifiedMarker(self, parentIdx=QModelIndex(), pathlist=()):
498        """
499        Public method to remove the modified marker from changed items.
500
501        @param parentIdx item to reset marker
502        @type QModelIndex
503        @param pathlist full path to the variable
504        @type list of str
505        """
506        parent = (parentIdx.internalPointer() if parentIdx.isValid()
507                  else self.rootNode)
508
509        parent.newItems.clear()
510        parent.changedItems.clear()
511
512        pll = len(pathlist)
513        posPaths = {x for x in self.openItems if len(x) > pll}
514        posPaths |= {x for x in self.closedItems if len(x) > pll}
515        posPaths = {x[pll] for x in posPaths if x[:pll] == pathlist}
516
517        if posPaths:
518            for child in parent.children:
519                if (
520                    child.hasChildren and
521                    child.nameWithId in posPaths and
522                    child.currentCount >= 0
523                ):
524                    # Discard loaded elements and refresh if still expanded
525                    child.currentCount = -1
526                    child.populated = False
527                    row = parent.children.index(child)
528                    newParentIdx = self.index(row, 0, parentIdx)
529                    self.resetModifiedMarker(
530                        newParentIdx, pathlist + (child.nameWithId,))
531
532        self.closedItems = []
533
534        # Little quirk: Refresh all visible items to clear the changed marker
535        if parentIdx == QModelIndex():
536            self.rootNode.currentCount = -1
537            self.rootNode.populated = False
538            idxStart = self.index(0, 0, QModelIndex())
539            idxEnd = self.index(0, 2, QModelIndex())
540            self.dataChanged.emit(idxStart, idxEnd)
541
542    def columnCount(self, parent=QModelIndex()):
543        """
544        Public method to get the column count.
545
546        @param parent the model parent
547        @type QModelIndex
548        @return number of columns
549        @rtype int
550        """
551        return 3
552
553    def rowCount(self, parent=QModelIndex()):
554        """
555        Public method to get the row count.
556
557        @param parent the model parent
558        @type QModelIndex
559        @return number of rows
560        @rtype int
561        """
562        node = parent.internalPointer() if parent.isValid() else self.rootNode
563
564        return len(node.children)
565
566    def flags(self, index):
567        """
568        Public method to get the item flags.
569
570        @param index of item
571        @type QModelIndex
572        @return item flags
573        @rtype QtCore.Qt.ItemFlag
574        """
575        if not index.isValid():
576            return Qt.ItemFlag.NoItemFlags
577
578        return Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsSelectable
579
580    def hasChildren(self, parent=QModelIndex()):
581        """
582        Public method to get a flag if parent has children.
583
584        @param parent the model parent
585        @type QModelIndex
586        @return flag indicating parent has children
587        @rtype bool
588        """
589        if not parent.isValid():
590            return self.rootNode.children != []
591
592        return parent.internalPointer().hasChildren
593
594    def index(self, row, column, parent=QModelIndex()):
595        """
596        Public method to get the index of item at row:column of parent.
597
598        @param row number of rows
599        @type int
600        @param column number of columns
601        @type int
602        @param parent the model parent
603        @type QModelIndex
604        @return new model index for child
605        @rtype QModelIndex
606        """
607        if not self.hasIndex(row, column, parent):
608            return QModelIndex()
609
610        node = parent.internalPointer() if parent.isValid() else self.rootNode
611
612        return self.createIndex(row, column, node.children[row])
613
614    def parent(self, child):
615        """
616        Public method to get the parent of the given child.
617
618        @param child the model child node
619        @type QModelIndex
620        @return new model index for parent
621        @rtype QModelIndex
622        """
623        if not child.isValid():
624            return QModelIndex()
625
626        childNode = child.internalPointer()
627        if childNode == self.rootNode:
628            return QModelIndex()
629
630        parentNode = childNode.parent
631
632        if parentNode == self.rootNode:
633            return QModelIndex()
634
635        row = parentNode.parent.children.index(parentNode)
636        return self.createIndex(row, 0, parentNode)
637
638    def data(self, index, role=Qt.ItemDataRole.DisplayRole):
639        """
640        Public method get the role data of item.
641
642        @param index the model index
643        @type QModelIndex
644        @param role the requested data role
645        @type QtCore.Qt.ItemDataRole
646        @return role data of item
647        @rtype Any
648        """
649        if not index.isValid() or index.row() < 0:
650            return None
651
652        node = index.internalPointer()
653        column = index.column()
654
655        if role in (
656            Qt.ItemDataRole.DisplayRole, SORT_ROLE, Qt.ItemDataRole.EditRole
657        ):
658            try:
659                if column == 0:
660                    # Sort first column with values from third column
661                    if role == SORT_ROLE:
662                        return node.sort
663                    return node.name + node.indicator
664                else:
665                    return {
666                        1: node.valueShort,
667                        2: node.type,
668                        3: node.sort
669                    }.get(column)
670            except AttributeError:
671                return ['None', '', '', ''][column]
672
673        elif role == Qt.ItemDataRole.BackgroundRole:
674            if node in node.parent.changedItems:
675                return self.__bgColorChanged
676            elif node in node.parent.newItems:
677                return self.__bgColorNew
678
679        elif role == Qt.ItemDataRole.ToolTipRole:
680            if column == 0:
681                tooltip = node.name + node.indicator
682            elif column == 1:
683                tooltip = node.tooltip
684            elif column == 2:
685                tooltip = node.type
686            elif column == 3:
687                tooltip = node.sort
688            else:
689                return None
690
691            if Qt.mightBeRichText(tooltip):
692                tooltip = Utilities.html_encode(tooltip)
693
694            if column == 0:
695                indentation = self.treeView.indentation()
696                indentCount = 0
697                currentNode = node
698                while currentNode.parent:
699                    indentCount += 1
700                    currentNode = currentNode.parent
701
702                indentation *= indentCount
703            else:
704                indentation = 0
705            # Check if text is longer than available space
706            fontMetrics = QFontMetrics(self.treeView.font())
707            try:
708                textSize = fontMetrics.horizontalAdvance(tooltip)
709            except AttributeError:
710                textSize = fontMetrics.width(tooltip)
711            textSize += indentation + 5  # How to determine border size?
712            header = self.treeView.header()
713            if textSize >= header.sectionSize(column):
714                return tooltip
715            else:
716                QToolTip.hideText()
717
718        return None
719
720    def headerData(self, section, orientation,
721                   role=Qt.ItemDataRole.DisplayRole):
722        """
723        Public method get the header names.
724
725        @param section the header section (row/coulumn)
726        @type int
727        @param orientation the header's orientation
728        @type QtCore.Qt.Orientation
729        @param role the requested data role
730        @type QtCore.Qt.ItemDataRole
731        @return header name
732        @rtype str or None
733        """
734        if (
735            role != Qt.ItemDataRole.DisplayRole or
736            orientation != Qt.Orientation.Horizontal
737        ):
738            return None
739
740        return {
741            0: self.rootNode.name,
742            1: self.rootNode.value,
743            2: self.rootNode.type,
744            3: self.rootNode.sort
745        }.get(section)
746
747    def __findPendingItem(self, parent=None, pathlist=()):
748        """
749        Private method to find the next item to request data from debugger.
750
751        @param parent the model parent
752        @type VariableItem
753        @param pathlist full path to the variable
754        @type list of str
755        @return next item index to request data from debugger
756        @rtype QModelIndex
757        """
758        if parent is None:
759            parent = self.rootNode
760
761        for child in parent.children:
762            if not child.hasChildren:
763                continue
764
765            if pathlist + (child.nameWithId,) in self.openItems:
766                if child.populated:
767                    index = None
768                else:
769                    idx = parent.children.index(child)
770                    index = self.createIndex(idx, 0, child)
771                    self.expand.emit(index)
772
773                if child.currentCount < 0:
774                    return index
775
776                possibleIndex = self.__findPendingItem(
777                    child, pathlist + (child.nameWithId,))
778
779                if (possibleIndex or index) is None:
780                    continue
781
782                return possibleIndex or index
783
784        return None
785
786    def getMore(self):
787        """
788        Public method to fetch the next variable from debugger.
789        """
790        # step 1: find expanded but not populated items
791        item = self.__findPendingItem()
792        if not item or not item.isValid():
793            return
794
795        # step 2: check if data has to be retrieved
796        node = item.internalPointer()
797        lastVisibleItem = self.index(node.currentCount - 1, 0, item)
798        lastVisibleItem = self.proxyModel.mapFromSource(lastVisibleItem)
799        rect = self.treeView.visualRect(lastVisibleItem)
800        if rect.y() > self.treeView.height() or node.pendigFetch:
801            return
802
803        node.pendigFetch = True
804        # step 3: get a pathlist up to the requested variable
805        pathlist = self.__buildTreePath(node)
806        # step 4: request the variable from the debugger
807        variablesFilter = e5App().getObject("DebugUI").variablesFilter(
808            self.__globalScope)
809        e5App().getObject("DebugServer").remoteClientVariable(
810            e5App().getObject("DebugUI").getSelectedDebuggerId(),
811            self.__globalScope, variablesFilter, pathlist, self.framenr)
812
813    def setExpanded(self, index, state):
814        """
815        Public method to set the expanded state of item.
816
817        @param index item to change expanded state
818        @type QModelIndex
819        @param state state of the item
820        @type bool
821        """
822        node = index.internalPointer()
823        pathlist = self.__buildTreePath(node)
824        if state:
825            if pathlist not in self.openItems:
826                self.openItems.append(pathlist)
827                if pathlist in self.closedItems:
828                    self.closedItems.remove(pathlist)
829                self.getMore()
830        else:
831            if pathlist in self.openItems:
832                self.openItems.remove(pathlist)
833            self.closedItems.append(pathlist)
834
835    def __buildTreePath(self, parent):
836        """
837        Private method to build up a path from the root to parent.
838
839        @param parent item to build the path for
840        @type VariableItem
841        @return list of names denoting the path from the root
842        @rtype tuple of str
843        """
844        pathlist = []
845
846        # build up a path from the top to the item
847        while parent.parent:
848            pathlist.append(parent.nameWithId)
849            parent = parent.parent
850
851        pathlist.reverse()
852        return tuple(pathlist)
853
854    def handlePreferencesChanged(self):
855        """
856        Public slot to handle the preferencesChanged signal.
857        """
858        self.__bgColorNew = QBrush(Preferences.getDebugger("BgColorNew"))
859        self.__bgColorChanged = QBrush(
860            Preferences.getDebugger("BgColorChanged"))
861
862        idxStart = self.index(0, 0, QModelIndex())
863        idxEnd = self.index(0, 2, QModelIndex())
864        self.dataChanged.emit(idxStart, idxEnd)
865
866
867class VariablesProxyModel(QSortFilterProxyModel):
868    """
869    Class for handling the sort operations.
870    """
871    def __init__(self, parent=None):
872        """
873        Constructor
874
875        @param parent the parent model index
876        @type QModelIndex
877        """
878        super().__init__(parent)
879        self.setSortRole(SORT_ROLE)
880
881    def hasChildren(self, parent):
882        """
883        Public method to get a flag if parent has children.
884
885        The given model index has to be transformed to the underlying source
886        model to get the correct result.
887
888        @param parent the model parent
889        @type QModelIndex
890        @return flag if parent has children
891        @rtype bool
892        """
893        return self.sourceModel().hasChildren(self.mapToSource(parent))
894
895    def setExpanded(self, index, state):
896        """
897        Public slot to get a flag if parent has children.
898
899        The given model index has to be transformed to the underlying source
900        model to get the correct result.
901        @param index item to change expanded state
902        @type QModelIndex
903        @param state state of the item
904        @type bool
905        """
906        self.sourceModel().setExpanded(self.mapToSource(index), state)
907
908
909class VariablesViewer(QTreeView):
910    """
911    Class implementing the variables viewer view.
912
913    This view is used to display the variables of the program being
914    debugged in a tree. Compound types will be shown with
915    their main entry first. Once the subtree has been expanded, the
916    individual entries will be shown. Double clicking an entry will
917    expand or collapse the item, if it has children and the double click
918    was performed on the first column of the tree, otherwise it'll
919    popup a dialog showing the variables parameters in a more readable
920    form. This is especially useful for lengthy strings.
921
922    This view has two modes for displaying the global and the local
923    variables.
924
925    @signal preferencesChanged() to inform model about new background colours
926    """
927    preferencesChanged = pyqtSignal()
928
929    def __init__(self, viewer, globalScope, parent=None):
930        """
931        Constructor
932
933        @param viewer reference to the debug viewer object
934        @type DebugViewer
935        @param globalScope flag indicating global (True) or local (False)
936            variables
937        @type bool
938        @param parent the parent
939        @type QWidget
940        """
941        super().__init__(parent)
942
943        self.__debugViewer = viewer
944        self.__globalScope = globalScope
945        self.framenr = 0
946
947        # Massive performance gain
948        self.setUniformRowHeights(True)
949
950        # Implements sorting and filtering
951        self.proxyModel = VariablesProxyModel()
952        # Variable model implements the underlying data model
953        self.varModel = VariablesModel(self, globalScope)
954        self.proxyModel.setSourceModel(self.varModel)
955        self.setModel(self.proxyModel)
956        self.preferencesChanged.connect(self.varModel.handlePreferencesChanged)
957        self.preferencesChanged.emit()  # Force initialization of colors
958
959        self.expanded.connect(
960            lambda idx: self.proxyModel.setExpanded(idx, True))
961        self.collapsed.connect(
962            lambda idx: self.proxyModel.setExpanded(idx, False))
963
964        self.setExpandsOnDoubleClick(False)
965        self.doubleClicked.connect(self.__itemDoubleClicked)
966
967        self.varModel.expand.connect(self.__mdlRequestExpand)
968
969        self.setSortingEnabled(True)
970        self.setAlternatingRowColors(True)
971        self.setSelectionBehavior(
972            QAbstractItemView.SelectionBehavior.SelectRows)
973
974        if self.__globalScope:
975            self.setWindowTitle(self.tr("Global Variables"))
976            self.setWhatsThis(self.tr(
977                """<b>The Global Variables Viewer Window</b>"""
978                """<p>This window displays the global variables"""
979                """ of the debugged program.</p>"""
980            ))
981        else:
982            self.setWindowTitle(self.tr("Local Variables"))
983            self.setWhatsThis(self.tr(
984                """<b>The Local Variables Viewer Window</b>"""
985                """<p>This window displays the local variables"""
986                """ of the debugged program.</p>"""
987            ))
988
989        header = self.header()
990        header.setSortIndicator(0, Qt.SortOrder.AscendingOrder)
991        header.setSortIndicatorShown(True)
992
993        try:
994            header.setSectionsClickable(True)
995        except Exception:
996            header.setClickable(True)
997
998        header.resizeSection(0, 130)    # variable column
999        header.resizeSection(1, 180)    # value column
1000        header.resizeSection(2, 50)     # type column
1001
1002        header.sortIndicatorChanged.connect(lambda *x: self.varModel.getMore())
1003
1004        self.__createPopupMenus()
1005        self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
1006        self.customContextMenuRequested.connect(self.__showContextMenu)
1007
1008        self.resortEnabled = True
1009
1010    def showVariables(self, vlist, frmnr):
1011        """
1012        Public method to show variables in a list.
1013
1014        @param vlist the list of variables to be displayed. Each
1015                list entry is a tuple of three values.
1016                <ul>
1017                <li>the variable name (string)</li>
1018                <li>the variables type (string)</li>
1019                <li>the variables value (string)</li>
1020                </ul>
1021        @type list
1022        @param frmnr frame number (0 is the current frame)
1023        @type int
1024        """
1025        self.varModel.resetModifiedMarker()
1026        self.varModel.showVariables(vlist, frmnr)
1027
1028    def showVariable(self, vlist):
1029        """
1030        Public method to show variables in a list.
1031
1032        @param vlist the list of subitems to be displayed.
1033                The first element gives the path of the
1034                parent variable. Each other list entry is
1035                a tuple of three values.
1036                <ul>
1037                <li>the variable name (string)</li>
1038                <li>the variables type (string)</li>
1039                <li>the variables value (string)</li>
1040                </ul>
1041        @type list
1042        """
1043        self.varModel.showVariables(vlist[1:], 0, vlist[0])
1044
1045    def handleResetUI(self):
1046        """
1047        Public method to reset the VariablesViewer.
1048        """
1049        self.varModel.clear(True)
1050
1051    def verticalScrollbarValueChanged(self, value):
1052        """
1053        Public slot informing about the scrollbar change.
1054
1055        @param value current value of the vertical scrollbar
1056        @type int
1057        """
1058        self.varModel.getMore()
1059        super().verticalScrollbarValueChanged(value)
1060
1061    def resizeEvent(self, event):
1062        """
1063        Protected slot informing about the widget size change.
1064
1065        @param event information
1066        @type QResizeEvent
1067        """
1068        self.varModel.getMore()
1069        super().resizeEvent(event)
1070
1071    def __itemDoubleClicked(self, index):
1072        """
1073        Private method called if an item was double clicked.
1074
1075        @param index the double clicked item
1076        @type QModelIndex
1077        """
1078        node = self.proxyModel.mapToSource(index).internalPointer()
1079        if node.hasChildren and index.column() == 0:
1080            state = self.isExpanded(index)
1081            self.setExpanded(index, not state)
1082        else:
1083            self.__showVariableDetails(index)
1084
1085    def __mdlRequestExpand(self, modelIndex):
1086        """
1087        Private method to inform the view about items to be expand.
1088
1089        @param modelIndex the model index
1090        @type QModelIndex
1091        """
1092        index = self.proxyModel.mapFromSource(modelIndex)
1093        self.expand(index)
1094
1095    def __createPopupMenus(self):
1096        """
1097        Private method to generate the popup menus.
1098        """
1099        self.menu = QMenu()
1100        self.menu.addAction(self.tr("Show Details..."), self.__showDetails)
1101        self.menu.addSeparator()
1102        self.menu.addAction(self.tr("Expand"), self.__expandChildren)
1103        self.menu.addAction(self.tr("Collapse"), self.__collapseChildren)
1104        self.menu.addAction(self.tr("Collapse All"), self.collapseAll)
1105        self.menu.addSeparator()
1106        self.menu.addAction(self.tr("Refresh"), self.__refreshView)
1107        self.menu.addSeparator()
1108        self.menu.addAction(self.tr("Configure..."), self.__configure)
1109        self.menu.addAction(self.tr("Variables Type Filter..."),
1110                            self.__configureFilter)
1111
1112        self.backMenu = QMenu()
1113        self.backMenu.addAction(self.tr("Refresh"), self.__refreshView)
1114        self.backMenu.addSeparator()
1115        self.backMenu.addAction(self.tr("Configure..."), self.__configure)
1116        self.backMenu.addAction(self.tr("Variables Type Filter..."),
1117                                self.__configureFilter)
1118
1119    def __showContextMenu(self, coord):
1120        """
1121        Private slot to show the context menu.
1122
1123        @param coord the position of the mouse pointer
1124        @type QPoint
1125        """
1126        gcoord = self.mapToGlobal(coord)
1127        if self.indexAt(coord).isValid():
1128            self.menu.popup(gcoord)
1129        else:
1130            self.backMenu.popup(gcoord)
1131
1132    def __expandChildren(self):
1133        """
1134        Private slot to expand all child items of current parent.
1135        """
1136        index = self.currentIndex()
1137        node = self.proxyModel.mapToSource(index).internalPointer()
1138        for child in node.children:
1139            if child.hasChildren:
1140                row = node.children.index(child)
1141                idx = self.varModel.createIndex(row, 0, child)
1142                idx = self.proxyModel.mapFromSource(idx)
1143                self.expand(idx)
1144
1145    def __collapseChildren(self):
1146        """
1147        Private slot to collapse all child items of current parent.
1148        """
1149        index = self.currentIndex()
1150        node = self.proxyModel.mapToSource(index).internalPointer()
1151        for child in node.children:
1152            row = node.children.index(child)
1153            idx = self.varModel.createIndex(row, 0, child)
1154            idx = self.proxyModel.mapFromSource(idx)
1155            if self.isExpanded(idx):
1156                self.collapse(idx)
1157
1158    def __refreshView(self):
1159        """
1160        Private slot to refresh the view.
1161        """
1162        if self.__globalScope:
1163            self.__debugViewer.setGlobalsFilter()
1164        else:
1165            self.__debugViewer.setLocalsFilter()
1166
1167    def __showDetails(self):
1168        """
1169        Private slot to show details about the selected variable.
1170        """
1171        idx = self.currentIndex()
1172        self.__showVariableDetails(idx)
1173
1174    def __showVariableDetails(self, index):
1175        """
1176        Private method to show details about a variable.
1177
1178        @param index reference to the variable item
1179        @type QModelIndex
1180        """
1181        node = self.proxyModel.mapToSource(index).internalPointer()
1182
1183        val = node.value
1184        vtype = node.type
1185        name = node.name
1186
1187        par = node.parent
1188        nlist = [name]
1189
1190        # build up the fully qualified name
1191        while par.parent is not None:
1192            pname = par.name
1193            if par.indicator:
1194                if nlist[0].endswith("."):
1195                    nlist[0] = '[{0}].'.format(nlist[0][:-1])
1196                else:
1197                    nlist[0] = '[{0}]'.format(nlist[0])
1198                nlist.insert(0, pname)
1199            else:
1200                if par.type == "django.MultiValueDict":
1201                    nlist[0] = 'getlist({0})'.format(nlist[0])
1202                elif par.type == "numpy.ndarray":
1203                    if nlist and nlist[0][0].isalpha():
1204                        if nlist[0] in ["min", "max", "mean"]:
1205                            nlist[0] = ".{0}()".format(nlist[0])
1206                        else:
1207                            nlist[0] = ".{0}".format(nlist[0])
1208                    nlist.insert(0, pname)
1209                else:
1210                    nlist.insert(0, '{0}.'.format(pname))
1211            par = par.parent
1212
1213        name = ''.join(nlist)
1214        # now show the dialog
1215        from .VariableDetailDialog import VariableDetailDialog
1216        dlg = VariableDetailDialog(name, vtype, val)
1217        dlg.exec()
1218
1219    def __configure(self):
1220        """
1221        Private method to open the configuration dialog.
1222        """
1223        e5App().getObject("UserInterface").showPreferences(
1224            "debuggerGeneralPage")
1225
1226    def __configureFilter(self):
1227        """
1228        Private method to open the variables filter dialog.
1229        """
1230        e5App().getObject("DebugUI").dbgFilterAct.triggered.emit()
1231
1232#
1233# eflag: noqa = M822
1234