1# -*- coding: utf-8 -*-
2
3# Copyright (c) 2002 - 2021 Detlev Offenbach <detlev@die-offenbachs.de>
4#
5
6"""
7Module implementing a widget containing various debug related views.
8
9The views avaliable are:
10<ul>
11  <li>selector showing all connected debugger backends with associated
12      threads</li>
13  <li>variables viewer for global variables for the selected debug client</li>
14  <li>variables viewer for local variables for the selected debug client</li>
15  <li>call stack viewer for the selected debug client</li>
16  <li>call trace viewer</li>
17  <li>viewer for breakpoints</li>
18  <li>viewer for watch expressions</li>
19  <li>viewer for exceptions</li>
20  <li>viewer for a code disassembly for an exception<li>
21</ul>
22"""
23
24import os
25
26from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QCoreApplication
27from PyQt5.QtWidgets import (
28    QWidget, QVBoxLayout, QHBoxLayout, QLineEdit, QSizePolicy, QPushButton,
29    QComboBox, QLabel, QTreeWidget, QTreeWidgetItem, QHeaderView, QSplitter
30)
31
32import UI.PixmapCache
33import Preferences
34
35from E5Gui.E5TabWidget import E5TabWidget
36
37
38class DebugViewer(QWidget):
39    """
40    Class implementing a widget containing various debug related views.
41
42    The individual tabs contain the interpreter shell (optional),
43    the filesystem browser (optional), the two variables viewers
44    (global and local), a breakpoint viewer, a watch expression viewer and
45    the exception logger. Additionally a list of all threads is shown.
46
47    @signal sourceFile(string, int) emitted to open a source file at a line
48    @signal preferencesChanged() emitted to react on changed preferences
49    """
50    sourceFile = pyqtSignal(str, int)
51    preferencesChanged = pyqtSignal()
52
53    ThreadIdRole = Qt.ItemDataRole.UserRole + 1
54    DebuggerStateRole = Qt.ItemDataRole.UserRole + 2
55
56    # Map debug state to icon name
57    StateIcon = {
58        "broken": "break",
59        "exception": "exceptions",
60        "running": "mediaPlaybackStart",
61        "syntax": "syntaxError22",
62    }
63
64    # Map debug state to user message
65    StateMessage = {
66        "broken": QCoreApplication.translate(
67            "DebugViewer", "waiting at breakpoint"),
68        "exception": QCoreApplication.translate(
69            "DebugViewer", "waiting at exception"),
70        "running": QCoreApplication.translate(
71            "DebugViewer", "running"),
72        "syntax": QCoreApplication.translate(
73            "DebugViewer", "syntax error"),
74    }
75
76    def __init__(self, debugServer, parent=None):
77        """
78        Constructor
79
80        @param debugServer reference to the debug server object
81        @type DebugServer
82        @param parent parent widget
83        @type QWidget
84        """
85        super().__init__(parent)
86
87        self.debugServer = debugServer
88        self.debugUI = None
89
90        self.setWindowIcon(UI.PixmapCache.getIcon("eric"))
91
92        self.__mainLayout = QVBoxLayout()
93        self.__mainLayout.setContentsMargins(0, 3, 0, 0)
94        self.setLayout(self.__mainLayout)
95
96        self.__mainSplitter = QSplitter(Qt.Orientation.Vertical, self)
97        self.__mainLayout.addWidget(self.__mainSplitter)
98
99        # add the viewer showing the connected debug backends
100        self.__debuggersWidget = QWidget()
101        self.__debuggersLayout = QVBoxLayout(self.__debuggersWidget)
102        self.__debuggersLayout.setContentsMargins(0, 0, 0, 0)
103        self.__debuggersLayout.addWidget(
104            QLabel(self.tr("Debuggers and Threads:")))
105        self.__debuggersList = QTreeWidget()
106        self.__debuggersList.setHeaderLabels(
107            [self.tr("ID"), self.tr("State"), ""])
108        self.__debuggersList.header().setStretchLastSection(True)
109        self.__debuggersList.setSortingEnabled(True)
110        self.__debuggersList.setRootIsDecorated(True)
111        self.__debuggersList.setAlternatingRowColors(True)
112        self.__debuggersLayout.addWidget(self.__debuggersList)
113        self.__mainSplitter.addWidget(self.__debuggersWidget)
114
115        self.__debuggersList.currentItemChanged.connect(
116            self.__debuggerSelected)
117
118        # add the tab widget containing various debug related views
119        self.__tabWidget = E5TabWidget()
120        self.__mainSplitter.addWidget(self.__tabWidget)
121
122        from .VariablesViewer import VariablesViewer
123        # add the global variables viewer
124        self.glvWidget = QWidget()
125        self.glvWidgetVLayout = QVBoxLayout(self.glvWidget)
126        self.glvWidgetVLayout.setContentsMargins(0, 0, 0, 0)
127        self.glvWidgetVLayout.setSpacing(3)
128        self.glvWidget.setLayout(self.glvWidgetVLayout)
129
130        self.globalsViewer = VariablesViewer(self, True, self.glvWidget)
131        self.glvWidgetVLayout.addWidget(self.globalsViewer)
132
133        self.glvWidgetHLayout = QHBoxLayout()
134        self.glvWidgetHLayout.setContentsMargins(3, 3, 3, 3)
135
136        self.globalsFilterEdit = QLineEdit(self.glvWidget)
137        self.globalsFilterEdit.setSizePolicy(
138            QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
139        self.glvWidgetHLayout.addWidget(self.globalsFilterEdit)
140        self.globalsFilterEdit.setToolTip(
141            self.tr("Enter regular expression patterns separated by ';'"
142                    " to define variable filters. "))
143        self.globalsFilterEdit.setWhatsThis(
144            self.tr("Enter regular expression patterns separated by ';'"
145                    " to define variable filters. All variables and"
146                    " class attributes matched by one of the expressions"
147                    " are not shown in the list above."))
148
149        self.setGlobalsFilterButton = QPushButton(
150            self.tr('Set'), self.glvWidget)
151        self.glvWidgetHLayout.addWidget(self.setGlobalsFilterButton)
152        self.glvWidgetVLayout.addLayout(self.glvWidgetHLayout)
153
154        index = self.__tabWidget.addTab(
155            self.glvWidget,
156            UI.PixmapCache.getIcon("globalVariables"), '')
157        self.__tabWidget.setTabToolTip(
158            index,
159            self.tr("Shows the list of global variables and their values."))
160
161        self.setGlobalsFilterButton.clicked.connect(
162            self.setGlobalsFilter)
163        self.globalsFilterEdit.returnPressed.connect(self.setGlobalsFilter)
164
165        # add the local variables viewer
166        self.lvWidget = QWidget()
167        self.lvWidgetVLayout = QVBoxLayout(self.lvWidget)
168        self.lvWidgetVLayout.setContentsMargins(0, 0, 0, 0)
169        self.lvWidgetVLayout.setSpacing(3)
170        self.lvWidget.setLayout(self.lvWidgetVLayout)
171
172        self.lvWidgetHLayout1 = QHBoxLayout()
173        self.lvWidgetHLayout1.setContentsMargins(3, 3, 3, 3)
174
175        self.stackComboBox = QComboBox(self.lvWidget)
176        self.stackComboBox.setSizePolicy(
177            QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
178        self.lvWidgetHLayout1.addWidget(self.stackComboBox)
179
180        self.sourceButton = QPushButton(self.tr('Source'), self.lvWidget)
181        self.lvWidgetHLayout1.addWidget(self.sourceButton)
182        self.sourceButton.setEnabled(False)
183        self.lvWidgetVLayout.addLayout(self.lvWidgetHLayout1)
184
185        self.localsViewer = VariablesViewer(self, False, self.lvWidget)
186        self.lvWidgetVLayout.addWidget(self.localsViewer)
187
188        self.lvWidgetHLayout2 = QHBoxLayout()
189        self.lvWidgetHLayout2.setContentsMargins(3, 3, 3, 3)
190
191        self.localsFilterEdit = QLineEdit(self.lvWidget)
192        self.localsFilterEdit.setSizePolicy(
193            QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
194        self.lvWidgetHLayout2.addWidget(self.localsFilterEdit)
195        self.localsFilterEdit.setToolTip(
196            self.tr(
197                "Enter regular expression patterns separated by ';' to define "
198                "variable filters. "))
199        self.localsFilterEdit.setWhatsThis(
200            self.tr(
201                "Enter regular expression patterns separated by ';' to define "
202                "variable filters. All variables and class attributes matched"
203                " by one of the expressions are not shown in the list above."))
204
205        self.setLocalsFilterButton = QPushButton(
206            self.tr('Set'), self.lvWidget)
207        self.lvWidgetHLayout2.addWidget(self.setLocalsFilterButton)
208        self.lvWidgetVLayout.addLayout(self.lvWidgetHLayout2)
209
210        index = self.__tabWidget.addTab(
211            self.lvWidget,
212            UI.PixmapCache.getIcon("localVariables"), '')
213        self.__tabWidget.setTabToolTip(
214            index,
215            self.tr("Shows the list of local variables and their values."))
216
217        self.sourceButton.clicked.connect(self.__showSource)
218        self.stackComboBox.currentIndexChanged[int].connect(
219            self.__frameSelected)
220        self.setLocalsFilterButton.clicked.connect(self.setLocalsFilter)
221        self.localsFilterEdit.returnPressed.connect(self.setLocalsFilter)
222
223        self.preferencesChanged.connect(self.handlePreferencesChanged)
224        self.preferencesChanged.connect(self.globalsViewer.preferencesChanged)
225        self.preferencesChanged.connect(self.localsViewer.preferencesChanged)
226
227        from .CallStackViewer import CallStackViewer
228        # add the call stack viewer
229        self.callStackViewer = CallStackViewer(self.debugServer)
230        index = self.__tabWidget.addTab(
231            self.callStackViewer,
232            UI.PixmapCache.getIcon("callStack"), "")
233        self.__tabWidget.setTabToolTip(
234            index,
235            self.tr("Shows the current call stack."))
236        self.callStackViewer.sourceFile.connect(self.sourceFile)
237        self.callStackViewer.frameSelected.connect(
238            self.__callStackFrameSelected)
239
240        from .CallTraceViewer import CallTraceViewer
241        # add the call trace viewer
242        self.callTraceViewer = CallTraceViewer(self.debugServer, self)
243        index = self.__tabWidget.addTab(
244            self.callTraceViewer,
245            UI.PixmapCache.getIcon("callTrace"), "")
246        self.__tabWidget.setTabToolTip(
247            index,
248            self.tr("Shows a trace of the program flow."))
249        self.callTraceViewer.sourceFile.connect(self.sourceFile)
250
251        from .BreakPointViewer import BreakPointViewer
252        # add the breakpoint viewer
253        self.breakpointViewer = BreakPointViewer()
254        self.breakpointViewer.setModel(self.debugServer.getBreakPointModel())
255        index = self.__tabWidget.addTab(
256            self.breakpointViewer,
257            UI.PixmapCache.getIcon("breakpoints"), '')
258        self.__tabWidget.setTabToolTip(
259            index,
260            self.tr("Shows a list of defined breakpoints."))
261        self.breakpointViewer.sourceFile.connect(self.sourceFile)
262
263        from .WatchPointViewer import WatchPointViewer
264        # add the watch expression viewer
265        self.watchpointViewer = WatchPointViewer()
266        self.watchpointViewer.setModel(self.debugServer.getWatchPointModel())
267        index = self.__tabWidget.addTab(
268            self.watchpointViewer,
269            UI.PixmapCache.getIcon("watchpoints"), '')
270        self.__tabWidget.setTabToolTip(
271            index,
272            self.tr("Shows a list of defined watchpoints."))
273
274        from .ExceptionLogger import ExceptionLogger
275        # add the exception logger
276        self.exceptionLogger = ExceptionLogger()
277        index = self.__tabWidget.addTab(
278            self.exceptionLogger,
279            UI.PixmapCache.getIcon("exceptions"), '')
280        self.__tabWidget.setTabToolTip(
281            index,
282            self.tr("Shows a list of raised exceptions."))
283
284        from UI.PythonDisViewer import PythonDisViewer, PythonDisViewerModes
285        # add the Python disassembly viewer
286        self.disassemblyViewer = PythonDisViewer(
287            None, mode=PythonDisViewerModes.TRACEBACK)
288        index = self.__tabWidget.addTab(
289            self.disassemblyViewer,
290            UI.PixmapCache.getIcon("disassembly"), '')
291        self.__tabWidget.setTabToolTip(
292            index,
293            self.tr("Shows a code disassembly in case of an exception."))
294
295        self.__tabWidget.setCurrentWidget(self.glvWidget)
296
297        self.__doDebuggersListUpdate = True
298
299        self.__mainSplitter.setSizes([100, 700])
300
301        self.currentStack = None
302        self.framenr = 0
303
304        self.__autoViewSource = Preferences.getDebugger("AutoViewSourceCode")
305        self.sourceButton.setVisible(not self.__autoViewSource)
306
307        # connect some debug server signals
308        self.debugServer.clientStack.connect(
309            self.handleClientStack)
310        self.debugServer.clientThreadList.connect(
311            self.__addThreadList)
312        self.debugServer.clientDebuggerId.connect(
313            self.__clientDebuggerId)
314        self.debugServer.passiveDebugStarted.connect(
315            self.handleDebuggingStarted)
316        self.debugServer.clientLine.connect(
317            self.__clientLine)
318        self.debugServer.clientSyntaxError.connect(
319            self.__clientSyntaxError)
320        self.debugServer.clientException.connect(
321            self.__clientException)
322        self.debugServer.clientExit.connect(
323            self.__clientExit)
324        self.debugServer.clientDisconnected.connect(
325            self.__removeDebugger)
326
327        self.debugServer.clientException.connect(
328            self.exceptionLogger.addException)
329        self.debugServer.passiveDebugStarted.connect(
330            self.exceptionLogger.debuggingStarted)
331
332        self.debugServer.clientLine.connect(
333            self.breakpointViewer.highlightBreakpoint)
334
335    def handlePreferencesChanged(self):
336        """
337        Public slot to handle the preferencesChanged signal.
338        """
339        self.__autoViewSource = Preferences.getDebugger("AutoViewSourceCode")
340        self.sourceButton.setVisible(not self.__autoViewSource)
341
342    def setDebugger(self, debugUI):
343        """
344        Public method to set a reference to the Debug UI.
345
346        @param debugUI reference to the DebugUI object
347        @type DebugUI
348        """
349        self.debugUI = debugUI
350        self.callStackViewer.setDebugger(debugUI)
351
352        # connect some debugUI signals
353        self.debugUI.clientStack.connect(self.handleClientStack)
354        self.debugUI.debuggingStarted.connect(
355            self.exceptionLogger.debuggingStarted)
356        self.debugUI.debuggingStarted.connect(
357            self.handleDebuggingStarted)
358
359    def handleResetUI(self, fullReset):
360        """
361        Public method to reset the viewer.
362
363        @param fullReset flag indicating a full reset is required
364        @type bool
365        """
366        self.globalsViewer.handleResetUI()
367        self.localsViewer.handleResetUI()
368        self.setGlobalsFilter()
369        self.setLocalsFilter()
370        self.sourceButton.setEnabled(False)
371        self.currentStack = None
372        self.stackComboBox.clear()
373        self.__tabWidget.setCurrentWidget(self.glvWidget)
374        self.breakpointViewer.handleResetUI()
375        if fullReset:
376            self.__debuggersList.clear()
377        self.disassemblyViewer.clear()
378
379    def initCallStackViewer(self, projectMode):
380        """
381        Public method to initialize the call stack viewer.
382
383        @param projectMode flag indicating to enable the project mode
384        @type bool
385        """
386        self.callStackViewer.clear()
387        self.callStackViewer.setProjectMode(projectMode)
388
389    def isCallTraceEnabled(self):
390        """
391        Public method to get the state of the call trace function.
392
393        @return flag indicating the state of the call trace function
394        @rtype bool
395        """
396        return self.callTraceViewer.isCallTraceEnabled()
397
398    def clearCallTrace(self):
399        """
400        Public method to clear the recorded call trace.
401        """
402        self.callTraceViewer.clear()
403
404    def setCallTraceToProjectMode(self, enabled):
405        """
406        Public slot to set the call trace viewer to project mode.
407
408        In project mode the call trace info is shown with project relative
409        path names.
410
411        @param enabled flag indicating to enable the project mode
412        @type bool
413        """
414        self.callTraceViewer.setProjectMode(enabled)
415
416    def showVariables(self, vlist, showGlobals):
417        """
418        Public method to show the variables in the respective window.
419
420        @param vlist list of variables to display
421        @type list
422        @param showGlobals flag indicating global/local state
423        @type bool
424        """
425        if showGlobals:
426            self.globalsViewer.showVariables(vlist, self.framenr)
427        else:
428            self.localsViewer.showVariables(vlist, self.framenr)
429
430    def showVariable(self, vlist, showGlobals):
431        """
432        Public method to show the variables in the respective window.
433
434        @param vlist list of variables to display
435        @type list
436        @param showGlobals flag indicating global/local state
437        @type bool
438        """
439        if showGlobals:
440            self.globalsViewer.showVariable(vlist)
441        else:
442            self.localsViewer.showVariable(vlist)
443
444    def showVariablesTab(self, showGlobals):
445        """
446        Public method to make a variables tab visible.
447
448        @param showGlobals flag indicating global/local state
449        @type bool
450        """
451        if showGlobals:
452            self.__tabWidget.setCurrentWidget(self.glvWidget)
453        else:
454            self.__tabWidget.setCurrentWidget(self.lvWidget)
455
456    def handleClientStack(self, stack, debuggerId):
457        """
458        Public slot to show the call stack of the program being debugged.
459
460        @param stack list of tuples with call stack data (file name,
461            line number, function name, formatted argument/values list)
462        @type list of tuples of (str, str, str, str)
463        @param debuggerId ID of the debugger backend
464        @type str
465        """
466        if debuggerId == self.getSelectedDebuggerId():
467            block = self.stackComboBox.blockSignals(True)
468            self.framenr = 0
469            self.stackComboBox.clear()
470            self.currentStack = stack
471            self.sourceButton.setEnabled(len(stack) > 0)
472            for s in stack:
473                # just show base filename to make it readable
474                s = (os.path.basename(s[0]), s[1], s[2])
475                self.stackComboBox.addItem('{0}:{1}:{2}'.format(*s))
476            self.stackComboBox.blockSignals(block)
477
478    def __clientLine(self, fn, line, debuggerId, threadName):
479        """
480        Private method to handle a change to the current line.
481
482        @param fn filename
483        @type str
484        @param line linenumber
485        @type int
486        @param debuggerId ID of the debugger backend
487        @type str
488        @param threadName name of the thread signaling the event
489        @type str
490        """
491        self.__setDebuggerIconAndState(debuggerId, "broken")
492        self.__setThreadIconAndState(debuggerId, threadName, "broken")
493        if debuggerId != self.getSelectedDebuggerId():
494            self.__setCurrentDebugger(debuggerId)
495
496    @pyqtSlot(str, int, str, bool, str)
497    def __clientExit(self, program, status, message, quiet, debuggerId):
498        """
499        Private method to handle the debugged program terminating.
500
501        @param program name of the exited program
502        @type str
503        @param status exit code of the debugged program
504        @type int
505        @param message exit message of the debugged program
506        @type str
507        @param quiet flag indicating to suppress exit info display
508        @type bool
509        @param debuggerId ID of the debugger backend
510        @type str
511        """
512        if not self.isOnlyDebugger():
513            if debuggerId == self.getSelectedDebuggerId():
514                # the current client has exited
515                self.globalsViewer.handleResetUI()
516                self.localsViewer.handleResetUI()
517                self.setGlobalsFilter()
518                self.setLocalsFilter()
519                self.sourceButton.setEnabled(False)
520                self.currentStack = None
521                self.stackComboBox.clear()
522
523            self.__removeDebugger(debuggerId)
524
525    def __clientSyntaxError(self, message, filename, lineNo, characterNo,
526                            debuggerId, threadName):
527        """
528        Private method to handle a syntax error in the debugged program.
529
530        @param message message of the syntax error
531        @type str
532        @param filename translated filename of the syntax error position
533        @type str
534        @param lineNo line number of the syntax error position
535        @type int
536        @param characterNo character number of the syntax error position
537        @type int
538        @param debuggerId ID of the debugger backend
539        @type str
540        @param threadName name of the thread signaling the event
541        @type str
542        """
543        self.__setDebuggerIconAndState(debuggerId, "syntax")
544        self.__setThreadIconAndState(debuggerId, threadName, "syntax")
545
546    def __clientException(self, exceptionType, exceptionMessage, stackTrace,
547                          debuggerId, threadName):
548        """
549        Private method to handle an exception of the debugged program.
550
551        @param exceptionType type of exception raised
552        @type str
553        @param exceptionMessage message given by the exception
554        @type (str
555        @param stackTrace list of stack entries
556        @type list of str
557        @param debuggerId ID of the debugger backend
558        @type str
559        @param threadName name of the thread signaling the event
560        @type str
561        """
562        self.__setDebuggerIconAndState(debuggerId, "exception")
563        self.__setThreadIconAndState(debuggerId, threadName, "exception")
564
565    def setVariablesFilter(self, globalsFilter, localsFilter):
566        """
567        Public slot to set the local variables filter.
568
569        @param globalsFilter filter list for global variable types
570        @type list of str
571        @param localsFilter filter list for local variable types
572        @type list of str
573        """
574        self.__globalsFilter = globalsFilter
575        self.__localsFilter = localsFilter
576
577    def __showSource(self):
578        """
579        Private slot to handle the source button press to show the selected
580        file.
581        """
582        index = self.stackComboBox.currentIndex()
583        if index > -1 and self.currentStack:
584            s = self.currentStack[index]
585            self.sourceFile.emit(s[0], int(s[1]))
586
587    def __frameSelected(self, frmnr):
588        """
589        Private slot to handle the selection of a new stack frame number.
590
591        @param frmnr frame number (0 is the current frame)
592        @type int
593        """
594        if frmnr >= 0:
595            self.framenr = frmnr
596            if self.debugServer.isDebugging():
597                self.debugServer.remoteClientVariables(
598                    self.getSelectedDebuggerId(), 0, self.__localsFilter,
599                    frmnr)
600
601            if self.__autoViewSource:
602                self.__showSource()
603
604    def setGlobalsFilter(self):
605        """
606        Public slot to set the global variable filter.
607        """
608        if self.debugServer.isDebugging():
609            filterStr = self.globalsFilterEdit.text()
610            self.debugServer.remoteClientSetFilter(
611                self.getSelectedDebuggerId(), 1, filterStr)
612            self.debugServer.remoteClientVariables(
613                self.getSelectedDebuggerId(), 2, self.__globalsFilter)
614
615    def setLocalsFilter(self):
616        """
617        Public slot to set the local variable filter.
618        """
619        if self.debugServer.isDebugging():
620            filterStr = self.localsFilterEdit.text()
621            self.debugServer.remoteClientSetFilter(
622                self.getSelectedDebuggerId(), 0, filterStr)
623            if self.currentStack:
624                self.debugServer.remoteClientVariables(
625                    self.getSelectedDebuggerId(), 0, self.__localsFilter,
626                    self.framenr)
627
628    def handleDebuggingStarted(self):
629        """
630        Public slot to handle the start of a debugging session.
631
632        This slot sets the variables filter expressions.
633        """
634        self.setGlobalsFilter()
635        self.setLocalsFilter()
636        self.showVariablesTab(False)
637
638        self.disassemblyViewer.clear()
639
640    def currentWidget(self):
641        """
642        Public method to get a reference to the current widget.
643
644        @return reference to the current widget
645        @rtype QWidget
646        """
647        return self.__tabWidget.currentWidget()
648
649    def setCurrentWidget(self, widget):
650        """
651        Public slot to set the current page based on the given widget.
652
653        @param widget reference to the widget
654        @type QWidget
655        """
656        self.__tabWidget.setCurrentWidget(widget)
657
658    def __callStackFrameSelected(self, frameNo):
659        """
660        Private slot to handle the selection of a call stack entry of the
661        call stack viewer.
662
663        @param frameNo frame number (index) of the selected entry
664        @type int
665        """
666        if frameNo >= 0:
667            self.stackComboBox.setCurrentIndex(frameNo)
668
669    def __debuggerSelected(self, current, previous):
670        """
671        Private slot to handle the selection of a debugger backend in the
672        debuggers list.
673
674        @param current reference to the new current item
675        @type QTreeWidgetItem
676        @param previous reference to the previous current item
677        @type QTreeWidgetItem
678        """
679        if current is not None and self.__doDebuggersListUpdate:
680            if current.parent() is None:
681                # it is a debugger item
682                debuggerId = current.text(0)
683                self.globalsViewer.handleResetUI()
684                self.localsViewer.handleResetUI()
685                self.currentStack = None
686                self.stackComboBox.clear()
687                self.callStackViewer.clear()
688
689                self.debugServer.remoteSetThread(debuggerId, -1)
690                self.__showSource()
691            else:
692                # it is a thread item
693                tid = current.data(0, self.ThreadIdRole)
694                self.debugServer.remoteSetThread(
695                    self.getSelectedDebuggerId(), tid)
696
697    def __clientDebuggerId(self, debuggerId):
698        """
699        Private slot to receive the ID of a newly connected debugger backend.
700
701        @param debuggerId ID of a newly connected debugger backend
702        @type str
703        """
704        itm = QTreeWidgetItem(self.__debuggersList, [debuggerId])
705        if self.__debuggersList.topLevelItemCount() > 1:
706            self.debugUI.showNotification(
707                self.tr("<p>Debugger with ID <b>{0}</b> has been connected."
708                        "</p>")
709                .format(debuggerId))
710
711        self.__debuggersList.header().resizeSections(
712            QHeaderView.ResizeMode.ResizeToContents)
713
714        if self.__debuggersList.topLevelItemCount() == 1:
715            # it is the only item, select it as the current one
716            self.__debuggersList.setCurrentItem(itm)
717
718    def __setCurrentDebugger(self, debuggerId):
719        """
720        Private method to set the current debugger based on the given ID.
721
722        @param debuggerId ID of the debugger to set as current debugger
723        @type str
724        """
725        debuggerItems = self.__debuggersList.findItems(
726            debuggerId, Qt.MatchFlag.MatchExactly)
727        if debuggerItems:
728            debuggerItem = debuggerItems[0]
729            currentItem = self.__debuggersList.currentItem()
730            if currentItem is debuggerItem:
731                # nothing to do
732                return
733
734            if currentItem:
735                currentParent = currentItem.parent()
736            else:
737                currentParent = None
738            if currentParent is None:
739                # current is a debugger item
740                self.__debuggersList.setCurrentItem(debuggerItem)
741            elif currentParent is debuggerItem:
742                # nothing to do
743                return
744            else:
745                self.__debuggersList.setCurrentItem(debuggerItem)
746
747    def isOnlyDebugger(self):
748        """
749        Public method to test, if only one debugger is connected.
750
751        @return flag indicating that only one debugger is connected
752        @rtype bool
753        """
754        return self.__debuggersList.topLevelItemCount() == 1
755
756    def getSelectedDebuggerId(self):
757        """
758        Public method to get the currently selected debugger ID.
759
760        @return selected debugger ID
761        @rtype str
762        """
763        itm = self.__debuggersList.currentItem()
764        if itm:
765            if itm.parent() is None:
766                # it is a debugger item
767                return itm.text(0)
768            else:
769                # it is a thread item
770                return itm.parent().text(0)
771        else:
772            return ""
773
774    def getSelectedDebuggerState(self):
775        """
776        Public method to get the currently selected debugger's state.
777
778        @return selected debugger's state (broken, exception, running)
779        @rtype str
780        """
781        itm = self.__debuggersList.currentItem()
782        if itm:
783            if itm.parent() is None:
784                # it is a debugger item
785                return itm.data(0, self.DebuggerStateRole)
786            else:
787                # it is a thread item
788                return itm.parent().data(0, self.DebuggerStateRole)
789        else:
790            return ""
791
792    def __setDebuggerIconAndState(self, debuggerId, state):
793        """
794        Private method to set the icon for a specific debugger ID.
795
796        @param debuggerId ID of the debugger backend (empty ID means the
797            currently selected one)
798        @type str
799        @param state state of the debugger (broken, exception, running)
800        @type str
801        """
802        debuggerItem = None
803        if debuggerId:
804            foundItems = self.__debuggersList.findItems(
805                debuggerId, Qt.MatchFlag.MatchExactly)
806            if foundItems:
807                debuggerItem = foundItems[0]
808        if debuggerItem is None:
809            debuggerItem = self.__debuggersList.currentItem()
810        if debuggerItem is not None:
811            try:
812                iconName = DebugViewer.StateIcon[state]
813            except KeyError:
814                iconName = "question"
815            try:
816                stateText = DebugViewer.StateMessage[state]
817            except KeyError:
818                stateText = self.tr("unknown state ({0})").format(state)
819            debuggerItem.setIcon(0, UI.PixmapCache.getIcon(iconName))
820            debuggerItem.setData(0, self.DebuggerStateRole, state)
821            debuggerItem.setText(1, stateText)
822
823            self.__debuggersList.header().resizeSections(
824                QHeaderView.ResizeMode.ResizeToContents)
825
826    def __removeDebugger(self, debuggerId):
827        """
828        Private method to remove a debugger given its ID.
829
830        @param debuggerId ID of the debugger to be removed from the list
831        @type str
832        """
833        foundItems = self.__debuggersList.findItems(
834            debuggerId, Qt.MatchFlag.MatchExactly)
835        if foundItems:
836            index = self.__debuggersList.indexOfTopLevelItem(foundItems[0])
837            itm = self.__debuggersList.takeTopLevelItem(index)
838            # __IGNORE_WARNING__
839            del itm
840
841    def __addThreadList(self, currentID, threadList, debuggerId):
842        """
843        Private method to add the list of threads to a debugger entry.
844
845        @param currentID id of the current thread
846        @type int
847        @param threadList list of dictionaries containing the thread data
848        @type list of dict
849        @param debuggerId ID of the debugger backend
850        @type str
851        """
852        debugStatus = -1    # i.e. running
853
854        debuggerItems = self.__debuggersList.findItems(
855            debuggerId, Qt.MatchFlag.MatchExactly)
856        if debuggerItems:
857            debuggerItem = debuggerItems[0]
858
859            currentItem = self.__debuggersList.currentItem()
860            if currentItem.parent() is debuggerItem:
861                currentChild = currentItem.text(0)
862            else:
863                currentChild = ""
864            self.__doDebuggersListUpdate = False
865            debuggerItem.takeChildren()
866            for thread in threadList:
867                if thread.get('except', False):
868                    stateText = DebugViewer.StateMessage["exception"]
869                    iconName = DebugViewer.StateIcon["exception"]
870                    debugStatus = 1
871                elif thread['broken']:
872                    stateText = DebugViewer.StateMessage["broken"]
873                    iconName = DebugViewer.StateIcon["broken"]
874                    if debugStatus < 1:
875                        debugStatus = 0
876                else:
877                    stateText = DebugViewer.StateMessage["running"]
878                    iconName = DebugViewer.StateIcon["running"]
879                itm = QTreeWidgetItem(debuggerItem,
880                                      [thread['name'], stateText])
881                itm.setData(0, self.ThreadIdRole, thread['id'])
882                itm.setIcon(0, UI.PixmapCache.getIcon(iconName))
883                if currentChild == thread['name']:
884                    self.__debuggersList.setCurrentItem(itm)
885                if thread['id'] == currentID:
886                    font = debuggerItem.font(0)
887                    font.setItalic(True)
888                    itm.setFont(0, font)
889
890            debuggerItem.setExpanded(debuggerItem.childCount() > 0)
891
892            self.__debuggersList.header().resizeSections(
893                QHeaderView.ResizeMode.ResizeToContents)
894            self.__debuggersList.header().setStretchLastSection(True)
895            self.__doDebuggersListUpdate = True
896
897            if debugStatus == -1:
898                debuggerState = "running"
899            elif debugStatus == 0:
900                debuggerState = "broken"
901            else:
902                debuggerState = "exception"
903            self.__setDebuggerIconAndState(debuggerId, debuggerState)
904
905    def __setThreadIconAndState(self, debuggerId, threadName, state):
906        """
907        Private method to set the icon for a specific thread name and
908        debugger ID.
909
910        @param debuggerId ID of the debugger backend (empty ID means the
911            currently selected one)
912        @type str
913        @param threadName name of the thread signaling the event
914        @type str
915        @param state state of the debugger (broken, exception, running)
916        @type str
917        """
918        debuggerItem = None
919        if debuggerId:
920            foundItems = self.__debuggersList.findItems(
921                debuggerId, Qt.MatchFlag.MatchExactly)
922            if foundItems:
923                debuggerItem = foundItems[0]
924        if debuggerItem is None:
925            debuggerItem = self.__debuggersList.currentItem()
926        if debuggerItem is not None:
927            for index in range(debuggerItem.childCount()):
928                childItem = debuggerItem.child(index)
929                if childItem.text(0) == threadName:
930                    break
931            else:
932                childItem = None
933
934            if childItem is not None:
935                try:
936                    iconName = DebugViewer.StateIcon[state]
937                except KeyError:
938                    iconName = "question"
939                try:
940                    stateText = DebugViewer.StateMessage[state]
941                except KeyError:
942                    stateText = self.tr("unknown state ({0})").format(state)
943                childItem.setIcon(0, UI.PixmapCache.getIcon(iconName))
944                childItem.setText(1, stateText)
945
946            self.__debuggersList.header().resizeSections(
947                QHeaderView.ResizeMode.ResizeToContents)
948