eric6/UI/PythonDisViewer.py

changeset 7705
90a9aefd4253
parent 7704
9251c4dc4f7a
child 7707
6abcf4275d0e
--- a/eric6/UI/PythonDisViewer.py	Sat Sep 19 19:04:21 2020 +0200
+++ b/eric6/UI/PythonDisViewer.py	Sun Sep 20 18:32:28 2020 +0200
@@ -15,9 +15,11 @@
 from PyQt5.QtGui import QCursor, QBrush
 from PyQt5.QtWidgets import (
     QTreeWidget, QApplication, QTreeWidgetItem, QAbstractItemView, QWidget,
-    QVBoxLayout, QLabel
+    QVBoxLayout, QLabel, QMenu
 )
 
+import Preferences
+
 
 class PythonDisViewer(QWidget):
     """
@@ -44,11 +46,12 @@
         self.__layout.addWidget(self.__disWidget)
         self.__layout.setContentsMargins(0, 0, 0, 0)
         
-        self.__infoLabel = QLabel(self.tr(
-            "italic: current instruction\n"
-            "bold: labelled instruction"
-        ))
-        self.__layout.addWidget(self.__infoLabel)
+        self.__currentInfoLabel = QLabel(self.tr(
+            "italic: current instruction"))
+        self.__labeledInfoLabel = QLabel(self.tr(
+            "bold: labeled instruction"))
+        self.__layout.addWidget(self.__currentInfoLabel)
+        self.__layout.addWidget(self.__labeledInfoLabel)
         
         self.__vm = viewmanager
         self.__vmConnected = False
@@ -64,12 +67,39 @@
         self.__disWidget.setSelectionMode(QAbstractItemView.SingleSelection)
         self.__disWidget.setAlternatingRowColors(True)
         
+        self.__menu = QMenu(self.__disWidget)
+        self.__menu.addAction(self.tr('Expand All'), self.__expandAll)
+        self.__menu.addAction(self.tr('Collapse All'), self.__collapseAll)
+        
+        self.__disWidget.setContextMenuPolicy(Qt.CustomContextMenu)
+        self.__disWidget.customContextMenuRequested.connect(
+            self.__contextMenuRequested)
+        
+        self.__errorColor = QBrush(
+            Preferences.getPython("DisViewerErrorColor"))
+        self.__currentInstructionColor = QBrush(
+            Preferences.getPython("DisViewerCurrentColor"))
+        self.__jumpTargetColor = QBrush(
+            Preferences.getPython("DisViewerLabeledColor"))
+        
         self.__disWidget.itemClicked.connect(self.__disItemClicked)
+        self.__disWidget.itemCollapsed.connect(self.__resizeColumns)
+        self.__disWidget.itemExpanded.connect(self.__resizeColumns)
         
         self.__vm.disViewerStateChanged.connect(self.__disViewerStateChanged)
         
         self.hide()
     
+    def __contextMenuRequested(self, coord):
+        """
+        Private slot to show the context menu.
+        
+        @param coord position of the mouse pointer
+        @type QPoint
+        """
+        coord = self.__disWidget.mapToGlobal(coord)
+        self.__menu.popup(coord)
+    
     def __editorChanged(self, editor):
         """
         Private slot to handle a change of the current editor.
@@ -95,25 +125,22 @@
         if editor and editor is self.__editor:
             self.__loadDIS()
     
-    def __editorDoubleClicked(self, editor, pos, buttons):
+    def __editorLineChanged(self, editor, lineno):
         """
         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
+        @param lineno line number of the editor's cursor (zero based)
+        @type int
         """
-        if editor is self.__editor and buttons == Qt.LeftButton:
+        if editor is self.__editor:
             if editor.isModified():
                 # reload the source
                 QTimer.singleShot(0, self.__loadDIS)
             
             # highlight the corresponding entry
