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