eric7/Debugger/DebugViewer.py

branch
eric7
changeset 8312
800c432b34c8
parent 8265
0090cfa83159
child 8318
962bce857696
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric7/Debugger/DebugViewer.py	Sat May 15 18:45:04 2021 +0200
@@ -0,0 +1,947 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2002 - 2021 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing a widget containing various debug related views.
+
+The views avaliable are:
+<ul>
+  <li>selector showing all connected debugger backends with associated
+      threads</li>
+  <li>variables viewer for global variables for the selected debug client</li>
+  <li>variables viewer for local variables for the selected debug client</li>
+  <li>call stack viewer for the selected debug client</li>
+  <li>call trace viewer</li>
+  <li>viewer for breakpoints</li>
+  <li>viewer for watch expressions</li>
+  <li>viewer for exceptions</li>
+  <li>viewer for a code disassembly for an exception<li>
+</ul>
+"""
+
+import os
+
+from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QCoreApplication
+from PyQt5.QtWidgets import (
+    QWidget, QVBoxLayout, QHBoxLayout, QLineEdit, QSizePolicy, QPushButton,
+    QComboBox, QLabel, QTreeWidget, QTreeWidgetItem, QHeaderView, QSplitter
+)
+
+import UI.PixmapCache
+import Preferences
+
+from E5Gui.E5TabWidget import E5TabWidget
+
+
+class DebugViewer(QWidget):
+    """
+    Class implementing a widget containing various debug related views.
+    
+    The individual tabs contain the interpreter shell (optional),
+    the filesystem browser (optional), the two variables viewers
+    (global and local), a breakpoint viewer, a watch expression viewer and
+    the exception logger. Additionally a list of all threads is shown.
+    
+    @signal sourceFile(string, int) emitted to open a source file at a line
+    @signal preferencesChanged() emitted to react on changed preferences
+    """
+    sourceFile = pyqtSignal(str, int)
+    preferencesChanged = pyqtSignal()
+    
+    ThreadIdRole = Qt.ItemDataRole.UserRole + 1
+    DebuggerStateRole = Qt.ItemDataRole.UserRole + 2
+    
+    # Map debug state to icon name
+    StateIcon = {
+        "broken": "break",
+        "exception": "exceptions",
+        "running": "mediaPlaybackStart",
+        "syntax": "syntaxError22",
+    }
+    
+    # Map debug state to user message
+    StateMessage = {
+        "broken": QCoreApplication.translate(
+            "DebugViewer", "waiting at breakpoint"),
+        "exception": QCoreApplication.translate(
+            "DebugViewer", "waiting at exception"),
+        "running": QCoreApplication.translate(
+            "DebugViewer", "running"),
+        "syntax": QCoreApplication.translate(
+            "DebugViewer", "syntax error"),
+    }
+    
+    def __init__(self, debugServer, parent=None):
+        """
+        Constructor
+        
+        @param debugServer reference to the debug server object
+        @type DebugServer
+        @param parent parent widget
+        @type QWidget
+        """
+        super().__init__(parent)
+        
+        self.debugServer = debugServer
+        self.debugUI = None
+        
+        self.setWindowIcon(UI.PixmapCache.getIcon("eric"))
+        
+        self.__mainLayout = QVBoxLayout()
+        self.__mainLayout.setContentsMargins(0, 3, 0, 0)
+        self.setLayout(self.__mainLayout)
+        
+        self.__mainSplitter = QSplitter(Qt.Orientation.Vertical, self)
+        self.__mainLayout.addWidget(self.__mainSplitter)
+        
+        # add the viewer showing the connected debug backends
+        self.__debuggersWidget = QWidget()
+        self.__debuggersLayout = QVBoxLayout(self.__debuggersWidget)
+        self.__debuggersLayout.setContentsMargins(0, 0, 0, 0)
+        self.__debuggersLayout.addWidget(
+            QLabel(self.tr("Debuggers and Threads:")))
+        self.__debuggersList = QTreeWidget()
+        self.__debuggersList.setHeaderLabels(
+            [self.tr("ID"), self.tr("State"), ""])
+        self.__debuggersList.header().setStretchLastSection(True)
+        self.__debuggersList.setSortingEnabled(True)
+        self.__debuggersList.setRootIsDecorated(True)
+        self.__debuggersList.setAlternatingRowColors(True)
+        self.__debuggersLayout.addWidget(self.__debuggersList)
+        self.__mainSplitter.addWidget(self.__debuggersWidget)
+        
+        self.__debuggersList.currentItemChanged.connect(
+            self.__debuggerSelected)
+        
+        # add the tab widget containing various debug related views
+        self.__tabWidget = E5TabWidget()
+        self.__mainSplitter.addWidget(self.__tabWidget)
+        
+        from .VariablesViewer import VariablesViewer
+        # add the global variables viewer
+        self.glvWidget = QWidget()
+        self.glvWidgetVLayout = QVBoxLayout(self.glvWidget)
+        self.glvWidgetVLayout.setContentsMargins(0, 0, 0, 0)
+        self.glvWidgetVLayout.setSpacing(3)
+        self.glvWidget.setLayout(self.glvWidgetVLayout)
+        
+        self.globalsViewer = VariablesViewer(self, True, self.glvWidget)
+        self.glvWidgetVLayout.addWidget(self.globalsViewer)
+        
+        self.glvWidgetHLayout = QHBoxLayout()
+        self.glvWidgetHLayout.setContentsMargins(3, 3, 3, 3)
+        
+        self.globalsFilterEdit = QLineEdit(self.glvWidget)
+        self.globalsFilterEdit.setSizePolicy(
+            QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
+        self.glvWidgetHLayout.addWidget(self.globalsFilterEdit)
+        self.globalsFilterEdit.setToolTip(
+            self.tr("Enter regular expression patterns separated by ';'"
+                    " to define variable filters. "))
+        self.globalsFilterEdit.setWhatsThis(
+            self.tr("Enter regular expression patterns separated by ';'"
+                    " to define variable filters. All variables and"
+                    " class attributes matched by one of the expressions"
+                    " are not shown in the list above."))
+        
+        self.setGlobalsFilterButton = QPushButton(
+            self.tr('Set'), self.glvWidget)
+        self.glvWidgetHLayout.addWidget(self.setGlobalsFilterButton)
+        self.glvWidgetVLayout.addLayout(self.glvWidgetHLayout)
+        
+        index = self.__tabWidget.addTab(
+            self.glvWidget,
+            UI.PixmapCache.getIcon("globalVariables"), '')
+        self.__tabWidget.setTabToolTip(
+            index,
+            self.tr("Shows the list of global variables and their values."))
+        
+        self.setGlobalsFilterButton.clicked.connect(
+            self.setGlobalsFilter)
+        self.globalsFilterEdit.returnPressed.connect(self.setGlobalsFilter)
+        
+        # add the local variables viewer
+        self.lvWidget = QWidget()
+        self.lvWidgetVLayout = QVBoxLayout(self.lvWidget)
+        self.lvWidgetVLayout.setContentsMargins(0, 0, 0, 0)
+        self.lvWidgetVLayout.setSpacing(3)
+        self.lvWidget.setLayout(self.lvWidgetVLayout)
+        
+        self.lvWidgetHLayout1 = QHBoxLayout()
+        self.lvWidgetHLayout1.setContentsMargins(3, 3, 3, 3)
+        
+        self.stackComboBox = QComboBox(self.lvWidget)
+        self.stackComboBox.setSizePolicy(
+            QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
+        self.lvWidgetHLayout1.addWidget(self.stackComboBox)
+
+        self.sourceButton = QPushButton(self.tr('Source'), self.lvWidget)
+        self.lvWidgetHLayout1.addWidget(self.sourceButton)
+        self.sourceButton.setEnabled(False)
+        self.lvWidgetVLayout.addLayout(self.lvWidgetHLayout1)
+
+        self.localsViewer = VariablesViewer(self, False, self.lvWidget)
+        self.lvWidgetVLayout.addWidget(self.localsViewer)
+        
+        self.lvWidgetHLayout2 = QHBoxLayout()
+        self.lvWidgetHLayout2.setContentsMargins(3, 3, 3, 3)
+        
+        self.localsFilterEdit = QLineEdit(self.lvWidget)
+        self.localsFilterEdit.setSizePolicy(
+            QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
+        self.lvWidgetHLayout2.addWidget(self.localsFilterEdit)
+        self.localsFilterEdit.setToolTip(
+            self.tr(
+                "Enter regular expression patterns separated by ';' to define "
+                "variable filters. "))
+        self.localsFilterEdit.setWhatsThis(
+            self.tr(
+                "Enter regular expression patterns separated by ';' to define "
+                "variable filters. All variables and class attributes matched"
+                " by one of the expressions are not shown in the list above."))
+        
+        self.setLocalsFilterButton = QPushButton(
+            self.tr('Set'), self.lvWidget)
+        self.lvWidgetHLayout2.addWidget(self.setLocalsFilterButton)
+        self.lvWidgetVLayout.addLayout(self.lvWidgetHLayout2)
+        
+        index = self.__tabWidget.addTab(
+            self.lvWidget,
+            UI.PixmapCache.getIcon("localVariables"), '')
+        self.__tabWidget.setTabToolTip(
+            index,
+            self.tr("Shows the list of local variables and their values."))
+        
+        self.sourceButton.clicked.connect(self.__showSource)
+        self.stackComboBox.currentIndexChanged[int].connect(
+            self.__frameSelected)
+        self.setLocalsFilterButton.clicked.connect(self.setLocalsFilter)
+        self.localsFilterEdit.returnPressed.connect(self.setLocalsFilter)
+        
+        self.preferencesChanged.connect(self.handlePreferencesChanged)
+        self.preferencesChanged.connect(self.globalsViewer.preferencesChanged)
+        self.preferencesChanged.connect(self.localsViewer.preferencesChanged)
+        
+        from .CallStackViewer import CallStackViewer
+        # add the call stack viewer
+        self.callStackViewer = CallStackViewer(self.debugServer)
+        index = self.__tabWidget.addTab(
+            self.callStackViewer,
+            UI.PixmapCache.getIcon("callStack"), "")
+        self.__tabWidget.setTabToolTip(
+            index,
+            self.tr("Shows the current call stack."))
+        self.callStackViewer.sourceFile.connect(self.sourceFile)
+        self.callStackViewer.frameSelected.connect(
+            self.__callStackFrameSelected)
+        
+        from .CallTraceViewer import CallTraceViewer
+        # add the call trace viewer
+        self.callTraceViewer = CallTraceViewer(self.debugServer, self)
+        index = self.__tabWidget.addTab(
+            self.callTraceViewer,
+            UI.PixmapCache.getIcon("callTrace"), "")
+        self.__tabWidget.setTabToolTip(
+            index,
+            self.tr("Shows a trace of the program flow."))
+        self.callTraceViewer.sourceFile.connect(self.sourceFile)
+        
+        from .BreakPointViewer import BreakPointViewer
+        # add the breakpoint viewer
+        self.breakpointViewer = BreakPointViewer()
+        self.breakpointViewer.setModel(self.debugServer.getBreakPointModel())
+        index = self.__tabWidget.addTab(
+            self.breakpointViewer,
+            UI.PixmapCache.getIcon("breakpoints"), '')
+        self.__tabWidget.setTabToolTip(
+            index,
+            self.tr("Shows a list of defined breakpoints."))
+        self.breakpointViewer.sourceFile.connect(self.sourceFile)
+        
+        from .WatchPointViewer import WatchPointViewer
+        # add the watch expression viewer
+        self.watchpointViewer = WatchPointViewer()
+        self.watchpointViewer.setModel(self.debugServer.getWatchPointModel())
+        index = self.__tabWidget.addTab(
+            self.watchpointViewer,
+            UI.PixmapCache.getIcon("watchpoints"), '')
+        self.__tabWidget.setTabToolTip(
+            index,
+            self.tr("Shows a list of defined watchpoints."))
+        
+        from .ExceptionLogger import ExceptionLogger
+        # add the exception logger
+        self.exceptionLogger = ExceptionLogger()
+        index = self.__tabWidget.addTab(
+            self.exceptionLogger,
+            UI.PixmapCache.getIcon("exceptions"), '')
+        self.__tabWidget.setTabToolTip(
+            index,
+            self.tr("Shows a list of raised exceptions."))
+        
+        from UI.PythonDisViewer import PythonDisViewer, PythonDisViewerModes
+        # add the Python disassembly viewer
+        self.disassemblyViewer = PythonDisViewer(
+            None, mode=PythonDisViewerModes.TRACEBACK)
+        index = self.__tabWidget.addTab(
+            self.disassemblyViewer,
+            UI.PixmapCache.getIcon("disassembly"), '')
+        self.__tabWidget.setTabToolTip(
+            index,
+            self.tr("Shows a code disassembly in case of an exception."))
+        
+        self.__tabWidget.setCurrentWidget(self.glvWidget)
+        
+        self.__doDebuggersListUpdate = True
+        
+        self.__mainSplitter.setSizes([100, 700])
+        
+        self.currentStack = None
+        self.framenr = 0
+        
+        self.__autoViewSource = Preferences.getDebugger("AutoViewSourceCode")
+        self.sourceButton.setVisible(not self.__autoViewSource)
+        
+        # connect some debug server signals
+        self.debugServer.clientStack.connect(
+            self.handleClientStack)
+        self.debugServer.clientThreadList.connect(
+            self.__addThreadList)
+        self.debugServer.clientDebuggerId.connect(
+            self.__clientDebuggerId)
+        self.debugServer.passiveDebugStarted.connect(
+            self.handleDebuggingStarted)
+        self.debugServer.clientLine.connect(
+            self.__clientLine)
+        self.debugServer.clientSyntaxError.connect(
+            self.__clientSyntaxError)
+        self.debugServer.clientException.connect(
+            self.__clientException)
+        self.debugServer.clientExit.connect(
+            self.__clientExit)
+        self.debugServer.clientDisconnected.connect(
+            self.__removeDebugger)
+        
+        self.debugServer.clientException.connect(
+            self.exceptionLogger.addException)
+        self.debugServer.passiveDebugStarted.connect(
+            self.exceptionLogger.debuggingStarted)
+        
+        self.debugServer.clientLine.connect(
+            self.breakpointViewer.highlightBreakpoint)
+    
+    def handlePreferencesChanged(self):
+        """
+        Public slot to handle the preferencesChanged signal.
+        """
+        self.__autoViewSource = Preferences.getDebugger("AutoViewSourceCode")
+        self.sourceButton.setVisible(not self.__autoViewSource)
+        
+    def setDebugger(self, debugUI):
+        """
+        Public method to set a reference to the Debug UI.
+        
+        @param debugUI reference to the DebugUI object
+        @type DebugUI
+        """
+        self.debugUI = debugUI
+        self.callStackViewer.setDebugger(debugUI)
+        
+        # connect some debugUI signals
+        self.debugUI.clientStack.connect(self.handleClientStack)
+        self.debugUI.debuggingStarted.connect(
+            self.exceptionLogger.debuggingStarted)
+        self.debugUI.debuggingStarted.connect(
+            self.handleDebuggingStarted)
+    
+    def handleResetUI(self, fullReset):
+        """
+        Public method to reset the viewer.
+        
+        @param fullReset flag indicating a full reset is required
+        @type bool
+        """
+        self.globalsViewer.handleResetUI()
+        self.localsViewer.handleResetUI()
+        self.setGlobalsFilter()
+        self.setLocalsFilter()
+        self.sourceButton.setEnabled(False)
+        self.currentStack = None
+        self.stackComboBox.clear()
+        self.__tabWidget.setCurrentWidget(self.glvWidget)
+        self.breakpointViewer.handleResetUI()
+        if fullReset:
+            self.__debuggersList.clear()
+        self.disassemblyViewer.clear()
+        
+    def initCallStackViewer(self, projectMode):
+        """
+        Public method to initialize the call stack viewer.
+        
+        @param projectMode flag indicating to enable the project mode
+        @type bool
+        """
+        self.callStackViewer.clear()
+        self.callStackViewer.setProjectMode(projectMode)
+        
+    def isCallTraceEnabled(self):
+        """
+        Public method to get the state of the call trace function.
+        
+        @return flag indicating the state of the call trace function
+        @rtype bool
+        """
+        return self.callTraceViewer.isCallTraceEnabled()
+        
+    def clearCallTrace(self):
+        """
+        Public method to clear the recorded call trace.
+        """
+        self.callTraceViewer.clear()
+        
+    def setCallTraceToProjectMode(self, enabled):
+        """
+        Public slot to set the call trace viewer to project mode.
+        
+        In project mode the call trace info is shown with project relative
+        path names.
+        
+        @param enabled flag indicating to enable the project mode
+        @type bool
+        """
+        self.callTraceViewer.setProjectMode(enabled)
+        
+    def showVariables(self, vlist, showGlobals):
+        """
+        Public method to show the variables in the respective window.
+        
+        @param vlist list of variables to display
+        @type list
+        @param showGlobals flag indicating global/local state
+        @type bool
+        """
+        if showGlobals:
+            self.globalsViewer.showVariables(vlist, self.framenr)
+        else:
+            self.localsViewer.showVariables(vlist, self.framenr)
+            
+    def showVariable(self, vlist, showGlobals):
+        """
+        Public method to show the variables in the respective window.
+        
+        @param vlist list of variables to display
+        @type list
+        @param showGlobals flag indicating global/local state
+        @type bool
+        """
+        if showGlobals:
+            self.globalsViewer.showVariable(vlist)
+        else:
+            self.localsViewer.showVariable(vlist)
+            
+    def showVariablesTab(self, showGlobals):
+        """
+        Public method to make a variables tab visible.
+        
+        @param showGlobals flag indicating global/local state
+        @type bool
+        """
+        if showGlobals:
+            self.__tabWidget.setCurrentWidget(self.glvWidget)
+        else:
+            self.__tabWidget.setCurrentWidget(self.lvWidget)
+        
+    def handleClientStack(self, stack, debuggerId):
+        """
+        Public slot to show the call stack of the program being debugged.
+        
+        @param stack list of tuples with call stack data (file name,
+            line number, function name, formatted argument/values list)
+        @type list of tuples of (str, str, str, str)
+        @param debuggerId ID of the debugger backend
+        @type str
+        """
+        if debuggerId == self.getSelectedDebuggerId():
+            block = self.stackComboBox.blockSignals(True)
+            self.framenr = 0
+            self.stackComboBox.clear()
+            self.currentStack = stack
+            self.sourceButton.setEnabled(len(stack) > 0)
+            for s in stack:
+                # just show base filename to make it readable
+                s = (os.path.basename(s[0]), s[1], s[2])
+                self.stackComboBox.addItem('{0}:{1}:{2}'.format(*s))
+            self.stackComboBox.blockSignals(block)
+    
+    def __clientLine(self, fn, line, debuggerId, threadName):
+        """
+        Private method to handle a change to the current line.
+        
+        @param fn filename
+        @type str
+        @param line linenumber
+        @type int
+        @param debuggerId ID of the debugger backend
+        @type str
+        @param threadName name of the thread signaling the event
+        @type str
+        """
+        self.__setDebuggerIconAndState(debuggerId, "broken")
+        self.__setThreadIconAndState(debuggerId, threadName, "broken")
+        if debuggerId != self.getSelectedDebuggerId():
+            self.__setCurrentDebugger(debuggerId)
+    
+    @pyqtSlot(str, int, str, bool, str)
+    def __clientExit(self, program, status, message, quiet, debuggerId):
+        """
+        Private method to handle the debugged program terminating.
+        
+        @param program name of the exited program
+        @type str
+        @param status exit code of the debugged program
+        @type int
+        @param message exit message of the debugged program
+        @type str
+        @param quiet flag indicating to suppress exit info display
+        @type bool
+        @param debuggerId ID of the debugger backend
+        @type str
+        """
+        if not self.isOnlyDebugger():
+            if debuggerId == self.getSelectedDebuggerId():
+                # the current client has exited
+                self.globalsViewer.handleResetUI()
+                self.localsViewer.handleResetUI()
+                self.setGlobalsFilter()
+                self.setLocalsFilter()
+                self.sourceButton.setEnabled(False)
+                self.currentStack = None
+                self.stackComboBox.clear()
+            
+            self.__removeDebugger(debuggerId)
+    
+    def __clientSyntaxError(self, message, filename, lineNo, characterNo,
+                            debuggerId, threadName):
+        """
+        Private method to handle a syntax error in the debugged program.
+        
+        @param message message of the syntax error
+        @type str
+        @param filename translated filename of the syntax error position
+        @type str
+        @param lineNo line number of the syntax error position
+        @type int
+        @param characterNo character number of the syntax error position
+        @type int
+        @param debuggerId ID of the debugger backend
+        @type str
+        @param threadName name of the thread signaling the event
+        @type str
+        """
+        self.__setDebuggerIconAndState(debuggerId, "syntax")
+        self.__setThreadIconAndState(debuggerId, threadName, "syntax")
+    
+    def __clientException(self, exceptionType, exceptionMessage, stackTrace,
+                          debuggerId, threadName):
+        """
+        Private method to handle an exception of the debugged program.
+        
+        @param exceptionType type of exception raised
+        @type str
+        @param exceptionMessage message given by the exception
+        @type (str
+        @param stackTrace list of stack entries
+        @type list of str
+        @param debuggerId ID of the debugger backend
+        @type str
+        @param threadName name of the thread signaling the event
+        @type str
+        """
+        self.__setDebuggerIconAndState(debuggerId, "exception")
+        self.__setThreadIconAndState(debuggerId, threadName, "exception")
+    
+    def setVariablesFilter(self, globalsFilter, localsFilter):
+        """
+        Public slot to set the local variables filter.
+        
+        @param globalsFilter filter list for global variable types
+        @type list of str
+        @param localsFilter filter list for local variable types
+        @type list of str
+        """
+        self.__globalsFilter = globalsFilter
+        self.__localsFilter = localsFilter
+        
+    def __showSource(self):
+        """
+        Private slot to handle the source button press to show the selected
+        file.
+        """
+        index = self.stackComboBox.currentIndex()
+        if index > -1 and self.currentStack:
+            s = self.currentStack[index]
+            self.sourceFile.emit(s[0], int(s[1]))
+        
+    def __frameSelected(self, frmnr):
+        """
+        Private slot to handle the selection of a new stack frame number.
+        
+        @param frmnr frame number (0 is the current frame)
+        @type int
+        """
+        if frmnr >= 0:
+            self.framenr = frmnr
+            if self.debugServer.isDebugging():
+                self.debugServer.remoteClientVariables(
+                    self.getSelectedDebuggerId(), 0, self.__localsFilter,
+                    frmnr)
+            
+            if self.__autoViewSource:
+                self.__showSource()
+        
+    def setGlobalsFilter(self):
+        """
+        Public slot to set the global variable filter.
+        """
+        if self.debugServer.isDebugging():
+            filterStr = self.globalsFilterEdit.text()
+            self.debugServer.remoteClientSetFilter(
+                self.getSelectedDebuggerId(), 1, filterStr)
+            self.debugServer.remoteClientVariables(
+                self.getSelectedDebuggerId(), 2, self.__globalsFilter)
+        
+    def setLocalsFilter(self):
+        """
+        Public slot to set the local variable filter.
+        """
+        if self.debugServer.isDebugging():
+            filterStr = self.localsFilterEdit.text()
+            self.debugServer.remoteClientSetFilter(
+                self.getSelectedDebuggerId(), 0, filterStr)
+            if self.currentStack:
+                self.debugServer.remoteClientVariables(
+                    self.getSelectedDebuggerId(), 0, self.__localsFilter,
+                    self.framenr)
+        
+    def handleDebuggingStarted(self):
+        """
+        Public slot to handle the start of a debugging session.
+        
+        This slot sets the variables filter expressions.
+        """
+        self.setGlobalsFilter()
+        self.setLocalsFilter()
+        self.showVariablesTab(False)
+        
+        self.disassemblyViewer.clear()
+        
+    def currentWidget(self):
+        """
+        Public method to get a reference to the current widget.
+        
+        @return reference to the current widget
+        @rtype QWidget
+        """
+        return self.__tabWidget.currentWidget()
+        
+    def setCurrentWidget(self, widget):
+        """
+        Public slot to set the current page based on the given widget.
+        
+        @param widget reference to the widget
+        @type QWidget
+        """
+        self.__tabWidget.setCurrentWidget(widget)
+    
+    def __callStackFrameSelected(self, frameNo):
+        """
+        Private slot to handle the selection of a call stack entry of the
+        call stack viewer.
+        
+        @param frameNo frame number (index) of the selected entry
+        @type int
+        """
+        if frameNo >= 0:
+            self.stackComboBox.setCurrentIndex(frameNo)
+    
+    def __debuggerSelected(self, current, previous):
+        """
+        Private slot to handle the selection of a debugger backend in the
+        debuggers list.
+        
+        @param current reference to the new current item
+        @type QTreeWidgetItem
+        @param previous reference to the previous current item
+        @type QTreeWidgetItem
+        """
+        if current is not None and self.__doDebuggersListUpdate:
+            if current.parent() is None:
+                # it is a debugger item
+                debuggerId = current.text(0)
+                self.globalsViewer.handleResetUI()
+                self.localsViewer.handleResetUI()
+                self.currentStack = None
+                self.stackComboBox.clear()
+                self.callStackViewer.clear()
+                
+                self.debugServer.remoteSetThread(debuggerId, -1)
+                self.__showSource()
+            else:
+                # it is a thread item
+                tid = current.data(0, self.ThreadIdRole)
+                self.debugServer.remoteSetThread(
+                    self.getSelectedDebuggerId(), tid)
+    
+    def __clientDebuggerId(self, debuggerId):
+        """
+        Private slot to receive the ID of a newly connected debugger backend.
+        
+        @param debuggerId ID of a newly connected debugger backend
+        @type str
+        """
+        itm = QTreeWidgetItem(self.__debuggersList, [debuggerId])
+        if self.__debuggersList.topLevelItemCount() > 1:
+            self.debugUI.showNotification(
+                self.tr("<p>Debugger with ID <b>{0}</b> has been connected."
+                        "</p>")
+                .format(debuggerId))
+        
+        self.__debuggersList.header().resizeSections(
+            QHeaderView.ResizeMode.ResizeToContents)
+        
+        if self.__debuggersList.topLevelItemCount() == 1:
+            # it is the only item, select it as the current one
+            self.__debuggersList.setCurrentItem(itm)
+    
+    def __setCurrentDebugger(self, debuggerId):
+        """
+        Private method to set the current debugger based on the given ID.
+        
+        @param debuggerId ID of the debugger to set as current debugger
+        @type str
+        """
+        debuggerItems = self.__debuggersList.findItems(
+            debuggerId, Qt.MatchFlag.MatchExactly)
+        if debuggerItems:
+            debuggerItem = debuggerItems[0]
+            currentItem = self.__debuggersList.currentItem()
+            if currentItem is debuggerItem:
+                # nothing to do
+                return
+            
+            if currentItem:
+                currentParent = currentItem.parent()
+            else:
+                currentParent = None
+            if currentParent is None:
+                # current is a debugger item
+                self.__debuggersList.setCurrentItem(debuggerItem)
+            elif currentParent is debuggerItem:
+                # nothing to do
+                return
+            else:
+                self.__debuggersList.setCurrentItem(debuggerItem)
+    
+    def isOnlyDebugger(self):
+        """
+        Public method to test, if only one debugger is connected.
+        
+        @return flag indicating that only one debugger is connected
+        @rtype bool
+        """
+        return self.__debuggersList.topLevelItemCount() == 1
+    
+    def getSelectedDebuggerId(self):
+        """
+        Public method to get the currently selected debugger ID.
+        
+        @return selected debugger ID
+        @rtype str
+        """
+        itm = self.__debuggersList.currentItem()
+        if itm:
+            if itm.parent() is None:
+                # it is a debugger item
+                return itm.text(0)
+            else:
+                # it is a thread item
+                return itm.parent().text(0)
+        else:
+            return ""
+    
+    def getSelectedDebuggerState(self):
+        """
+        Public method to get the currently selected debugger's state.
+        
+        @return selected debugger's state (broken, exception, running)
+        @rtype str
+        """
+        itm = self.__debuggersList.currentItem()
+        if itm:
+            if itm.parent() is None:
+                # it is a debugger item
+                return itm.data(0, self.DebuggerStateRole)
+            else:
+                # it is a thread item
+                return itm.parent().data(0, self.DebuggerStateRole)
+        else:
+            return ""
+    
+    def __setDebuggerIconAndState(self, debuggerId, state):
+        """
+        Private method to set the icon for a specific debugger ID.
+        
+        @param debuggerId ID of the debugger backend (empty ID means the
+            currently selected one)
+        @type str
+        @param state state of the debugger (broken, exception, running)
+        @type str
+        """
+        debuggerItem = None
+        if debuggerId:
+            foundItems = self.__debuggersList.findItems(
+                debuggerId, Qt.MatchFlag.MatchExactly)
+            if foundItems:
+                debuggerItem = foundItems[0]
+        if debuggerItem is None:
+            debuggerItem = self.__debuggersList.currentItem()
+        if debuggerItem is not None:
+            try:
+                iconName = DebugViewer.StateIcon[state]
+            except KeyError:
+                iconName = "question"
+            try:
+                stateText = DebugViewer.StateMessage[state]
+            except KeyError:
+                stateText = self.tr("unknown state ({0})").format(state)
+            debuggerItem.setIcon(0, UI.PixmapCache.getIcon(iconName))
+            debuggerItem.setData(0, self.DebuggerStateRole, state)
+            debuggerItem.setText(1, stateText)
+            
+            self.__debuggersList.header().resizeSections(
+                QHeaderView.ResizeMode.ResizeToContents)
+    
+    def __removeDebugger(self, debuggerId):
+        """
+        Private method to remove a debugger given its ID.
+        
+        @param debuggerId ID of the debugger to be removed from the list
+        @type str
+        """
+        foundItems = self.__debuggersList.findItems(
+            debuggerId, Qt.MatchFlag.MatchExactly)
+        if foundItems:
+            index = self.__debuggersList.indexOfTopLevelItem(foundItems[0])
+            itm = self.__debuggersList.takeTopLevelItem(index)
+            # __IGNORE_WARNING__
+            del itm
+    
+    def __addThreadList(self, currentID, threadList, debuggerId):
+        """
+        Private method to add the list of threads to a debugger entry.
+        
+        @param currentID id of the current thread
+        @type int
+        @param threadList list of dictionaries containing the thread data
+        @type list of dict
+        @param debuggerId ID of the debugger backend
+        @type str
+        """
+        debugStatus = -1    # i.e. running
+        
+        debuggerItems = self.__debuggersList.findItems(
+            debuggerId, Qt.MatchFlag.MatchExactly)
+        if debuggerItems:
+            debuggerItem = debuggerItems[0]
+            
+            currentItem = self.__debuggersList.currentItem()
+            if currentItem.parent() is debuggerItem:
+                currentChild = currentItem.text(0)
+            else:
+                currentChild = ""
+            self.__doDebuggersListUpdate = False
+            debuggerItem.takeChildren()
+            for thread in threadList:
+                if thread.get('except', False):
+                    stateText = DebugViewer.StateMessage["exception"]
+                    iconName = DebugViewer.StateIcon["exception"]
+                    debugStatus = 1
+                elif thread['broken']:
+                    stateText = DebugViewer.StateMessage["broken"]
+                    iconName = DebugViewer.StateIcon["broken"]
+                    if debugStatus < 1:
+                        debugStatus = 0
+                else:
+                    stateText = DebugViewer.StateMessage["running"]
+                    iconName = DebugViewer.StateIcon["running"]
+                itm = QTreeWidgetItem(debuggerItem,
+                                      [thread['name'], stateText])
+                itm.setData(0, self.ThreadIdRole, thread['id'])
+                itm.setIcon(0, UI.PixmapCache.getIcon(iconName))
+                if currentChild == thread['name']:
+                    self.__debuggersList.setCurrentItem(itm)
+                if thread['id'] == currentID:
+                    font = debuggerItem.font(0)
+                    font.setItalic(True)
+                    itm.setFont(0, font)
+            
+            debuggerItem.setExpanded(debuggerItem.childCount() > 0)
+            
+            self.__debuggersList.header().resizeSections(
+                QHeaderView.ResizeMode.ResizeToContents)
+            self.__debuggersList.header().setStretchLastSection(True)
+            self.__doDebuggersListUpdate = True
+            
+            if debugStatus == -1:
+                debuggerState = "running"
+            elif debugStatus == 0:
+                debuggerState = "broken"
+            else:
+                debuggerState = "exception"
+            self.__setDebuggerIconAndState(debuggerId, debuggerState)
+    
+    def __setThreadIconAndState(self, debuggerId, threadName, state):
+        """
+        Private method to set the icon for a specific thread name and
+        debugger ID.
+        
+        @param debuggerId ID of the debugger backend (empty ID means the
+            currently selected one)
+        @type str
+        @param threadName name of the thread signaling the event
+        @type str
+        @param state state of the debugger (broken, exception, running)
+        @type str
+        """
+        debuggerItem = None
+        if debuggerId:
+            foundItems = self.__debuggersList.findItems(
+                debuggerId, Qt.MatchFlag.MatchExactly)
+            if foundItems:
+                debuggerItem = foundItems[0]
+        if debuggerItem is None:
+            debuggerItem = self.__debuggersList.currentItem()
+        if debuggerItem is not None:
+            for index in range(debuggerItem.childCount()):
+                childItem = debuggerItem.child(index)
+                if childItem.text(0) == threadName:
+                    break
+            else:
+                childItem = None
+            
+            if childItem is not None:
+                try:
+                    iconName = DebugViewer.StateIcon[state]
+                except KeyError:
+                    iconName = "question"
+                try:
+                    stateText = DebugViewer.StateMessage[state]
+                except KeyError:
+                    stateText = self.tr("unknown state ({0})").format(state)
+                childItem.setIcon(0, UI.PixmapCache.getIcon(iconName))
+                childItem.setText(1, stateText)
+            
+            self.__debuggersList.header().resizeSections(
+                QHeaderView.ResizeMode.ResizeToContents)

eric ide

mercurial