-##            QTimer.singleShot(0, self.__selectItemForEditorSelection)
-            QTimer.singleShot(0, self.__grabFocus)
+            QTimer.singleShot(0, self.__selectItemForEditorLine)
     
     def __lastEditorClosed(self):
         """
@@ -131,7 +158,7 @@
         if not self.__vmConnected:
             self.__vm.editorChangedEd.connect(self.__editorChanged)
             self.__vm.editorSavedEd.connect(self.__editorSaved)
-            self.__vm.editorDoubleClickedEd.connect(self.__editorDoubleClicked)
+            self.__vm.editorLineChangedEd.connect(self.__editorLineChanged)
             self.__vmConnected = True
     
     def hide(self):
@@ -146,8 +173,7 @@
         if self.__vmConnected:
             self.__vm.editorChangedEd.disconnect(self.__editorChanged)
             self.__vm.editorSavedEd.disconnect(self.__editorSaved)
-            self.__vm.editorDoubleClickedEd.disconnect(
-                self.__editorDoubleClicked)
+            self.__vm.editorLineChangedEd.disconnect(self.__editorLineChanged)
             self.__vmConnected = False
     
     def shutdown(self):
@@ -173,6 +199,24 @@
             self.hide()
             self.__editor = None
     
+    def __expandAll(self):
+        """
+        Private slot to expand all items.
+        """
+        block = self.__disWidget.blockSignals(True)
+        self.__disWidget.expandAll()
+        self.__disWidget.blockSignals(block)
+        self.__resizeColumns()
+    
+    def __collapseAll(self):
+        """
+        Private slot to collapse all items.
+        """
+        block = self.__disWidget.blockSignals(True)
+        self.__disWidget.collapseAll()
+        self.__disWidget.blockSignals(block)
+        self.__resizeColumns()
+    
     def __createErrorItem(self, error):
         """
         Private method to create a top level error item.
@@ -184,10 +228,10 @@
         """
         itm = QTreeWidgetItem(self.__disWidget, [error])
         itm.setFirstColumnSpanned(True)
-        itm.setForeground(0, QBrush(Qt.red))
+        itm.setForeground(0, self.__errorColor)
         return itm
     
-    def __createTitleItem(self, title, line):
+    def __createTitleItem(self, title, line, parentItem):
         """
         Private method to create a title item.
         
@@ -195,10 +239,12 @@
         @type str
         @param line start line of the titled disassembly
         @type int
+        @param parentItem reference to the parent item
+        @type QTreeWidget or QTreeWidgetItem
         @return generated item
         @rtype QTreeWidgetItem
         """
-        itm = QTreeWidgetItem(self.__disWidget, [title])
+        itm = QTreeWidgetItem(parentItem, [title])
         itm.setFirstColumnSpanned(True)
         itm.setExpanded(True)
         
@@ -241,6 +287,7 @@
         itm = QTreeWidgetItem(parent, fields)
         for col in (0, 1, 3):
             itm.setTextAlignment(col, Qt.AlignRight)
+        # set font to indicate current instruction and jump target
         font = itm.font(0)
         if instr.offset == lasti:
             font.setItalic(True)
@@ -248,6 +295,16 @@
             font.setBold(True)
         for col in range(itm.columnCount()):
             itm.setFont(col, font)
+        # set color to indicate current instruction or jump target
+        if instr.offset == lasti:
+            foreground = self.__currentInstructionColor
+        elif instr.is_jump_target:
+            foreground = self.__jumpTargetColor
+        else:
+            foreground = None
+        if foreground:
+            for col in range(itm.columnCount()):
+                itm.setForeground(col, foreground)
         
         itm.setExpanded(True)
         
@@ -300,7 +357,9 @@
             return
         
         filename = self.__editor.getFileName()
-        if not filename:
+        if filename:
+            filename = os.path.basename(filename)
+        else:
             filename = "<dis>"
         
         QApplication.setOverrideCursor(QCursor(Qt.WaitCursor))
@@ -312,10 +371,12 @@
         
         if codeObject:
             self.setUpdatesEnabled(False)
+            block = self.__disWidget.blockSignals(True)
             
             self.__disassembleObject(codeObject, self.__disWidget, filename)
             QTimer.singleShot(0, self.__resizeColumns)
             
+            self.__disWidget.blockSignals(block)
             self.setUpdatesEnabled(True)
         
         QApplication.restoreOverrideCursor()
@@ -339,6 +400,58 @@
         # just adjust the sizes of the columns
         self.__resizeColumns()
     
+    def __clearSelection(self):
+        """
+        Private method to clear all selected items.
+        """
+        for itm in self.__disWidget.selectedItems():
+            itm.setSelected(False)
+    
+    def __selectChildren(self, itm, lineno):
+        """
+        Private method to select children of the given item covering the given
+        line number.
+        
+        @param itm reference to the item
+        @type QTreeWidgetItem
+        @param lineno line number to base the selection on
+        @type int
+        """
+        for index in range(itm.childCount()):
+            child = itm.child(index)
+            if (
+                child.data(0, self.StartLineRole) <= lineno <=
+                child.data(0, self.EndLineRole)
+            ):
+                child.setSelected(True)
+                self.__selectChildren(child, lineno)
+            
+            if child.data(0, self.StartLineRole) == lineno:
+                self.__disWidget.scrollToItem(
+                    child, QAbstractItemView.PositionAtCenter)
+    
+    def __selectItemForEditorLine(self):
+        """
+        Private slot to select the items corresponding with the cursor line
+        of the current editor.
+        """
+        # step 1: clear all selected items
+        self.__clearSelection()
+        
+        # step 2: retrieve the editor cursor line
+        cline, cindex = self.__editor.getCursorPosition()
+        # make the line numbers 1-based
+        cline += 1
+        
+        for index in range(self.__disWidget.topLevelItemCount()):
+            itm = self.__disWidget.topLevelItem(index)
+            if (
+                itm.data(0, self.StartLineRole) <= cline <=
+                itm.data(0, self.EndLineRole)
+            ):
+                itm.setSelected(True)
+                self.__selectChildren(itm, cline)
+    
     def __grabFocus(self):
         """
         Private method to grab the input focus.
