|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2008 - 2009 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing the download dialog. |
|
8 """ |
|
9 |
|
10 from PyQt4.QtCore import * |
|
11 from PyQt4.QtGui import * |
|
12 from PyQt4.QtNetwork import QNetworkReply, QNetworkAccessManager, QNetworkRequest |
|
13 |
|
14 import Preferences |
|
15 |
|
16 import Helpviewer.HelpWindow |
|
17 |
|
18 from Ui_DownloadDialog import Ui_DownloadDialog |
|
19 |
|
20 class DownloadDialog(QWidget, Ui_DownloadDialog): |
|
21 """ |
|
22 Class implementing the download dialog. |
|
23 |
|
24 @signal done() emitted just before the dialog is closed |
|
25 """ |
|
26 done = pyqtSignal() |
|
27 |
|
28 def __init__(self, reply = None, requestFilename = False, webPage = None, |
|
29 download = False, parent = None): |
|
30 """ |
|
31 Constructor |
|
32 |
|
33 @param reply reference to the network reply object (QNetworkReply) |
|
34 @param requestFilename flag indicating to ask the user for a filename (boolean) |
|
35 @param webPage reference to the web page object the download originated |
|
36 from (QWebPage) |
|
37 @param download flag indicating a download operation (boolean) |
|
38 @param parent reference to the parent widget (QWidget) |
|
39 """ |
|
40 QWidget.__init__(self, parent) |
|
41 self.setupUi(self) |
|
42 self.setAttribute(Qt.WA_DeleteOnClose) |
|
43 |
|
44 self.__tryAgainButton = \ |
|
45 self.buttonBox.addButton(self.trUtf8("Try Again"), |
|
46 QDialogButtonBox.ActionRole) |
|
47 self.__stopButton = \ |
|
48 self.buttonBox.addButton(self.trUtf8("Stop"), QDialogButtonBox.ActionRole) |
|
49 self.__openButton = self.buttonBox.button(QDialogButtonBox.Open) |
|
50 self.__closeButton = self.buttonBox.button(QDialogButtonBox.Close) |
|
51 |
|
52 self.__tryAgainButton.setEnabled(False) |
|
53 self.__closeButton.setEnabled(False) |
|
54 self.__openButton.setEnabled(False) |
|
55 self.keepOpenCheckBox.setChecked(True) |
|
56 |
|
57 icon = self.style().standardIcon(QStyle.SP_FileIcon) |
|
58 self.fileIcon.setPixmap(icon.pixmap(48, 48)) |
|
59 |
|
60 self.__reply = reply |
|
61 self.__requestFilename = requestFilename |
|
62 self.__page = webPage |
|
63 self.__toDownload = download |
|
64 self.__bytesReceived = 0 |
|
65 self.__downloadTime = QTime() |
|
66 self.__output = QFile() |
|
67 |
|
68 self.__initialize() |
|
69 |
|
70 def __initialize(self): |
|
71 """ |
|
72 Private method to (re)initialize the dialog. |
|
73 """ |
|
74 if self.__reply is None: |
|
75 return |
|
76 |
|
77 self.__startedSaving = False |
|
78 self.__downloadFinished = False |
|
79 |
|
80 self.__url = self.__reply.url() |
|
81 self.__reply.setParent(self) |
|
82 self.connect(self.__reply, SIGNAL("readyRead()"), self.__readyRead) |
|
83 self.connect(self.__reply, SIGNAL("error(QNetworkReply::NetworkError)"), |
|
84 self.__networkError) |
|
85 self.connect(self.__reply, SIGNAL("downloadProgress(qint64, qint64)"), |
|
86 self.__downloadProgress) |
|
87 self.connect(self.__reply, SIGNAL("metaDataChanged()"), |
|
88 self.__metaDataChanged) |
|
89 self.connect(self.__reply, SIGNAL("finished()"), self.__finished) |
|
90 |
|
91 # reset info |
|
92 self.infoLabel.clear() |
|
93 self.progressBar.setValue(0) |
|
94 self.__getFileName() |
|
95 |
|
96 # start timer for the download estimation |
|
97 self.__downloadTime.start() |
|
98 |
|
99 if self.__reply.error() != QNetworkReply.NoError: |
|
100 self.__networkError() |
|
101 self.__finished() |
|
102 |
|
103 def __getFileName(self): |
|
104 """ |
|
105 Private method to get the filename to save to from the user. |
|
106 """ |
|
107 downloadDirectory = Preferences.getUI("DownloadPath") |
|
108 if not downloadDirectory: |
|
109 downloadDirectory = \ |
|
110 QDesktopServices.storageLocation(QDesktopServices.DocumentsLocation) |
|
111 if downloadDirectory: |
|
112 downloadDirectory += '/' |
|
113 |
|
114 defaultFileName = self.__saveFileName(downloadDirectory) |
|
115 fileName = defaultFileName |
|
116 baseName = QFileInfo(fileName).completeBaseName() |
|
117 self.__autoOpen = False |
|
118 if not self.__toDownload: |
|
119 res = QMessageBox.question(None, |
|
120 self.trUtf8("Downloading"), |
|
121 self.trUtf8("""<p>You are about to download the file <b>{0}</b>.</p>""" |
|
122 """<p>What do you want to do?</p>""").format(baseName), |
|
123 QMessageBox.StandardButtons(\ |
|
124 QMessageBox.Open | \ |
|
125 QMessageBox.Save)) |
|
126 self.__autoOpen = res == QMessageBox.Open |
|
127 fileName = QDesktopServices.storageLocation(QDesktopServices.TempLocation) + \ |
|
128 '/' + baseName |
|
129 |
|
130 if not self.__autoOpen and self.__requestFilename: |
|
131 fileName = QFileDialog.getSaveFileName( |
|
132 None, |
|
133 self.trUtf8("Save File"), |
|
134 defaultFileName, |
|
135 "") |
|
136 if not fileName: |
|
137 self.__reply.close() |
|
138 if not self.keepOpenCheckBox.isChecked(): |
|
139 self.close() |
|
140 else: |
|
141 self.filenameLabel.setText(self.trUtf8("Download canceled: {0}")\ |
|
142 .format(QFileInfo(defaultFileName).fileName())) |
|
143 self.__stop() |
|
144 return |
|
145 |
|
146 self.__output.setFileName(fileName) |
|
147 self.filenameLabel.setText(QFileInfo(self.__output.fileName()).fileName()) |
|
148 if self.__requestFilename: |
|
149 self.__readyRead() |
|
150 |
|
151 def __saveFileName(self, directory): |
|
152 """ |
|
153 Private method to calculate a name for the file to download. |
|
154 |
|
155 @param directory name of the directory to store the file into (string) |
|
156 @return proposed filename (string) |
|
157 """ |
|
158 path = "" |
|
159 if self.__reply.hasRawHeader("Content-Disposition"): |
|
160 header = unicode(self.__reply.rawHeader("Content-Disposition")) |
|
161 if header: |
|
162 pos = header.find("filename=") |
|
163 if pos != -1: |
|
164 path = header[pos + 9:] |
|
165 if path.startswith('"') and path.endswith('"'): |
|
166 path = path[1:-1] |
|
167 if not path: |
|
168 path = self.__url.path() |
|
169 |
|
170 info = QFileInfo(path) |
|
171 baseName = info.completeBaseName() |
|
172 endName = info.suffix() |
|
173 |
|
174 if not baseName: |
|
175 baseName = "unnamed_download" |
|
176 |
|
177 name = directory + baseName |
|
178 if endName: |
|
179 name += '.' + endName |
|
180 i = 1 |
|
181 while QFile.exists(name): |
|
182 # file exists already, don't overwrite |
|
183 name = directory + baseName + ('-%d' % i) |
|
184 if endName: |
|
185 name += '.' + endName |
|
186 i += 1 |
|
187 return name |
|
188 |
|
189 @pyqtSlot(QAbstractButton) |
|
190 def on_buttonBox_clicked(self, button): |
|
191 """ |
|
192 Private slot called by a button of the button box clicked. |
|
193 |
|
194 @param button button that was clicked (QAbstractButton) |
|
195 """ |
|
196 if button == self.__closeButton: |
|
197 self.close() |
|
198 elif button == self.__openButton: |
|
199 self.__open() |
|
200 elif button == self.__stopButton: |
|
201 self.__stop() |
|
202 elif button == self.__tryAgainButton: |
|
203 self.__tryAgain() |
|
204 |
|
205 def __stop(self): |
|
206 """ |
|
207 Private slot to stop the download. |
|
208 """ |
|
209 self.__stopButton.setEnabled(False) |
|
210 self.__closeButton.setEnabled(True) |
|
211 self.__tryAgainButton.setEnabled(True) |
|
212 self.__reply.abort() |
|
213 |
|
214 def __open(self): |
|
215 """ |
|
216 Private slot to open the downloaded file. |
|
217 """ |
|
218 info = QFileInfo(self.__output) |
|
219 url = QUrl.fromLocalFile(info.absoluteFilePath()) |
|
220 QDesktopServices.openUrl(url) |
|
221 |
|
222 def __tryAgain(self): |
|
223 """ |
|
224 Private slot to retry the download. |
|
225 """ |
|
226 self.__tryAgainButton.setEnabled(False) |
|
227 self.__closeButton.setEnabled(False) |
|
228 self.__stopButton.setEnabled(True) |
|
229 |
|
230 if self.__page: |
|
231 nam = self.__page.networkAccessManager() |
|
232 else: |
|
233 nam = QNetworkAccessManager() |
|
234 reply = nam.get(QNetworkRequest(self.__url)) |
|
235 if self.__reply: |
|
236 self.__reply.deleteLater() |
|
237 if self.__output.exists(): |
|
238 self.__output.remove() |
|
239 self.__reply = reply |
|
240 self.__initialize() |
|
241 |
|
242 def __readyRead(self): |
|
243 """ |
|
244 Private slot to read the available data. |
|
245 """ |
|
246 if self.__requestFilename and not self.__output.fileName(): |
|
247 return |
|
248 |
|
249 if not self.__output.isOpen(): |
|
250 # in case someone else has already put a file there |
|
251 if not self.__requestFilename: |
|
252 self.__getFileName() |
|
253 if not self.__output.open(QIODevice.WriteOnly): |
|
254 self.infoLabel.setText(self.trUtf8("Error opening save file: {0}")\ |
|
255 .format(self.__output.errorString())) |
|
256 self.__stopButton.click() |
|
257 return |
|
258 |
|
259 bytesWritten = self.__output.write(self.__reply.readAll()) |
|
260 if bytesWritten == -1: |
|
261 self.infoLabel.setText(self.trUtf8("Error saving: {0}")\ |
|
262 .format(self.__output.errorString())) |
|
263 self.__stopButton.click() |
|
264 else: |
|
265 size = self.__reply.header(QNetworkRequest.ContentLengthHeader).toInt()[0] |
|
266 if size == bytesWritten: |
|
267 self.__downloadProgress(size, size) |
|
268 self.__downloadFinished = True |
|
269 self.__startedSaving = True |
|
270 if self.__downloadFinished: |
|
271 self.__finished() |
|
272 |
|
273 def __networkError(self): |
|
274 """ |
|
275 Private slot to handle a network error. |
|
276 """ |
|
277 if self.__reply.error() != QNetworkReply.OperationCanceledError: |
|
278 self.infoLabel.setText(self.trUtf8("Network Error: {0}")\ |
|
279 .format(self.__reply.errorString())) |
|
280 self.__tryAgainButton.setEnabled(True) |
|
281 self.__closeButton.setEnabled(True) |
|
282 self.__openButton.setEnabled(False) |
|
283 |
|
284 def __metaDataChanged(self): |
|
285 """ |
|
286 Private slot to handle a change of the meta data. |
|
287 """ |
|
288 locationHeader = self.__reply.header(QNetworkRequest.LocationHeader) |
|
289 if locationHeader.isValid(): |
|
290 self.__url = locationHeader.toUrl() |
|
291 self.__reply.deleteLater() |
|
292 self.__reply = Helpviewer.HelpWindow.HelpWindow.networkAccessManager().get( |
|
293 QNetworkRequest(self.__url)) |
|
294 self.__initialize() |
|
295 |
|
296 def __downloadProgress(self, received, total): |
|
297 """ |
|
298 Private method show the download progress. |
|
299 |
|
300 @param received number of bytes received (integer) |
|
301 @param total number of total bytes (integer) |
|
302 """ |
|
303 self.__bytesReceived = received |
|
304 if total == -1: |
|
305 self.progressBar.setValue(0) |
|
306 self.progressBar.setMaximum(0) |
|
307 else: |
|
308 self.progressBar.setValue(received) |
|
309 self.progressBar.setMaximum(total) |
|
310 self.__updateInfoLabel() |
|
311 |
|
312 def __updateInfoLabel(self): |
|
313 """ |
|
314 Private method to update the info label. |
|
315 """ |
|
316 if self.__reply.error() != QNetworkReply.NoError and \ |
|
317 self.__reply.error() != QNetworkReply.OperationCanceledError: |
|
318 return |
|
319 |
|
320 bytesTotal = self.progressBar.maximum() |
|
321 running = not self.__downloadedSuccessfully() |
|
322 |
|
323 info = "" |
|
324 if self.__downloading(): |
|
325 remaining = "" |
|
326 speed = self.__bytesReceived * 1000.0 / self.__downloadTime.elapsed() |
|
327 if bytesTotal != 0: |
|
328 timeRemaining = int((bytesTotal - self.__bytesReceived) / speed) |
|
329 |
|
330 if timeRemaining > 60: |
|
331 minutes = int(timeRemaining / 60) |
|
332 seconds = int(timeRemaining % 60) |
|
333 remaining = self.trUtf8("- {0}:{1:02} minutes remaining")\ |
|
334 .format(minutes, seconds) |
|
335 else: |
|
336 # when downloading, the eta should never be 0 |
|
337 if timeRemaining == 0: |
|
338 timeRemaining = 1 |
|
339 |
|
340 remaining = self.trUtf8("- {0} seconds remaining")\ |
|
341 .format(timeRemaining) |
|
342 info = self.trUtf8("{0} of {1} ({2}/sec) {3}")\ |
|
343 .format( |
|
344 self.__dataString(self.__bytesReceived), |
|
345 bytesTotal == 0 and self.trUtf8("?") \ |
|
346 or self.__dataString(bytesTotal), |
|
347 self.__dataString(int(speed)), |
|
348 remaining) |
|
349 else: |
|
350 if self.__bytesReceived == bytesTotal: |
|
351 info = self.trUtf8("{0} downloaded")\ |
|
352 .format(self.__dataString(self.__output.size())) |
|
353 else: |
|
354 info = self.trUtf8("{0} of {1} - Stopped")\ |
|
355 .format(self.__dataString(self.__bytesReceived), |
|
356 self.__dataString(bytesTotal)) |
|
357 self.infoLabel.setText(info) |
|
358 |
|
359 def __dataString(self, size): |
|
360 """ |
|
361 Private method to generate a formatted data string. |
|
362 |
|
363 @param size size to be formatted (integer) |
|
364 @return formatted data string (QString) |
|
365 """ |
|
366 unit = "" |
|
367 if size < 1024: |
|
368 unit = self.trUtf8("bytes") |
|
369 elif size < 1024 * 1024: |
|
370 size /= 1024 |
|
371 unit = self.trUtf8("kB") |
|
372 else: |
|
373 size /= 1024 * 1024 |
|
374 unit = self.trUtf8("MB") |
|
375 return "{0} {1}".format(size, unit) |
|
376 |
|
377 def __downloading(self): |
|
378 """ |
|
379 Private method to determine, if a download is in progress. |
|
380 |
|
381 @return flag indicating a download is in progress (boolean) |
|
382 """ |
|
383 return self.__stopButton.isEnabled() |
|
384 |
|
385 def __downloadedSuccessfully(self): |
|
386 """ |
|
387 Private method to determine the download status. |
|
388 |
|
389 @return download status (boolean) |
|
390 """ |
|
391 return (not self.__stopButton.isEnabled() and \ |
|
392 not self.__tryAgainButton.isEnabled()) |
|
393 |
|
394 def __finished(self): |
|
395 """ |
|
396 Private slot to handle the download finished. |
|
397 """ |
|
398 self.__downloadFinished = True |
|
399 if not self.__startedSaving: |
|
400 return |
|
401 |
|
402 self.__stopButton.setEnabled(False) |
|
403 self.__closeButton.setEnabled(True) |
|
404 self.__openButton.setEnabled(True) |
|
405 self.__output.close() |
|
406 self.__updateInfoLabel() |
|
407 |
|
408 if not self.keepOpenCheckBox.isChecked() and \ |
|
409 self.__reply.error() == QNetworkReply.NoError: |
|
410 self.close() |
|
411 |
|
412 if self.__autoOpen: |
|
413 self.__open() |
|
414 |
|
415 def closeEvent(self, evt): |
|
416 """ |
|
417 Protected method called when the dialog is closed. |
|
418 """ |
|
419 self.__output.close() |
|
420 self.done.emit() |