UI/PythonAstViewer.py

changeset 6932
8a3df4c6ac9a
parent 6931
faac36ec9d76
--- 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)

eric ide

mercurial