--- a/UI/PythonAstViewer.py Sun Apr 07 19:55:21 2019 +0200 +++ b/UI/PythonAstViewer.py Mon Apr 08 19:08:44 2019 +0200 @@ -17,7 +17,7 @@ import ast -from PyQt5.QtCore import Qt, QTimer +from PyQt5.QtCore import pyqtSlot, Qt, QTimer from PyQt5.QtGui import QCursor, QBrush from PyQt5.QtWidgets import QTreeWidget, QApplication, QTreeWidgetItem, \ QAbstractItemView, QWidget, QVBoxLayout @@ -25,9 +25,6 @@ from ThirdParty.asttokens.asttokens import ASTTokens -# TODO: highlight code area in editor when a tree node is clicked -# TODO: jump to node when a double click in the editor is detected -# (rebuild the tree, if the source code has changed) class PythonAstViewer(QWidget): """ Class implementing a widget to visualize the Python AST for some Python @@ -68,6 +65,8 @@ self.__astWidget.setSelectionMode(QAbstractItemView.SingleSelection) self.__astWidget.setAlternatingRowColors(True) + self.__astWidget.itemClicked.connect(self.__astItemClicked) + self.__vm.astViewerStateChanged.connect(self.__astViewerStateChanged) self.hide() @@ -80,6 +79,8 @@ @type Editor """ if editor is not self.__editor: + if self.__editor: + self.__editor.clearAllHighlights() self.__editor = editor if self.__editor: self.__loadAST() @@ -94,6 +95,26 @@ if editor and editor is self.__editor: self.__loadAST() + def __editorDoubleClicked(self, editor, pos, buttons): + """ + Private slot to handle a mouse button double click in the editor. + + @param editor reference to the editor, that emitted the signal + @type Editor + @param pos position of the double click + @type QPoint + @param buttons mouse buttons that were double clicked + @type Qt.MouseButtons + """ + if editor is self.__editor and buttons == Qt.LeftButton: + if editor.isModified(): + # reload the source + QTimer.singleShot(0, self.__loadAST) + else: + # highlight the corresponding entry + QTimer.singleShot(0, self.__selectItemForEditorSelection) + QTimer.singleShot(0, self.__grabFocus) + def __lastEditorClosed(self): """ Private slot to handle the last editor closed signal of the view @@ -110,6 +131,7 @@ if not self.__vmConnected: self.__vm.editorChangedEd.connect(self.__editorChanged) self.__vm.editorSavedEd.connect(self.__editorSaved) + self.__vm.editorDoubleClickedEd.connect(self.__editorDoubleClicked) self.__vmConnected = True def hide(self): @@ -118,9 +140,14 @@ """ super(PythonAstViewer, self).hide() + if self.__editor: + self.__editor.clearAllHighlights() + if self.__vmConnected: self.__vm.editorChangedEd.disconnect(self.__editorChanged) self.__vm.editorSavedEd.disconnect(self.__editorSaved) + self.__vm.editorDoubleClickedEd.disconnect( + self.__editorDoubleClicked) self.__vmConnected = False def shutdown(self): @@ -143,8 +170,8 @@ self.show() self.__loadAST() else: + self.hide() self.__editor = None - self.hide() def __createErrorItem(self, error): """ @@ -169,6 +196,7 @@ return self.__astWidget.clear() + self.__editor.clearAllHighlights() if not self.__editor.isPyFile(): self.__createErrorItem(self.tr( @@ -202,6 +230,8 @@ self.setUpdatesEnabled(True) QApplication.restoreOverrideCursor() + + self.__grabFocus() def __populateNode(self, name, nodeOrFields, parent): """ @@ -293,7 +323,7 @@ @return best matching node @rtype ast.AST """ - if textRange == (-1, -1, -1, -1): + if textRange in [(-1, -1, -1, -1), (0, -1, 0, -1)]: # no valid range, i.e. no selection return None @@ -324,10 +354,17 @@ @return best matching tree item @rtype QTreeWidgetItem """ - if textRange == (-1, -1, -1, -1): + if textRange in [(-1, -1, -1, -1), (0, -1, 0, -1)]: # no valid range, i.e. no selection return None + lineno = itm.data(0, self.StartLineRole) + if lineno is not None and not self.__rangeContainsSmallerOrEqual( + (itm.data(0, self.StartLineRole), itm.data(0, self.StartIndexRole), + itm.data(0, self.EndLineRole), itm.data(0, self.EndIndexRole)), + textRange): + return None + # first look among children for index in range(itm.childCount()): child = itm.child(index) @@ -337,7 +374,7 @@ # no suitable child was found lineno = itm.data(0, self.StartLineRole) - if lineno is not None and self.__rangeContainsSmaller( + if lineno is not None and self.__rangeContainsSmallerOrEqual( (itm.data(0, self.StartLineRole), itm.data(0, self.StartIndexRole), itm.data(0, self.EndLineRole), itm.data(0, self.EndIndexRole)), textRange): @@ -391,6 +428,19 @@ (firstStart < secondStart and firstEnd == secondEnd) ) + def __rangeContainsSmallerOrEqual(self, first, second): + """ + Private method to check, if second is contained in or equal to first. + + @param first text range to check against + @type tuple of (int, int, int, int) + @param second text range to check for + @type tuple of (int, int, int, int) + @return flag indicating second is contained in or equal to first + @rtype bool + """ + return first == second or self.__rangeContainsSmaller(first, second) + def __clearSelection(self): """ Private method to clear all selected items. @@ -418,3 +468,33 @@ self.__astWidget.scrollToItem( itm, QAbstractItemView.PositionAtCenter) itm.setSelected(True) + + def __grabFocus(self): + """ + Private method to grab the input focus. + """ + self.__astWidget.setFocus(Qt.OtherFocusReason) + + @pyqtSlot(QTreeWidgetItem, int) + def __astItemClicked(self, itm, column): + """ + Private slot handling a user click on an AST node item. + + @param itm reference to the clicked item + @type QTreeWidgetItem + @param column column number of the click + @type int + """ + self.__editor.clearAllHighlights() + + if itm is not None: + startLine = itm.data(0, self.StartLineRole) + if startLine is not None: + startIndex = itm.data(0, self.StartIndexRole) + endLine = itm.data(0, self.EndLineRole) + endIndex = itm.data(0, self.EndIndexRole) + + self.__editor.gotoLine(startLine, firstVisible=True, + expand=True) + self.__editor.setHighlight(startLine - 1, startIndex, + endLine - 1, endIndex)