1# -*- coding: utf-8 -*-
2
3# Copyright (c) 2013 - 2021 Detlev Offenbach <detlev@die-offenbachs.de>
4#
5
6"""
7Module implementing the Call Stack viewer widget.
8"""
9
10from PyQt5.QtCore import pyqtSignal, Qt, QFileInfo
11from PyQt5.QtWidgets import (
12    QTreeWidget, QTreeWidgetItem, QMenu, QWidget, QVBoxLayout, QLabel
13)
14
15from E5Gui.E5Application import e5App
16from E5Gui import E5FileDialog, E5MessageBox
17
18import Utilities
19
20
21class CallStackViewer(QWidget):
22    """
23    Class implementing the Call Stack viewer widget.
24
25    @signal sourceFile(str, int) emitted to show the source of a stack entry
26    @signal frameSelected(int) emitted to signal the selection of a frame entry
27    """
28    sourceFile = pyqtSignal(str, int)
29    frameSelected = pyqtSignal(int)
30
31    FilenameRole = Qt.ItemDataRole.UserRole + 1
32    LinenoRole = Qt.ItemDataRole.UserRole + 2
33
34    def __init__(self, debugServer, parent=None):
35        """
36        Constructor
37
38        @param debugServer reference to the debug server object
39        @type DebugServer
40        @param parent reference to the parent widget
41        @type QWidget
42        """
43        super().__init__(parent)
44
45        self.__layout = QVBoxLayout(self)
46        self.setLayout(self.__layout)
47        self.__debuggerLabel = QLabel(self)
48        self.__layout.addWidget(self.__debuggerLabel)
49        self.__callStackList = QTreeWidget(self)
50        self.__layout.addWidget(self.__callStackList)
51
52        self.__callStackList.setHeaderHidden(True)
53        self.__callStackList.setAlternatingRowColors(True)
54        self.__callStackList.setItemsExpandable(False)
55        self.__callStackList.setRootIsDecorated(False)
56        self.setWindowTitle(self.tr("Call Stack"))
57
58        self.__menu = QMenu(self.__callStackList)
59        self.__sourceAct = self.__menu.addAction(
60            self.tr("Show source"), self.__openSource)
61        self.__menu.addAction(self.tr("Clear"), self.__callStackList.clear)
62        self.__menu.addSeparator()
63        self.__menu.addAction(self.tr("Save"), self.__saveStackTrace)
64        self.__callStackList.setContextMenuPolicy(
65            Qt.ContextMenuPolicy.CustomContextMenu)
66        self.__callStackList.customContextMenuRequested.connect(
67            self.__showContextMenu)
68
69        self.__dbs = debugServer
70
71        # file name, line number, function name, arguments
72        self.__entryFormat = self.tr("File: {0}\nLine: {1}\n{2}{3}")
73        # file name, line number
74        self.__entryFormatShort = self.tr("File: {0}\nLine: {1}")
75
76        self.__projectMode = False
77        self.__project = None
78
79        self.__dbs.clientStack.connect(self.__showCallStack)
80        self.__callStackList.itemDoubleClicked.connect(
81            self.__itemDoubleClicked)
82
83    def setDebugger(self, debugUI):
84        """
85        Public method to set a reference to the Debug UI.
86
87        @param debugUI reference to the DebugUI object
88        @type DebugUI
89        """
90        debugUI.clientStack.connect(self.__showCallStack)
91
92    def setProjectMode(self, enabled):
93        """
94        Public slot to set the call trace viewer to project mode.
95
96        In project mode the call trace info is shown with project relative
97        path names.
98
99        @param enabled flag indicating to enable the project mode
100        @type bool
101        """
102        self.__projectMode = enabled
103        if enabled and self.__project is None:
104            self.__project = e5App().getObject("Project")
105
106    def __showContextMenu(self, coord):
107        """
108        Private slot to show the context menu.
109
110        @param coord the position of the mouse pointer
111        @type QPoint
112        """
113        if self.__callStackList.topLevelItemCount() > 0:
114            itm = self.__callStackList.currentItem()
115            self.__sourceAct.setEnabled(itm is not None)
116            self.__menu.popup(self.__callStackList.mapToGlobal(coord))
117
118    def clear(self):
119        """
120        Public method to clear the stack viewer data.
121        """
122        self.__debuggerLabel.clear()
123        self.__callStackList.clear()
124
125    def __showCallStack(self, stack, debuggerId):
126        """
127        Private slot to show the call stack of the program being debugged.
128
129        @param stack list of tuples with call stack data (file name,
130            line number, function name, formatted argument/values list)
131        @type list of tuples of (str, str, str, str)
132        @param debuggerId ID of the debugger backend
133        @type str
134        """
135        self.__debuggerLabel.setText(debuggerId)
136
137        self.__callStackList.clear()
138        for fname, fline, ffunc, fargs in stack:
139            dfname = (
140                self.__project.getRelativePath(fname)
141                if self.__projectMode else
142                fname
143            )
144            itm = (
145                # use normal format
146                QTreeWidgetItem(
147                    self.__callStackList,
148                    [self.__entryFormat.format(dfname, fline, ffunc, fargs)]
149                )
150                if ffunc and not ffunc.startswith("<") else
151                # use short format
152                QTreeWidgetItem(
153                    self.__callStackList,
154                    [self.__entryFormatShort.format(dfname, fline)]
155                )
156            )
157            itm.setData(0, self.FilenameRole, fname)
158            itm.setData(0, self.LinenoRole, fline)
159
160        self.__callStackList.resizeColumnToContents(0)
161
162    def __itemDoubleClicked(self, itm):
163        """
164        Private slot to handle a double click of a stack entry.
165
166        @param itm reference to the double clicked item
167        @type QTreeWidgetItem
168        """
169        fname = itm.data(0, self.FilenameRole)
170        fline = itm.data(0, self.LinenoRole)
171        if self.__projectMode:
172            fname = self.__project.getAbsolutePath(fname)
173        self.sourceFile.emit(fname, fline)
174
175        index = self.__callStackList.indexOfTopLevelItem(itm)
176        self.frameSelected.emit(index)
177
178    def __openSource(self):
179        """
180        Private slot to show the source for the selected stack entry.
181        """
182        itm = self.__callStackList.currentItem()
183        if itm:
184            self.__itemDoubleClicked(itm)
185
186    def __saveStackTrace(self):
187        """
188        Private slot to save the stack trace info to a file.
189        """
190        if self.__callStackList.topLevelItemCount() > 0:
191            fname, selectedFilter = E5FileDialog.getSaveFileNameAndFilter(
192                self,
193                self.tr("Save Call Stack Info"),
194                "",
195                self.tr("Text Files (*.txt);;All Files (*)"),
196                None,
197                E5FileDialog.Options(E5FileDialog.DontConfirmOverwrite))
198            if fname:
199                ext = QFileInfo(fname).suffix()
200                if not ext:
201                    ex = selectedFilter.split("(*")[1].split(")")[0]
202                    if ex:
203                        fname += ex
204                if QFileInfo(fname).exists():
205                    res = E5MessageBox.yesNo(
206                        self,
207                        self.tr("Save Call Stack Info"),
208                        self.tr("<p>The file <b>{0}</b> already exists."
209                                " Overwrite it?</p>").format(fname),
210                        icon=E5MessageBox.Warning)
211                    if not res:
212                        return
213                    fname = Utilities.toNativeSeparators(fname)
214
215                try:
216                    title = self.tr("Call Stack of '{0}'").format(
217                        self.__debuggerLabel.text())
218                    with open(fname, "w", encoding="utf-8") as f:
219                        f.write("{0}\n".format(title))
220                        f.write("{0}\n\n".format(len(title) * "="))
221                        itm = self.__callStackList.topLevelItem(0)
222                        while itm is not None:
223                            f.write("{0}\n".format(itm.text(0)))
224                            f.write("{0}\n".format(78 * "="))
225                            itm = self.__callStackList.itemBelow(itm)
226                except OSError as err:
227                    E5MessageBox.critical(
228                        self,
229                        self.tr("Error saving Call Stack Info"),
230                        self.tr("""<p>The call stack info could not be"""
231                                """ written to <b>{0}</b></p>"""
232                                """<p>Reason: {1}</p>""")
233                        .format(fname, str(err)))
234