@@ -383,7 +496,7 @@
             c = compile(source, name, 'exec')
         return c
     
-    def __disassembleObject(self, co, parentItem, name="", lasti=-1):
+    def __disassembleObject(self, co, parentItem, parentName="", lasti=-1):
         """
         Private method to disassemble the given code object recursively.
         
@@ -391,22 +504,22 @@
         @type code object
         @param parentItem reference to the parent item
         @type QTreeWidget or QTreeWidgetItem
-        @param name name of the code object
+        @param parentName name of the parent code object
         @type str
         @param lasti index of the instruction of a traceback
         @type int
         """
         if co.co_name == "<module>":
-            title = (
-                self.tr("Disassembly of module '{0}'")
-                .format(os.path.basename(co.co_filename))
-            )
+            title = os.path.basename(co.co_filename)
+            name = ""
         else:
-            title = (
-                self.tr("Disassembly of code object '{0}'")
-                .format(co.co_name)
-            )
-        titleItem = self.__createTitleItem(title, co.co_firstlineno)
+            if parentName:
+                name = "{0}.{1}".format(parentName, co.co_name)
+            else:
+                name = co.co_name
+            title = self.tr("Code Object '{0}'").format(name)
+        titleItem = self.__createTitleItem(title, co.co_firstlineno,
+                                           parentItem)
         lastStartItem = None
         for instr in dis.get_instructions(co):
             if instr.starts_line:
@@ -419,8 +532,24 @@
         if lastStartItem:
             self.__updateItemEndLine(lastStartItem)
         
-        self.__updateItemEndLine(titleItem)
-        
         for x in co.co_consts:
             if hasattr(x, 'co_code'):
-                self.__disassembleObject(x, self.__disWidget, lasti=lasti)
+                self.__disassembleObject(x, titleItem, parentName=name,
+                                         lasti=lasti)
+        
+        self.__updateItemEndLine(titleItem)
+    
+    @pyqtSlot()
+    def preferencesChanged(self):
+        """
+        Public slot handling changes of the Disassembly viewer settings.
+        """
+        self.__errorColor = QBrush(
+            Preferences.getPython("DisViewerErrorColor"))
+        self.__currentInstructionColor = QBrush(
+            Preferences.getPython("DisViewerCurrentColor"))
+        self.__jumpTargetColor = QBrush(
+            Preferences.getPython("DisViewerLabeledColor"))
+        
+        if self.isVisible():
+            self.__loadDIS()

eric ide

mercurial