eric6/UI/PythonDisViewer.py

changeset 7707
6abcf4275d0e
parent 7705
90a9aefd4253
child 7710
9aad21c7765d
--- a/eric6/UI/PythonDisViewer.py	Sun Sep 20 18:32:43 2020 +0200
+++ b/eric6/UI/PythonDisViewer.py	Mon Sep 21 19:03:35 2020 +0200
@@ -11,6 +11,9 @@
 import os
 import dis
 
+import enum
+
+
 from PyQt5.QtCore import pyqtSlot, Qt, QTimer
 from PyQt5.QtGui import QCursor, QBrush
 from PyQt5.QtWidgets import (
@@ -21,6 +24,14 @@
 import Preferences
 
 
+class PythonDisViewerModes(enum.Enum):
+    """
+    Class implementing the disassembly viewer operation modes.
+    """
+    SourceDisassemblyMode = 0
+    TracebackMode = 1
+
+
 class PythonDisViewer(QWidget):
     """
     Class implementing a widget to visualize the Python Disassembly for some
@@ -29,12 +40,16 @@
     StartLineRole = Qt.UserRole
     EndLineRole = Qt.UserRole + 1
     
-    def __init__(self, viewmanager, parent=None):
+    def __init__(self, viewmanager,
+                 mode=PythonDisViewerModes.SourceDisassemblyMode,
+                 parent=None):
         """
         Constructor
         
         @param viewmanager reference to the viewmanager object
         @type ViewManager
+        @param mode operation mode of the viewer
+        @type int
         @param parent reference to the parent widget
         @type QWidget
         """
@@ -53,9 +68,13 @@
         self.__layout.addWidget(self.__currentInfoLabel)
         self.__layout.addWidget(self.__labeledInfoLabel)
         
+        self.setWindowTitle(self.tr("Disassembly"))
+        
         self.__vm = viewmanager
         self.__vmConnected = False
         
+        self.__mode = mode
+        
         self.__editor = None
         self.__source = ""
         
@@ -86,9 +105,13 @@
         self.__disWidget.itemCollapsed.connect(self.__resizeColumns)
         self.__disWidget.itemExpanded.connect(self.__resizeColumns)
         
-        self.__vm.disViewerStateChanged.connect(self.__disViewerStateChanged)
+        if self.__mode == PythonDisViewerModes.SourceDisassemblyMode:
+            self.__vm.disViewerStateChanged.connect(
+                self.__disViewerStateChanged)
         
-        self.hide()
+            self.hide()
+        elif self.__mode == PythonDisViewerModes.TracebackMode:
+            self.__styleLabels()
     
     def __contextMenuRequested(self, coord):
         """
@@ -142,6 +165,16 @@
             # highlight the corresponding entry
             QTimer.singleShot(0, self.__selectItemForEditorLine)
     
+    def __editorLanguageChanged(self, editor):
+        """
+        Private slot to handle a change of the editor language.
+        
+        @param editor reference to the editor which changed language
+        @type Editor
+        """
+        if editor is self.__editor:
+            QTimer.singleShot(0, self.__loadDIS)
+    
     def __lastEditorClosed(self):
         """
         Private slot to handle the last editor closed signal of the view
@@ -155,11 +188,18 @@
         """
         super(PythonDisViewer, self).show()
         
-        if not self.__vmConnected:
+        if (
+            self.__mode == PythonDisViewerModes.SourceDisassemblyMode and
+            not self.__vmConnected
+        ):
             self.__vm.editorChangedEd.connect(self.__editorChanged)
             self.__vm.editorSavedEd.connect(self.__editorSaved)
             self.__vm.editorLineChangedEd.connect(self.__editorLineChanged)
+            self.__vm.editorLanguageChanged.connect(
+                self.__editorLanguageChanged)
             self.__vmConnected = True
+        
+        self.__styleLabels()
     
     def hide(self):
         """
@@ -170,10 +210,15 @@
         if self.__editor:
             self.__editor.clearAllHighlights()
         
-        if self.__vmConnected:
+        if (
+            self.__mode == PythonDisViewerModes.SourceDisassemblyMode and
+            self.__vmConnected
+        ):
             self.__vm.editorChangedEd.disconnect(self.__editorChanged)
             self.__vm.editorSavedEd.disconnect(self.__editorSaved)
             self.__vm.editorLineChangedEd.disconnect(self.__editorLineChanged)
+            self.__vm.editorLanguageChanged.disconnect(
+                self.__editorLanguageChanged)
             self.__vmConnected = False
     
     def shutdown(self):
@@ -189,15 +234,16 @@
         @param on flag indicating to show the Disassembly
         @type bool
         """
-        editor = self.__vm.activeWindow()
-        if on and editor and editor.isPyFile():
-            if editor is not self.__editor:
-                self.__editor = editor
-            self.show()
-            self.__loadDIS()
-        else:
-            self.hide()
-            self.__editor = None
+        if self.__mode == PythonDisViewerModes.SourceDisassemblyMode:
+            editor = self.__vm.activeWindow()
+            if on:
+                if editor is not self.__editor:
+                    self.__editor = editor
+                self.show()
+                self.__loadDIS()
+            else:
+                self.hide()
+                self.__editor = None
     
     def __expandAll(self):
         """
@@ -339,21 +385,31 @@
         Private method to generate the Disassembly from the source of the
         current editor and visualize it.
         """
+        if self.__mode != PythonDisViewerModes.SourceDisassemblyMode:
+            # wrong mode, just return
+            return
+        
         if not self.__editor:
+            self.__createErrorItem(self.tr(
+                "No editor has been opened."
+            ))
             return
         
         self.__disWidget.clear()
         self.__editor.clearAllHighlights()
         
-        if not self.__editor.isPyFile():
+        source = self.__editor.text()
+        if not source.strip():
+            # empty editor or white space only
             self.__createErrorItem(self.tr(
-                "The current editor text does not contain Python source."
+                "The current editor does not contain any source code."
             ))
             return
         
-        source = self.__editor.text()
-        if not source.strip():
-            # empty editor or white space only
+        if not self.__editor.isPyFile():
+            self.__createErrorItem(self.tr(
+                "The current editor does not contain Python source code."
+            ))
             return
         
         filename = self.__editor.getFileName()
@@ -380,8 +436,69 @@
             self.setUpdatesEnabled(True)
         
         QApplication.restoreOverrideCursor()
+    
+    @pyqtSlot(dict)
+    def showDisassembly(self, disassembly):
+        """
+        Public slot to receive a code disassembly from the debug client.
         
-        self.__grabFocus()
+        @param disassembly dictionary containing the disassembly information
+        @type dict
+        """
+        if self.__mode == PythonDisViewerModes.TracebackMode:
+            if (
+                disassembly and
+                "instructions" in disassembly and
+                disassembly["instructions"]
+            ):
+                self.__disWidget.clear()
+                
+                self.setUpdatesEnabled(False)
+                block = self.__disWidget.blockSignals(True)
+                
+                titleItem = self.__createTitleItem(
+                    self.tr("Disassembly of last traceback"),
+                    disassembly["firstlineno"],
+                    self.__disWidget
+                )
+                
+                lasti = disassembly["lasti"]
+                lastStartItem = None
+                for instrDict in disassembly["instructions"]:
+                    instr = dis.Instruction(
+                        instrDict["opname"],
+                        0,                              # dummy value
+                        instrDict["arg"],
+                        "",                             # dummy value
+                        instrDict["argrepr"],
+                        instrDict["offset"],
+                        instrDict["lineno"],
+                        instrDict["isJumpTarget"],
+                    )
+                    if instrDict["lineno"] > 0:
+                        if lastStartItem:
+                            self.__updateItemEndLine(lastStartItem)
+                        lastStartItem = self.__createInstructionItem(
+                            instr, titleItem, lasti=lasti)
+                    else:
+                        self.__createInstructionItem(
+                            instr, lastStartItem, lasti=lasti)
+                if lastStartItem:
+                    self.__updateItemEndLine(lastStartItem)
+                
+                QTimer.singleShot(0, self.__resizeColumns)
+                
+                self.__disWidget.blockSignals(block)
+                self.setUpdatesEnabled(True)
+                
+                if lasti:
+                    lastInstructions = self.__disWidget.findItems(
+                        "{0:d}".format(lasti),
+                        Qt.MatchFixedString | Qt.MatchRecursive, 1)
+                    if lastInstructions:
+                        self.__disWidget.scrollToItem(
+                            lastInstructions[0],
+                            QAbstractItemView.PositionAtCenter)
     
     def __resizeColumns(self):
         """
@@ -452,12 +569,6 @@
                 itm.setSelected(True)
                 self.__selectChildren(itm, cline)
     
-    def __grabFocus(self):
-        """
-        Private method to grab the input focus.
-        """
-        self.__disWidget.setFocus(Qt.OtherFocusReason)
-    
     @pyqtSlot(QTreeWidgetItem, int)
     def __disItemClicked(self, itm, column):
         """
@@ -468,6 +579,7 @@
         @param column column number of the click
         @type int
         """
+        # TODO: add code to deal with Traceback mode
         self.__editor.clearAllHighlights()
         
         if itm is not None:
@@ -553,3 +665,36 @@
         
         if self.isVisible():
             self.__loadDIS()
+        
+        self.__styleLabels()
+    
+    def __styleLabels(self):
+        """
+        Private method to style the info labels iaw. selected colors.
+        """
+        # current instruction
+        self.__currentInfoLabel.setStyleSheet(
+            "QLabel {{ color : {0}; }}".format(
+                self.__currentInstructionColor.color().name()
+            )
+        )
+        font = self.__currentInfoLabel.font()
+        font.setItalic(True)
+        self.__currentInfoLabel.setFont(font)
+        
+        # labeled instruction
+        self.__labeledInfoLabel.setStyleSheet(
+            "QLabel {{ color : {0}; }}".format(
+                self.__jumpTargetColor.color().name()
+            )
+        )
+        font = self.__labeledInfoLabel.font()
+        font.setBold(True)
+        self.__labeledInfoLabel.setFont(font)
+    
+    @pyqtSlot()
+    def clear(self):
+        """
+        Public method to clear the display.
+        """
+        self.__disWidget.clear()

eric ide

mercurial