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. |
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) |