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