src/eric7/Debugger/CallTraceViewer.py

branch
eric7
changeset 9209
b99e7fd55fd3
parent 9153
506e35e424d5
child 9221
bf71ee032bb4
equal deleted inserted replaced
9208:3fc8dfeb6ebe 9209:b99e7fd55fd3
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2012 - 2022 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing the Call Trace viewer widget.
8 """
9
10 import pathlib
11 import re
12
13 from PyQt6.QtCore import pyqtSlot, pyqtSignal, Qt
14 from PyQt6.QtWidgets import QWidget, QTreeWidgetItem
15
16 from EricWidgets.EricApplication import ericApp
17 from EricWidgets import EricFileDialog, EricMessageBox
18
19 from .Ui_CallTraceViewer import Ui_CallTraceViewer
20
21 import UI.PixmapCache
22 import Preferences
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.getSettings().value("CallTrace/StopOnExit", True))
75 self.stopCheckBox.setChecked(stopOnExit)
76
77 self.__callTraceEnabled = (Preferences.toBool(
78 Preferences.getSettings().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.getSettings().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.getSettings().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 = EricFileDialog.getSaveFileNameAndFilter(
156 self,
157 self.tr("Save Call Trace Info"),
158 "",
159 self.tr("Text Files (*.txt);;All Files (*)"),
160 None,
161 EricFileDialog.DontConfirmOverwrite)
162 if fname:
163 fpath = pathlib.Path(fname)
164 if not fpath.suffix:
165 ex = selectedFilter.split("(*")[1].split(")")[0]
166 if ex:
167 fpath = fpath.with_suffix(ex)
168 if fpath.exists():
169 res = EricMessageBox.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(fpath),
174 icon=EricMessageBox.Warning)
175 if not res:
176 return
177
178 try:
179 title = self.tr("Call Trace Info of '{0}'").format(
180 self.__tracedDebuggerId)
181 with fpath.open("w", encoding="utf-8") as f:
182 f.write("{0}\n".format(title))
183 f.write("{0}\n\n".format(len(title) * "="))
184 itm = self.callTrace.topLevelItem(0)
185 while itm is not None:
186 isCall = itm.data(0, Qt.ItemDataRole.UserRole)
187 call = "->" if isCall else "<-"
188 f.write("{0} {1} || {2}\n".format(
189 call,
190 itm.text(1), itm.text(2)))
191 itm = self.callTrace.itemBelow(itm)
192 except OSError as err:
193 EricMessageBox.critical(
194 self,
195 self.tr("Error saving Call Trace Info"),
196 self.tr("""<p>The call trace info could not"""
197 """ be written to <b>{0}</b></p>"""
198 """<p>Reason: {1}</p>""")
199 .format(fpath, str(err)))
200
201 @pyqtSlot(QTreeWidgetItem, int)
202 def on_callTrace_itemDoubleClicked(self, item, column):
203 """
204 Private slot to open the double clicked file in an editor.
205
206 @param item reference to the double clicked item
207 @type QTreeWidgetItem
208 @param column column that was double clicked
209 @type int
210 """
211 if item is not None and column > 0:
212 columnStr = item.text(column)
213 match = self.__entryRe.fullmatch(columnStr.strip())
214 if match:
215 filename, lineno, func = match.groups()
216 try:
217 lineno = int(lineno)
218 except ValueError:
219 # do nothing, if the line info is not an integer
220 return
221 if self.__projectMode:
222 filename = self.__project.getAbsolutePath(filename)
223 self.sourceFile.emit(filename, lineno)
224
225 def clear(self):
226 """
227 Public slot to clear the call trace info.
228 """
229 self.callTrace.clear()
230 self.__callStack = []
231
232 def setProjectMode(self, enabled):
233 """
234 Public slot to set the call trace viewer to project mode.
235
236 In project mode the call trace info is shown with project relative
237 path names.
238
239 @param enabled flag indicating to enable the project mode
240 @type bool
241 """
242 self.__projectMode = enabled
243 if enabled and self.__project is None:
244 self.__project = ericApp().getObject("Project")
245
246 def __addCallTraceInfo(self, isCall, fromFile, fromLine, fromFunction,
247 toFile, toLine, toFunction, debuggerId):
248 """
249 Private method to add an entry to the call trace viewer.
250
251 @param isCall flag indicating a 'call'
252 @type bool
253 @param fromFile name of the originating file
254 @type str
255 @param fromLine line number in the originating file
256 @type str
257 @param fromFunction name of the originating function
258 @type str
259 @param toFile name of the target file
260 @type str
261 @param toLine line number in the target file
262 @type str
263 @param toFunction name of the target function
264 @type str
265 @param debuggerId ID of the debugger backend
266 @type str
267 """
268 if debuggerId == self.__tracedDebuggerId:
269 if isCall:
270 icon = UI.PixmapCache.getIcon("forward")
271 else:
272 icon = UI.PixmapCache.getIcon("back")
273 parentItem = (
274 self.__callStack[-1] if self.__callStack else self.callTrace)
275
276 if self.__projectMode:
277 fromFile = self.__project.getRelativePath(fromFile)
278 toFile = self.__project.getRelativePath(toFile)
279
280 itm = QTreeWidgetItem(
281 parentItem,
282 ["",
283 self.__entryFormat.format(fromFile, fromLine, fromFunction),
284 self.__entryFormat.format(toFile, toLine, toFunction)])
285 itm.setIcon(0, icon)
286 itm.setData(0, Qt.ItemDataRole.UserRole, isCall)
287 itm.setExpanded(True)
288
289 if isCall:
290 self.__callStack.append(itm)
291 else:
292 if self.__callStack:
293 self.__callStack.pop(-1)
294
295 def isCallTraceEnabled(self):
296 """
297 Public method to get the state of the call trace function.
298
299 @return flag indicating the state of the call trace function
300 @rtype bool
301 """
302 return self.__callTraceEnabled
303
304 @pyqtSlot(str, int, str, bool, str)
305 def __clientExit(self, program, status, message, quiet, debuggerId):
306 """
307 Private slot to handle a debug client terminating.
308
309 @param program name of the exited program
310 @type str
311 @param status exit code of the debugged program
312 @type int
313 @param message exit message of the debugged program
314 @type str
315 @param quiet flag indicating to suppress exit info display
316 @type bool
317 @param debuggerId ID of the debugger backend
318 @type str
319 """
320 if debuggerId == self.__tracedDebuggerId:
321 if self.stopCheckBox.isChecked():
322 self.__setCallTraceEnabled(False)
323 self.__tracedDebuggerId = ""

eric ide

mercurial