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