UI/PythonAstViewer.py

changeset 6932
8a3df4c6ac9a
parent 6931
faac36ec9d76
equal deleted inserted replaced
6931:faac36ec9d76 6932:8a3df4c6ac9a
15 except NameError: 15 except NameError:
16 pass 16 pass
17 17
18 import ast 18 import ast
19 19
20 from PyQt5.QtCore import Qt, QTimer 20 from PyQt5.QtCore import pyqtSlot, Qt, QTimer
21 from PyQt5.QtGui import QCursor, QBrush 21 from PyQt5.QtGui import QCursor, QBrush
22 from PyQt5.QtWidgets import QTreeWidget, QApplication, QTreeWidgetItem, \ 22 from PyQt5.QtWidgets import QTreeWidget, QApplication, QTreeWidgetItem, \
23 QAbstractItemView, QWidget, QVBoxLayout 23 QAbstractItemView, QWidget, QVBoxLayout
24 24
25 from ThirdParty.asttokens.asttokens import ASTTokens 25 from ThirdParty.asttokens.asttokens import ASTTokens
26 26
27 27
28 # TODO: highlight code area in editor when a tree node is clicked
29 # TODO: jump to node when a double click in the editor is detected
30 # (rebuild the tree, if the source code has changed)
31 class PythonAstViewer(QWidget): 28 class PythonAstViewer(QWidget):
32 """ 29 """
33 Class implementing a widget to visualize the Python AST for some Python 30 Class implementing a widget to visualize the Python AST for some Python
34 sources. 31 sources.
35 """ 32 """
66 self.__astWidget.setSortingEnabled(False) 63 self.__astWidget.setSortingEnabled(False)
67 self.__astWidget.setSelectionBehavior(QAbstractItemView.SelectRows) 64 self.__astWidget.setSelectionBehavior(QAbstractItemView.SelectRows)
68 self.__astWidget.setSelectionMode(QAbstractItemView.SingleSelection) 65 self.__astWidget.setSelectionMode(QAbstractItemView.SingleSelection)
69 self.__astWidget.setAlternatingRowColors(True) 66 self.__astWidget.setAlternatingRowColors(True)
70 67
68 self.__astWidget.itemClicked.connect(self.__astItemClicked)
69
71 self.__vm.astViewerStateChanged.connect(self.__astViewerStateChanged) 70 self.__vm.astViewerStateChanged.connect(self.__astViewerStateChanged)
72 71
73 self.hide() 72 self.hide()
74 73
75 def __editorChanged(self, editor): 74 def __editorChanged(self, editor):
78 77
79 @param editor reference to the current editor 78 @param editor reference to the current editor
80 @type Editor 79 @type Editor
81 """ 80 """
82 if editor is not self.__editor: 81 if editor is not self.__editor:
82 if self.__editor:
83 self.__editor.clearAllHighlights()
83 self.__editor = editor 84 self.__editor = editor
84 if self.__editor: 85 if self.__editor:
85 self.__loadAST() 86 self.__loadAST()
86 87
87 def __editorSaved(self, editor): 88 def __editorSaved(self, editor):
92 @type Editor 93 @type Editor
93 """ 94 """
94 if editor and editor is self.__editor: 95 if editor and editor is self.__editor:
95 self.__loadAST() 96 self.__loadAST()
96 97
98 def __editorDoubleClicked(self, editor, pos, buttons):
99 """
100 Private slot to handle a mouse button double click in the editor.
101
102 @param editor reference to the editor, that emitted the signal
103 @type Editor
104 @param pos position of the double click
105 @type QPoint
106 @param buttons mouse buttons that were double clicked
107 @type Qt.MouseButtons
108 """
109 if editor is self.__editor and buttons == Qt.LeftButton:
110 if editor.isModified():
111 # reload the source
112 QTimer.singleShot(0, self.__loadAST)
113 else:
114 # highlight the corresponding entry
115 QTimer.singleShot(0, self.__selectItemForEditorSelection)
116 QTimer.singleShot(0, self.__grabFocus)
117
97 def __lastEditorClosed(self): 118 def __lastEditorClosed(self):
98 """ 119 """
99 Private slot to handle the last editor closed signal of the view 120 Private slot to handle the last editor closed signal of the view
100 manager. 121 manager.
101 """ 122 """
108 super(PythonAstViewer, self).show() 129 super(PythonAstViewer, self).show()
109 130
110 if not self.__vmConnected: 131 if not self.__vmConnected:
111 self.__vm.editorChangedEd.connect(self.__editorChanged) 132 self.__vm.editorChangedEd.connect(self.__editorChanged)
112 self.__vm.editorSavedEd.connect(self.__editorSaved) 133 self.__vm.editorSavedEd.connect(self.__editorSaved)
134 self.__vm.editorDoubleClickedEd.connect(self.__editorDoubleClicked)
113 self.__vmConnected = True 135 self.__vmConnected = True
114 136
115 def hide(self): 137 def hide(self):
116 """ 138 """
117 Public slot to hide the AST viewer. 139 Public slot to hide the AST viewer.
118 """ 140 """
119 super(PythonAstViewer, self).hide() 141 super(PythonAstViewer, self).hide()
142
143 if self.__editor:
144 self.__editor.clearAllHighlights()
120 145
121 if self.__vmConnected: 146 if self.__vmConnected:
122 self.__vm.editorChangedEd.disconnect(self.__editorChanged) 147 self.__vm.editorChangedEd.disconnect(self.__editorChanged)
123 self.__vm.editorSavedEd.disconnect(self.__editorSaved) 148 self.__vm.editorSavedEd.disconnect(self.__editorSaved)
149 self.__vm.editorDoubleClickedEd.disconnect(
150 self.__editorDoubleClicked)
124 self.__vmConnected = False 151 self.__vmConnected = False
125 152
126 def shutdown(self): 153 def shutdown(self):
127 """ 154 """
128 Public method to perform shutdown actions. 155 Public method to perform shutdown actions.
141 if editor is not self.__editor: 168 if editor is not self.__editor:
142 self.__editor = editor 169 self.__editor = editor
143 self.show() 170 self.show()
144 self.__loadAST() 171 self.__loadAST()
145 else: 172 else:
173 self.hide()
146 self.__editor = None 174 self.__editor = None
147 self.hide()
148 175
149 def __createErrorItem(self, error): 176 def __createErrorItem(self, error):
150 """ 177 """
151 Private method to create a top level error item. 178 Private method to create a top level error item.
152 179
167 """ 194 """
168 if not self.__editor: 195 if not self.__editor:
169 return 196 return
170 197
171 self.__astWidget.clear() 198 self.__astWidget.clear()
199 self.__editor.clearAllHighlights()
172 200
173 if not self.__editor.isPyFile(): 201 if not self.__editor.isPyFile():
174 self.__createErrorItem(self.tr( 202 self.__createErrorItem(self.tr(
175 "The current editor text does not contain Python source." 203 "The current editor text does not contain Python source."
176 )) 204 ))
200 QTimer.singleShot(0, self.__resizeColumns) 228 QTimer.singleShot(0, self.__resizeColumns)
201 229
202 self.setUpdatesEnabled(True) 230 self.setUpdatesEnabled(True)
203 231
204 QApplication.restoreOverrideCursor() 232 QApplication.restoreOverrideCursor()
233
234 self.__grabFocus()
205 235
206 def __populateNode(self, name, nodeOrFields, parent): 236 def __populateNode(self, name, nodeOrFields, parent):
207 """ 237 """
208 Private method to populate the tree view with a node. 238 Private method to populate the tree view with a node.
209 239
291 @param textRange tuple giving the start and end positions 321 @param textRange tuple giving the start and end positions
292 @type tuple of (int, int, int, int) 322 @type tuple of (int, int, int, int)
293 @return best matching node 323 @return best matching node
294 @rtype ast.AST 324 @rtype ast.AST
295 """ 325 """
296 if textRange == (-1, -1, -1, -1): 326 if textRange in [(-1, -1, -1, -1), (0, -1, 0, -1)]:
297 # no valid range, i.e. no selection 327 # no valid range, i.e. no selection
298 return None 328 return None
299 329
300 # first look among children 330 # first look among children
301 for child in ast.iter_child_nodes(node): 331 for child in ast.iter_child_nodes(node):
322 @param textRange tuple giving the start and end positions 352 @param textRange tuple giving the start and end positions
323 @type tuple of (int, int, int, int) 353 @type tuple of (int, int, int, int)
324 @return best matching tree item 354 @return best matching tree item
325 @rtype QTreeWidgetItem 355 @rtype QTreeWidgetItem
326 """ 356 """
327 if textRange == (-1, -1, -1, -1): 357 if textRange in [(-1, -1, -1, -1), (0, -1, 0, -1)]:
328 # no valid range, i.e. no selection 358 # no valid range, i.e. no selection
359 return None
360
361 lineno = itm.data(0, self.StartLineRole)
362 if lineno is not None and not self.__rangeContainsSmallerOrEqual(
363 (itm.data(0, self.StartLineRole), itm.data(0, self.StartIndexRole),
364 itm.data(0, self.EndLineRole), itm.data(0, self.EndIndexRole)),
365 textRange):
329 return None 366 return None
330 367
331 # first look among children 368 # first look among children
332 for index in range(itm.childCount()): 369 for index in range(itm.childCount()):
333 child = itm.child(index) 370 child = itm.child(index)
335 if result is not None: 372 if result is not None:
336 return result 373 return result
337 374
338 # no suitable child was found 375 # no suitable child was found
339 lineno = itm.data(0, self.StartLineRole) 376 lineno = itm.data(0, self.StartLineRole)
340 if lineno is not None and self.__rangeContainsSmaller( 377 if lineno is not None and self.__rangeContainsSmallerOrEqual(
341 (itm.data(0, self.StartLineRole), itm.data(0, self.StartIndexRole), 378 (itm.data(0, self.StartLineRole), itm.data(0, self.StartIndexRole),
342 itm.data(0, self.EndLineRole), itm.data(0, self.EndIndexRole)), 379 itm.data(0, self.EndLineRole), itm.data(0, self.EndIndexRole)),
343 textRange): 380 textRange):
344 return itm 381 return itm
345 else: 382 else:
389 (firstStart < secondStart and firstEnd > secondEnd) or 426 (firstStart < secondStart and firstEnd > secondEnd) or
390 (firstStart == secondStart and firstEnd > secondEnd) or 427 (firstStart == secondStart and firstEnd > secondEnd) or
391 (firstStart < secondStart and firstEnd == secondEnd) 428 (firstStart < secondStart and firstEnd == secondEnd)
392 ) 429 )
393 430
431 def __rangeContainsSmallerOrEqual(self, first, second):
432 """
433 Private method to check, if second is contained in or equal to first.
434
435 @param first text range to check against
436 @type tuple of (int, int, int, int)
437 @param second text range to check for
438 @type tuple of (int, int, int, int)
439 @return flag indicating second is contained in or equal to first
440 @rtype bool
441 """
442 return first == second or self.__rangeContainsSmaller(first, second)
443
394 def __clearSelection(self): 444 def __clearSelection(self):
395 """ 445 """
396 Private method to clear all selected items. 446 Private method to clear all selected items.
397 """ 447 """
398 for itm in self.__astWidget.selectedItems(): 448 for itm in self.__astWidget.selectedItems():
416 self.__astWidget.topLevelItem(0), selection) 466 self.__astWidget.topLevelItem(0), selection)
417 if itm: 467 if itm:
418 self.__astWidget.scrollToItem( 468 self.__astWidget.scrollToItem(
419 itm, QAbstractItemView.PositionAtCenter) 469 itm, QAbstractItemView.PositionAtCenter)
420 itm.setSelected(True) 470 itm.setSelected(True)
471
472 def __grabFocus(self):
473 """
474 Private method to grab the input focus.
475 """
476 self.__astWidget.setFocus(Qt.OtherFocusReason)
477
478 @pyqtSlot(QTreeWidgetItem, int)
479 def __astItemClicked(self, itm, column):
480 """
481 Private slot handling a user click on an AST node item.
482
483 @param itm reference to the clicked item
484 @type QTreeWidgetItem
485 @param column column number of the click
486 @type int
487 """
488 self.__editor.clearAllHighlights()
489
490 if itm is not None:
491 startLine = itm.data(0, self.StartLineRole)
492 if startLine is not None:
493 startIndex = itm.data(0, self.StartIndexRole)
494 endLine = itm.data(0, self.EndLineRole)
495 endIndex = itm.data(0, self.EndIndexRole)
496
497 self.__editor.gotoLine(startLine, firstVisible=True,
498 expand=True)
499 self.__editor.setHighlight(startLine - 1, startIndex,
500 endLine - 1, endIndex)

eric ide

mercurial