|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2009 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing a network monitor dialog. |
|
8 """ |
|
9 |
|
10 from PyQt4.QtGui import * |
|
11 from PyQt4.QtCore import * |
|
12 from PyQt4.QtNetwork import QNetworkRequest, QNetworkAccessManager |
|
13 |
|
14 import UI.PixmapCache |
|
15 |
|
16 from E4NetworkHeaderDetailsDialog import E4NetworkHeaderDetailsDialog |
|
17 |
|
18 from Ui_E4NetworkMonitor import Ui_E4NetworkMonitor |
|
19 |
|
20 class E4NetworkRequest(object): |
|
21 """ |
|
22 Class for storing all data related to a specific request. |
|
23 """ |
|
24 def __init__(self): |
|
25 """ |
|
26 Constructor |
|
27 """ |
|
28 self.op = -1 |
|
29 self.request = None |
|
30 self.reply = None |
|
31 |
|
32 self.response = "" |
|
33 self.length = 0 |
|
34 self.contentType = "" |
|
35 self.info = "" |
|
36 self.replyHeaders = [] # list of tuple of two items |
|
37 |
|
38 class E4NetworkMonitor(QDialog, Ui_E4NetworkMonitor): |
|
39 """ |
|
40 Class implementing a network monitor dialog. |
|
41 """ |
|
42 _monitor = None |
|
43 |
|
44 @classmethod |
|
45 def instance(cls, networkAccessManager): |
|
46 """ |
|
47 Class method to get a reference to our singleton. |
|
48 |
|
49 @param networkAccessManager reference to the network access manager |
|
50 (QNetworkAccessManager) |
|
51 """ |
|
52 if cls._monitor is None: |
|
53 cls._monitor = E4NetworkMonitor(networkAccessManager) |
|
54 cls._monitor.setAttribute(Qt.WA_DeleteOnClose, True) |
|
55 |
|
56 return cls._monitor |
|
57 |
|
58 @classmethod |
|
59 def closeMonitor(cls): |
|
60 """ |
|
61 Class method to close the monitor dialog. |
|
62 """ |
|
63 if cls._monitor is not None: |
|
64 cls._monitor.close() |
|
65 |
|
66 def __init__(self, networkAccessManager, parent = None): |
|
67 """ |
|
68 Constructor |
|
69 |
|
70 @param networkAccessManager reference to the network access manager |
|
71 (QNetworkAccessManager) |
|
72 @param parent reference to the parent widget (QWidget) |
|
73 """ |
|
74 QDialog.__init__(self, parent) |
|
75 self.setupUi(self) |
|
76 |
|
77 self.clearButton.setIcon(UI.PixmapCache.getIcon("clearLeft.png")) |
|
78 |
|
79 self.__requestHeaders = QStandardItemModel(self) |
|
80 self.__requestHeaders.setHorizontalHeaderLabels( |
|
81 [self.trUtf8("Name"), self.trUtf8("Value")]) |
|
82 self.requestHeadersList.setModel(self.__requestHeaders) |
|
83 self.requestHeadersList.horizontalHeader().setStretchLastSection(True) |
|
84 self.connect(self.requestHeadersList, |
|
85 SIGNAL("doubleClicked(const QModelIndex&)"), |
|
86 self.__showHeaderDetails) |
|
87 |
|
88 self.__replyHeaders = QStandardItemModel(self) |
|
89 self.__replyHeaders.setHorizontalHeaderLabels( |
|
90 [self.trUtf8("Name"), self.trUtf8("Value")]) |
|
91 self.responseHeadersList.setModel(self.__replyHeaders) |
|
92 self.responseHeadersList.horizontalHeader().setStretchLastSection(True) |
|
93 self.connect(self.responseHeadersList, |
|
94 SIGNAL("doubleClicked(const QModelIndex&)"), |
|
95 self.__showHeaderDetails) |
|
96 |
|
97 self.requestsList.horizontalHeader().setStretchLastSection(True) |
|
98 self.requestsList.verticalHeader().setMinimumSectionSize(-1) |
|
99 |
|
100 self.__proxyModel = QSortFilterProxyModel(self) |
|
101 self.__proxyModel.setFilterKeyColumn(-1) |
|
102 self.connect(self.searchEdit, SIGNAL("textChanged(QString)"), |
|
103 self.__proxyModel.setFilterFixedString) |
|
104 |
|
105 self.connect(self.removeButton, SIGNAL("clicked()"), |
|
106 self.requestsList.removeSelected) |
|
107 self.connect(self.removeAllButton, SIGNAL("clicked()"), |
|
108 self.requestsList.removeAll) |
|
109 |
|
110 self.__model = E4RequestModel(networkAccessManager, self) |
|
111 self.__proxyModel.setSourceModel(self.__model) |
|
112 self.requestsList.setModel(self.__proxyModel) |
|
113 self.connect(self.requestsList.selectionModel(), |
|
114 SIGNAL("currentChanged(const QModelIndex&, const QModelIndex&)"), |
|
115 self.__currentChanged) |
|
116 |
|
117 fm = self.fontMetrics() |
|
118 em = fm.width("m") |
|
119 self.requestsList.horizontalHeader().resizeSection(0, em * 5) |
|
120 self.requestsList.horizontalHeader().resizeSection(1, em * 20) |
|
121 self.requestsList.horizontalHeader().resizeSection(3, em * 5) |
|
122 self.requestsList.horizontalHeader().resizeSection(4, em * 15) |
|
123 |
|
124 self.__headersDlg = None |
|
125 |
|
126 def closeEvent(self, evt): |
|
127 """ |
|
128 Protected method called upon closing the dialog. |
|
129 |
|
130 @param evt reference to the close event object (QCloseEvent) |
|
131 """ |
|
132 self.deleteLater() |
|
133 self.__class__._monitor = None |
|
134 QDialog.closeEvent(self, evt) |
|
135 |
|
136 def reject(self): |
|
137 """ |
|
138 Public slot to close the dialog with a Reject status. |
|
139 """ |
|
140 self.__class__._monitor = None |
|
141 QDialog.reject(self) |
|
142 |
|
143 def __currentChanged(self, current, previous): |
|
144 """ |
|
145 Private slot to handle a change of the current index. |
|
146 |
|
147 @param current new current index (QModelIndex) |
|
148 @param previous old current index (QModelIndex) |
|
149 """ |
|
150 self.__requestHeaders.setRowCount(0) |
|
151 self.__replyHeaders.setRowCount(0) |
|
152 |
|
153 if not current.isValid(): |
|
154 return |
|
155 |
|
156 row = self.__proxyModel.mapToSource(current).row() |
|
157 |
|
158 req = self.__model.requests[row].request |
|
159 |
|
160 for header in req.rawHeaderList(): |
|
161 self.__requestHeaders.insertRows(0, 1, QModelIndex()) |
|
162 self.__requestHeaders.setData( |
|
163 self.__requestHeaders.index(0, 0), |
|
164 QVariant(header)) |
|
165 self.__requestHeaders.setData( |
|
166 self.__requestHeaders.index(0, 1), |
|
167 QVariant(req.rawHeader(header))) |
|
168 self.__requestHeaders.item(0, 0).setFlags( |
|
169 Qt.ItemIsSelectable | Qt.ItemIsEnabled) |
|
170 self.__requestHeaders.item(0, 1).setFlags( |
|
171 Qt.ItemIsSelectable | Qt.ItemIsEnabled) |
|
172 |
|
173 for header in self.__model.requests[row].replyHeaders: |
|
174 self.__replyHeaders.insertRows(0, 1, QModelIndex()) |
|
175 self.__replyHeaders.setData( |
|
176 self.__replyHeaders.index(0, 0), |
|
177 QVariant(header[0])) |
|
178 self.__replyHeaders.setData( |
|
179 self.__replyHeaders.index(0, 1), |
|
180 QVariant(header[1])) |
|
181 self.__replyHeaders.item(0, 0).setFlags( |
|
182 Qt.ItemIsSelectable | Qt.ItemIsEnabled) |
|
183 self.__replyHeaders.item(0, 1).setFlags( |
|
184 Qt.ItemIsSelectable | Qt.ItemIsEnabled) |
|
185 |
|
186 def __showHeaderDetails(self, index): |
|
187 """ |
|
188 Private slot to show a dialog with the header details. |
|
189 |
|
190 @param index index of the entry to show (QModelIndex) |
|
191 """ |
|
192 if not index.isValid(): |
|
193 return |
|
194 |
|
195 headerList = self.sender() |
|
196 if headerList is None: |
|
197 return |
|
198 |
|
199 row = index.row() |
|
200 name = headerList.model().data(headerList.model().index(row, 0)).toString() |
|
201 value = headerList.model().data(headerList.model().index(row, 1)).toString() |
|
202 if self.__headersDlg is None: |
|
203 self.__headersDlg = E4NetworkHeaderDetailsDialog(self) |
|
204 self.__headersDlg.setData(name, value) |
|
205 self.__headersDlg.show() |
|
206 |
|
207 class E4RequestModel(QAbstractTableModel): |
|
208 """ |
|
209 Class implementing a model storing request objects. |
|
210 """ |
|
211 def __init__(self, networkAccessManager, parent = None): |
|
212 """ |
|
213 Constructor |
|
214 |
|
215 @param networkAccessManager reference to the network access manager |
|
216 (QNetworkAccessManager) |
|
217 @param parent reference to the parent object (QObject) |
|
218 """ |
|
219 QAbstractTableModel.__init__(self, parent) |
|
220 |
|
221 self.__headerData = [ |
|
222 self.trUtf8("Method"), |
|
223 self.trUtf8("Address"), |
|
224 self.trUtf8("Response"), |
|
225 self.trUtf8("Length"), |
|
226 self.trUtf8("Content Type"), |
|
227 self.trUtf8("Info"), |
|
228 ] |
|
229 |
|
230 self.__operations = { |
|
231 QNetworkAccessManager.HeadOperation : "HEAD", |
|
232 QNetworkAccessManager.GetOperation : "GET", |
|
233 QNetworkAccessManager.PutOperation : "PUT", |
|
234 QNetworkAccessManager.PostOperation : "POST", |
|
235 } |
|
236 |
|
237 self.requests = [] |
|
238 self.connect(networkAccessManager, |
|
239 SIGNAL("requestCreated(QNetworkAccessManager::Operation, const QNetworkRequest&, QNetworkReply*)"), |
|
240 self.__requestCreated) |
|
241 |
|
242 def __requestCreated(self, operation, request, reply): |
|
243 """ |
|
244 Private slot handling the creation of a network request. |
|
245 |
|
246 @param operation network operation (QNetworkAccessManager.Operation) |
|
247 @param request reference to the request object (QNetworkRequest) |
|
248 @param reply reference to the reply object(QNetworkReply) |
|
249 """ |
|
250 req = E4NetworkRequest() |
|
251 req.op = operation |
|
252 req.request = QNetworkRequest(request) |
|
253 req.reply = reply |
|
254 self.__addRequest(req) |
|
255 |
|
256 def __addRequest(self, req): |
|
257 """ |
|
258 Private method to add a request object to the model. |
|
259 |
|
260 @param req reference to the request object (E4NetworkRequest) |
|
261 """ |
|
262 self.beginInsertRows(QModelIndex(), len(self.requests), len(self.requests)) |
|
263 self.requests.append(req) |
|
264 self.connect(req.reply, SIGNAL("finished()"), self.__addReply) |
|
265 self.endInsertRows() |
|
266 |
|
267 def __addReply(self): |
|
268 """ |
|
269 Private slot to add the reply data to the model. |
|
270 """ |
|
271 reply = self.sender() |
|
272 if reply is None: |
|
273 return |
|
274 |
|
275 offset = len(self.requests) - 1 |
|
276 while offset >= 0: |
|
277 if self.requests[offset].reply is reply: |
|
278 break |
|
279 offset -= 1 |
|
280 if offset < 0: |
|
281 return |
|
282 |
|
283 # save the reply header data |
|
284 for header in reply.rawHeaderList(): |
|
285 self.requests[offset].replyHeaders.append((header, reply.rawHeader(header))) |
|
286 |
|
287 # save reply info to be displayed |
|
288 status = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute).toInt()[0] |
|
289 reason = reply.attribute(QNetworkRequest.HttpReasonPhraseAttribute).toString() |
|
290 self.requests[offset].response = "%d %s" % (status, reason) |
|
291 self.requests[offset].length = \ |
|
292 reply.header(QNetworkRequest.ContentLengthHeader).toInt()[0] |
|
293 self.requests[offset].contentType = \ |
|
294 reply.header(QNetworkRequest.ContentTypeHeader).toString() |
|
295 |
|
296 if status == 302: |
|
297 target = reply.attribute(QNetworkRequest.RedirectionTargetAttribute).toUrl() |
|
298 self.requests[offset].info = \ |
|
299 self.trUtf8("Redirect: {0}").format(target.toString()) |
|
300 |
|
301 def headerData(self, section, orientation, role): |
|
302 """ |
|
303 Public method to get header data from the model. |
|
304 |
|
305 @param section section number (integer) |
|
306 @param orientation orientation (Qt.Orientation) |
|
307 @param role role of the data to retrieve (integer) |
|
308 @return requested data |
|
309 """ |
|
310 if orientation == Qt.Horizontal and role == Qt.DisplayRole: |
|
311 return QVariant(self.__headerData[section]) |
|
312 |
|
313 return QAbstractTableModel.headerData(self, section, orientation, role) |
|
314 |
|
315 def data(self, index, role): |
|
316 """ |
|
317 Public method to get data from the model. |
|
318 |
|
319 @param index index to get data for (QModelIndex) |
|
320 @param role role of the data to retrieve (integer) |
|
321 @return requested data |
|
322 """ |
|
323 if index.row() < 0 or index.row() >= len(self.requests): |
|
324 return QVariant() |
|
325 |
|
326 if role == Qt.DisplayRole or role == Qt.EditRole: |
|
327 col = index.column() |
|
328 if col == 0: |
|
329 try: |
|
330 return QVariant(self.__operations[self.requests[index.row()].op]) |
|
331 except KeyError: |
|
332 return QVariant(self.trUtf8("Unknown")) |
|
333 elif col == 1: |
|
334 return QVariant(self.requests[index.row()].request.url().toEncoded()) |
|
335 elif col == 2: |
|
336 return QVariant(self.requests[index.row()].response) |
|
337 elif col == 3: |
|
338 return QVariant(self.requests[index.row()].length) |
|
339 elif col == 4: |
|
340 return QVariant(self.requests[index.row()].contentType) |
|
341 elif col == 5: |
|
342 return QVariant(self.requests[index.row()].info) |
|
343 |
|
344 return QVariant() |
|
345 |
|
346 def columnCount(self, parent): |
|
347 """ |
|
348 Public method to get the number of columns of the model. |
|
349 |
|
350 @param parent parent index (QModelIndex) |
|
351 @return number of columns (integer) |
|
352 """ |
|
353 if parent.column() > 0: |
|
354 return 0 |
|
355 else: |
|
356 return len(self.__headerData) |
|
357 |
|
358 def rowCount(self, parent): |
|
359 """ |
|
360 Public method to get the number of rows of the model. |
|
361 |
|
362 @param parent parent index (QModelIndex) |
|
363 @return number of columns (integer) |
|
364 """ |
|
365 if parent.isValid(): |
|
366 return 0 |
|
367 else: |
|
368 return len(self.requests) |
|
369 |
|
370 def removeRows(self, row, count, parent): |
|
371 """ |
|
372 Public method to remove entries from the model. |
|
373 |
|
374 @param row start row (integer) |
|
375 @param count number of rows to remove (integer) |
|
376 @param parent parent index (QModelIndex) |
|
377 @return flag indicating success (boolean) |
|
378 """ |
|
379 if parent.isValid(): |
|
380 return False |
|
381 |
|
382 lastRow = row + count - 1 |
|
383 self.beginRemoveRows(parent, row, lastRow) |
|
384 del self.requests[row:lastRow + 1] |
|
385 self.endRemoveRows() |
|
386 return True |