eric7/Debugger/VariablesViewer.py

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

eric ide

mercurial