eric7/Debugger/CallTraceViewer.py

branch
eric7
changeset 8312
800c432b34c8
parent 8230
8b5c6896655b
child 8318
962bce857696
equal deleted inserted replaced
8311:4e8b98454baa 8312:800c432b34c8
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2012 - 2021 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing the Call Trace viewer widget.
8 """
9
10 import re
11
12 from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QFileInfo
13 from PyQt5.QtWidgets import QWidget, QTreeWidgetItem
14
15 from E5Gui.E5Application import e5App
16 from E5Gui import E5FileDialog, E5MessageBox
17
18 from .Ui_CallTraceViewer import Ui_CallTraceViewer
19
20 import UI.PixmapCache
21 import Preferences
22 import Utilities
23
24
25 class CallTraceViewer(QWidget, Ui_CallTraceViewer):
26 """
27 Class implementing the Call Trace viewer widget.
28
29 @signal sourceFile(str, int) emitted to show the source of a call/return
30 point
31 """
32 sourceFile = pyqtSignal(str, int)
33
34 def __init__(self, debugServer, debugViewer, parent=None):
35 """
36 Constructor
37
38 @param debugServer reference to the debug server object
39 @type DebugServer
40 @param debugViewer reference to the debug viewer object
41 @type DebugViewer
42 @param parent reference to the parent widget
43 @type QWidget
44 """
45 super().__init__(parent)
46 self.setupUi(self)
47
48 self.__dbs = debugServer
49 self.__debugViewer = debugViewer
50
51 self.startTraceButton.setIcon(
52 UI.PixmapCache.getIcon("callTraceStart"))
53 self.stopTraceButton.setIcon(
54 UI.PixmapCache.getIcon("callTraceStop"))
55 self.resizeButton.setIcon(UI.PixmapCache.getIcon("resizeColumns"))
56 self.clearButton.setIcon(UI.PixmapCache.getIcon("editDelete"))
57 self.saveButton.setIcon(UI.PixmapCache.getIcon("fileSave"))
58
59 self.__headerItem = QTreeWidgetItem(
60 ["", self.tr("From"), self.tr("To")])
61 self.__headerItem.setIcon(0, UI.PixmapCache.getIcon("callReturn"))
62 self.callTrace.setHeaderItem(self.__headerItem)
63
64 self.__callStack = []
65
66 self.__entryFormat = "{0}:{1} ({2})"
67 self.__entryRe = re.compile(r"""(.+):(\d+)\s\((.*)\)""")
68
69 self.__projectMode = False
70 self.__project = None
71 self.__tracedDebuggerId = ""
72
73 stopOnExit = Preferences.toBool(
74 Preferences.Prefs.settings.value("CallTrace/StopOnExit", True))
75 self.stopCheckBox.setChecked(stopOnExit)
76
77 self.__callTraceEnabled = (Preferences.toBool(
78 Preferences.Prefs.settings.value("CallTrace/Enabled", False)) and
79 not stopOnExit)
80
81 if self.__callTraceEnabled:
82 self.startTraceButton.setEnabled(False)
83 else:
84 self.stopTraceButton.setEnabled(False)
85
86 self.__dbs.callTraceInfo.connect(self.__addCallTraceInfo)
87 self.__dbs.clientExit.connect(self.__clientExit)
88
89 def __setCallTraceEnabled(self, enabled):
90 """
91 Private slot to set the call trace enabled status.
92
93 @param enabled flag indicating the new state
94 @type bool
95 """
96 if enabled:
97 self.__tracedDebuggerId = (
98 self.__debugViewer.getSelectedDebuggerId()
99 )
100 self.__dbs.setCallTraceEnabled(self.__tracedDebuggerId, enabled)
101 self.stopTraceButton.setEnabled(enabled)
102 self.startTraceButton.setEnabled(not enabled)
103 self.__callTraceEnabled = enabled
104 Preferences.Prefs.settings.setValue("CallTrace/Enabled", enabled)
105
106 if not enabled:
107 for column in range(self.callTrace.columnCount()):
108 self.callTrace.resizeColumnToContents(column)
109
110 @pyqtSlot(bool)
111 def on_stopCheckBox_clicked(self, checked):
112 """
113 Private slot to handle a click on the stop check box.
114
115 @param checked state of the check box
116 @type bool
117 """
118 Preferences.Prefs.settings.setValue("CallTrace/StopOnExit", checked)
119
120 @pyqtSlot()
121 def on_startTraceButton_clicked(self):
122 """
123 Private slot to start call tracing.
124 """
125 self.__setCallTraceEnabled(True)
126
127 @pyqtSlot()
128 def on_stopTraceButton_clicked(self):
129 """
130 Private slot to start call tracing.
131 """
132 self.__setCallTraceEnabled(False)
133
134 @pyqtSlot()
135 def on_resizeButton_clicked(self):
136 """
137 Private slot to resize the columns of the call trace to their contents.
138 """
139 for column in range(self.callTrace.columnCount()):
140 self.callTrace.resizeColumnToContents(column)
141
142 @pyqtSlot()
143 def on_clearButton_clicked(self):
144 """
145 Private slot to clear the call trace.
146 """
147 self.clear()
148
149 @pyqtSlot()
150 def on_saveButton_clicked(self):
151 """
152 Private slot to save the call trace info to a file.
153 """
154 if self.callTrace.topLevelItemCount() > 0:
155 fname, selectedFilter = E5FileDialog.getSaveFileNameAndFilter(
156 self,
157 self.tr("Save Call Trace Info"),
158 "",
159 self.tr("Text Files (*.txt);;All Files (*)"),
160 None,
161 E5FileDialog.Options(E5FileDialog.DontConfirmOverwrite))
162 if fname:
163 ext = QFileInfo(fname).suffix()
164 if not ext:
165 ex = selectedFilter.split("(*")[1].split(")")[0]
166 if ex:
167 fname += ex
168 if QFileInfo(fname).exists():
169 res = E5MessageBox.yesNo(
170 self,
171 self.tr("Save Call Trace Info"),
172 self.tr("<p>The file <b>{0}</b> already exists."
173 " Overwrite it?</p>").format(fname),
174 icon=E5MessageBox.Warning)
175 if not res:
176 return
177 fname = Utilities.toNativeSeparators(fname)
178
179 try:
180 title = self.tr("Call Trace Info of '{0}'").format(
181 self.__tracedDebuggerId)
182 with open(fname, "w", encoding="utf-8") as f:
183 f.write("{0}\n".format(title))
184 f.write("{0}\n\n".format(len(title) * "="))
185 itm = self.callTrace.topLevelItem(0)
186 while itm is not None:
187 isCall = itm.data(0, Qt.ItemDataRole.UserRole)
188 call = "->" if isCall else "<-"
189 f.write("{0} {1} || {2}\n".format(
190 call,
191 itm.text(1), itm.text(2)))
192 itm = self.callTrace.itemBelow(itm)
193 except OSError as err:
194 E5MessageBox.critical(
195 self,
196 self.tr("Error saving Call Trace Info"),
197 self.tr("""<p>The call trace info could not"""
198 """ be written to <b>{0}</b></p>"""
199 """<p>Reason: {1}</p>""")
200 .format(fname, str(err)))
201
202 @pyqtSlot(QTreeWidgetItem, int)
203 def on_callTrace_itemDoubleClicked(self, item, column):
204 """
205 Private slot to open the double clicked file in an editor.
206
207 @param item reference to the double clicked item
208 @type QTreeWidgetItem
209 @param column column that was double clicked
210 @type int
211 """
212 if item is not None and column > 0:
213 columnStr = item.text(column)
214 match = self.__entryRe.fullmatch(columnStr.strip())
215 if match:
216 filename, lineno, func = match.groups()
217 try:
218 lineno = int(lineno)
219 except ValueError:
220 # do nothing, if the line info is not an integer
221 return
222 if self.__projectMode:
223 filename = self.__project.getAbsolutePath(filename)
224 self.sourceFile.emit(filename, lineno)
225
226 def clear(self):
227 """
228 Public slot to clear the call trace info.
229 """
230 self.callTrace.clear()
231 self.__callStack = []
232
233 def setProjectMode(self, enabled):
234 """
235 Public slot to set the call trace viewer to project mode.
236
237 In project mode the call trace info is shown with project relative
238 path names.
239
240 @param enabled flag indicating to enable the project mode
241 @type bool
242 """
243 self.__projectMode = enabled
244 if enabled and self.__project is None:
245 self.__project = e5App().getObject("Project")
246
247 def __addCallTraceInfo(self, isCall, fromFile, fromLine, fromFunction,
248 toFile, toLine, toFunction, debuggerId):
249 """
250 Private method to add an entry to the call trace viewer.
251
252 @param isCall flag indicating a 'call'
253 @type bool
254 @param fromFile name of the originating file
255 @type str
256 @param fromLine line number in the originating file
257 @type str
258 @param fromFunction name of the originating function
259 @type str
260 @param toFile name of the target file
261 @type str
262 @param toLine line number in the target file
263 @type str
264 @param toFunction name of the target function
265 @type str
266 @param debuggerId ID of the debugger backend
267 @type str
268 """
269 if debuggerId == self.__tracedDebuggerId:
270 if isCall:
271 icon = UI.PixmapCache.getIcon("forward")
272 else:
273 icon = UI.PixmapCache.getIcon("back")
274 parentItem = (
275 self.__callStack[-1] if self.__callStack else self.callTrace)
276
277 if self.__projectMode:
278 fromFile = self.__project.getRelativePath(fromFile)
279 toFile = self.__project.getRelativePath(toFile)
280
281 itm = QTreeWidgetItem(
282 parentItem,
283 ["",
284 self.__entryFormat.format(fromFile, fromLine, fromFunction),
285 self.__entryFormat.format(toFile, toLine, toFunction)])
286 itm.setIcon(0, icon)
287 itm.setData(0, Qt.ItemDataRole.UserRole, isCall)
288 itm.setExpanded(True)
289
290 if isCall:
291 self.__callStack.append(itm)
292 else:
293 if self.__callStack:
294 self.__callStack.pop(-1)
295
296 def isCallTraceEnabled(self):
297 """
298 Public method to get the state of the call trace function.
299
300 @return flag indicating the state of the call trace function
301 @rtype bool
302 """
303 return self.__callTraceEnabled
304
305 @pyqtSlot(str, int, str, bool, str)
306 def __clientExit(self, program, status, message, quiet, debuggerId):
307 """
308 Private slot to handle a debug client terminating.
309
310 @param program name of the exited program
311 @type str
312 @param status exit code of the debugged program
313 @type int
314 @param message exit message of the debugged program
315 @type str
316 @param quiet flag indicating to suppress exit info display
317 @type bool
318 @param debuggerId ID of the debugger backend
319 @type str
320 """
321 if debuggerId == self.__tracedDebuggerId:
322 if self.stopCheckBox.isChecked():
323 self.__setCallTraceEnabled(False)
324 self.__tracedDebuggerId = ""

eric ide

mercurial