|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2010 - 2019 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing the download manager class. |
|
8 """ |
|
9 |
|
10 from __future__ import unicode_literals |
|
11 |
|
12 from PyQt5.QtCore import pyqtSlot, Qt, QModelIndex, QFileInfo |
|
13 from PyQt5.QtGui import QCursor |
|
14 from PyQt5.QtWidgets import QDialog, QStyle, QFileIconProvider, QMenu, \ |
|
15 QApplication |
|
16 from PyQt5.QtNetwork import QNetworkRequest |
|
17 from PyQt5.QtWebKit import QWebSettings |
|
18 |
|
19 from E5Gui import E5MessageBox |
|
20 |
|
21 from .Ui_DownloadManager import Ui_DownloadManager |
|
22 |
|
23 from .DownloadModel import DownloadModel |
|
24 |
|
25 import Helpviewer.HelpWindow |
|
26 |
|
27 from Utilities.AutoSaver import AutoSaver |
|
28 import UI.PixmapCache |
|
29 import Preferences |
|
30 |
|
31 |
|
32 class DownloadManager(QDialog, Ui_DownloadManager): |
|
33 """ |
|
34 Class implementing the download manager. |
|
35 """ |
|
36 RemoveNever = 0 |
|
37 RemoveExit = 1 |
|
38 RemoveSuccessFullDownload = 2 |
|
39 |
|
40 def __init__(self, parent=None): |
|
41 """ |
|
42 Constructor |
|
43 |
|
44 @param parent reference to the parent widget (QWidget) |
|
45 """ |
|
46 super(DownloadManager, self).__init__(parent) |
|
47 self.setupUi(self) |
|
48 self.setWindowFlags(Qt.Window) |
|
49 |
|
50 self.__saveTimer = AutoSaver(self, self.save) |
|
51 |
|
52 self.__model = DownloadModel(self) |
|
53 self.__manager = Helpviewer.HelpWindow.HelpWindow\ |
|
54 .networkAccessManager() |
|
55 |
|
56 self.__iconProvider = None |
|
57 self.__downloads = [] |
|
58 self.__downloadDirectory = "" |
|
59 self.__loaded = False |
|
60 |
|
61 self.__rowHeightMultiplier = 1.1 |
|
62 |
|
63 self.setDownloadDirectory(Preferences.getUI("DownloadPath")) |
|
64 |
|
65 self.downloadsView.setShowGrid(False) |
|
66 self.downloadsView.verticalHeader().hide() |
|
67 self.downloadsView.horizontalHeader().hide() |
|
68 self.downloadsView.setAlternatingRowColors(True) |
|
69 self.downloadsView.horizontalHeader().setStretchLastSection(True) |
|
70 self.downloadsView.setModel(self.__model) |
|
71 self.downloadsView.setContextMenuPolicy(Qt.CustomContextMenu) |
|
72 self.downloadsView.customContextMenuRequested.connect( |
|
73 self.__customContextMenuRequested) |
|
74 |
|
75 self.__load() |
|
76 |
|
77 def __customContextMenuRequested(self, pos): |
|
78 """ |
|
79 Private slot to handle the context menu request for the bookmarks tree. |
|
80 |
|
81 @param pos position the context menu was requested (QPoint) |
|
82 """ |
|
83 menu = QMenu() |
|
84 |
|
85 selectedRowsCount = len( |
|
86 self.downloadsView.selectionModel().selectedRows()) |
|
87 |
|
88 if selectedRowsCount == 1: |
|
89 row = self.downloadsView.selectionModel().selectedRows()[0].row() |
|
90 itm = self.__downloads[row] |
|
91 if itm.downloadCanceled(): |
|
92 menu.addAction( |
|
93 UI.PixmapCache.getIcon("restart.png"), |
|
94 self.tr("Retry"), self.__contextMenuRetry) |
|
95 else: |
|
96 if itm.downloadedSuccessfully(): |
|
97 menu.addAction( |
|
98 UI.PixmapCache.getIcon("open.png"), |
|
99 self.tr("Open"), self.__contextMenuOpen) |
|
100 elif itm.downloading(): |
|
101 menu.addAction( |
|
102 UI.PixmapCache.getIcon("stopLoading.png"), |
|
103 self.tr("Cancel"), self.__contextMenuCancel) |
|
104 menu.addSeparator() |
|
105 menu.addAction( |
|
106 self.tr("Open Containing Folder"), |
|
107 self.__contextMenuOpenFolder) |
|
108 menu.addSeparator() |
|
109 menu.addAction( |
|
110 self.tr("Go to Download Page"), |
|
111 self.__contextMenuGotoPage) |
|
112 menu.addAction( |
|
113 self.tr("Copy Download Link"), |
|
114 self.__contextMenuCopyLink) |
|
115 menu.addSeparator() |
|
116 menu.addAction(self.tr("Select All"), self.__contextMenuSelectAll) |
|
117 if selectedRowsCount > 1 or \ |
|
118 (selectedRowsCount == 1 and |
|
119 not self.__downloads[ |
|
120 self.downloadsView.selectionModel().selectedRows()[0].row()] |
|
121 .downloading()): |
|
122 menu.addSeparator() |
|
123 menu.addAction( |
|
124 self.tr("Remove From List"), |
|
125 self.__contextMenuRemoveSelected) |
|
126 |
|
127 menu.exec_(QCursor.pos()) |
|
128 |
|
129 def shutdown(self): |
|
130 """ |
|
131 Public method to stop the download manager. |
|
132 """ |
|
133 self.__saveTimer.changeOccurred() |
|
134 self.__saveTimer.saveIfNeccessary() |
|
135 self.close() |
|
136 |
|
137 def activeDownloads(self): |
|
138 """ |
|
139 Public method to get the number of active downloads. |
|
140 |
|
141 @return number of active downloads (integer) |
|
142 """ |
|
143 count = 0 |
|
144 |
|
145 for download in self.__downloads: |
|
146 if download.downloading(): |
|
147 count += 1 |
|
148 return count |
|
149 |
|
150 def allowQuit(self): |
|
151 """ |
|
152 Public method to check, if it is ok to quit. |
|
153 |
|
154 @return flag indicating allowance to quit (boolean) |
|
155 """ |
|
156 if self.activeDownloads() > 0: |
|
157 res = E5MessageBox.yesNo( |
|
158 self, |
|
159 self.tr(""), |
|
160 self.tr("""There are %n downloads in progress.\n""" |
|
161 """Do you want to quit anyway?""", "", |
|
162 self.activeDownloads()), |
|
163 icon=E5MessageBox.Warning) |
|
164 if not res: |
|
165 self.show() |
|
166 return False |
|
167 return True |
|
168 |
|
169 def download(self, requestOrUrl, requestFileName=False, mainWindow=None): |
|
170 """ |
|
171 Public method to download a file. |
|
172 |
|
173 @param requestOrUrl reference to a request object (QNetworkRequest) |
|
174 or a URL to be downloaded (QUrl) |
|
175 @keyparam requestFileName flag indicating to ask for the |
|
176 download file name (boolean) |
|
177 @keyparam mainWindow reference to the main window (HelpWindow) |
|
178 """ |
|
179 request = QNetworkRequest(requestOrUrl) |
|
180 if request.url().isEmpty(): |
|
181 return |
|
182 self.handleUnsupportedContent( |
|
183 self.__manager.get(request), |
|
184 requestFileName=requestFileName, |
|
185 download=True, |
|
186 mainWindow=mainWindow) |
|
187 |
|
188 def handleUnsupportedContent(self, reply, requestFileName=False, |
|
189 webPage=None, download=False, |
|
190 mainWindow=None): |
|
191 """ |
|
192 Public method to handle unsupported content by downloading the |
|
193 referenced resource. |
|
194 |
|
195 @param reply reference to the reply object (QNetworkReply) |
|
196 @keyparam requestFileName indicating to ask for a filename |
|
197 (boolean) |
|
198 @keyparam webPage reference to the web page (HelpWebPage) |
|
199 @keyparam download flag indicating a download request (boolean) |
|
200 @keyparam mainWindow reference to the main window (HelpWindow) |
|
201 """ |
|
202 if reply is None or reply.url().isEmpty(): |
|
203 return |
|
204 |
|
205 size = reply.header(QNetworkRequest.ContentLengthHeader) |
|
206 if size == 0: |
|
207 return |
|
208 |
|
209 from .DownloadItem import DownloadItem |
|
210 itm = DownloadItem( |
|
211 reply=reply, requestFilename=requestFileName, |
|
212 webPage=webPage, download=download, parent=self, |
|
213 mainWindow=mainWindow) |
|
214 self.__addItem(itm) |
|
215 |
|
216 if itm.canceledFileSelect(): |
|
217 return |
|
218 |
|
219 if not self.isVisible(): |
|
220 self.show() |
|
221 |
|
222 self.activateWindow() |
|
223 self.raise_() |
|
224 |
|
225 def __addItem(self, itm, append=False): |
|
226 """ |
|
227 Private method to add a download to the list of downloads. |
|
228 |
|
229 @param itm reference to the download item |
|
230 @type DownloadItem |
|
231 @param append flag indicating to append the item |
|
232 @type bool |
|
233 """ |
|
234 itm.statusChanged.connect(lambda: self.__updateRow(itm)) |
|
235 itm.downloadFinished.connect(self.__finished) |
|
236 |
|
237 # insert at top of window |
|
238 if append: |
|
239 row = len(self.__downloads) |
|
240 else: |
|
241 row = 0 |
|
242 self.__model.beginInsertRows(QModelIndex(), row, row) |
|
243 if append: |
|
244 self.__downloads.append(itm) |
|
245 else: |
|
246 self.__downloads.insert(0, itm) |
|
247 self.__model.endInsertRows() |
|
248 |
|
249 self.downloadsView.setIndexWidget(self.__model.index(row, 0), itm) |
|
250 icon = self.style().standardIcon(QStyle.SP_FileIcon) |
|
251 itm.setIcon(icon) |
|
252 self.downloadsView.setRowHeight( |
|
253 row, itm.sizeHint().height() * self.__rowHeightMultiplier) |
|
254 # just in case the download finished before the constructor returned |
|
255 self.__updateRow(itm) |
|
256 self.changeOccurred() |
|
257 self.__updateActiveItemCount() |
|
258 |
|
259 def __updateRow(self, itm): |
|
260 """ |
|
261 Private slot to update a download item. |
|
262 |
|
263 @param itm reference to the download item |
|
264 @type DownloadItem |
|
265 """ |
|
266 if itm not in self.__downloads: |
|
267 return |
|
268 |
|
269 row = self.__downloads.index(itm) |
|
270 |
|
271 if self.__iconProvider is None: |
|
272 self.__iconProvider = QFileIconProvider() |
|
273 |
|
274 icon = self.__iconProvider.icon(QFileInfo(itm.fileName())) |
|
275 if icon.isNull(): |
|
276 icon = self.style().standardIcon(QStyle.SP_FileIcon) |
|
277 itm.setIcon(icon) |
|
278 |
|
279 self.downloadsView.setRowHeight( |
|
280 row, |
|
281 itm.minimumSizeHint().height() * self.__rowHeightMultiplier) |
|
282 |
|
283 remove = False |
|
284 globalSettings = QWebSettings.globalSettings() |
|
285 if not itm.downloading() and \ |
|
286 globalSettings.testAttribute(QWebSettings.PrivateBrowsingEnabled): |
|
287 remove = True |
|
288 |
|
289 if itm.downloadedSuccessfully() and \ |
|
290 self.removePolicy() == DownloadManager.RemoveSuccessFullDownload: |
|
291 remove = True |
|
292 |
|
293 if remove: |
|
294 self.__model.removeRow(row) |
|
295 |
|
296 self.cleanupButton.setEnabled( |
|
297 (len(self.__downloads) - self.activeDownloads()) > 0) |
|
298 |
|
299 # record the change |
|
300 self.changeOccurred() |
|
301 |
|
302 def removePolicy(self): |
|
303 """ |
|
304 Public method to get the remove policy. |
|
305 |
|
306 @return remove policy (integer) |
|
307 """ |
|
308 return Preferences.getHelp("DownloadManagerRemovePolicy") |
|
309 |
|
310 def setRemovePolicy(self, policy): |
|
311 """ |
|
312 Public method to set the remove policy. |
|
313 |
|
314 @param policy policy to be set |
|
315 (DownloadManager.RemoveExit, DownloadManager.RemoveNever, |
|
316 DownloadManager.RemoveSuccessFullDownload) |
|
317 """ |
|
318 assert policy in (DownloadManager.RemoveExit, |
|
319 DownloadManager.RemoveNever, |
|
320 DownloadManager.RemoveSuccessFullDownload) |
|
321 |
|
322 if policy == self.removePolicy(): |
|
323 return |
|
324 |
|
325 Preferences.setHelp("DownloadManagerRemovePolicy", self.policy) |
|
326 |
|
327 def save(self): |
|
328 """ |
|
329 Public method to save the download settings. |
|
330 """ |
|
331 if not self.__loaded: |
|
332 return |
|
333 |
|
334 Preferences.setHelp("DownloadManagerSize", self.size()) |
|
335 Preferences.setHelp("DownloadManagerPosition", self.pos()) |
|
336 if self.removePolicy() == DownloadManager.RemoveExit: |
|
337 return |
|
338 |
|
339 downloads = [] |
|
340 for download in self.__downloads: |
|
341 downloads.append(download.getData()) |
|
342 Preferences.setHelp("DownloadManagerDownloads", downloads) |
|
343 |
|
344 def __load(self): |
|
345 """ |
|
346 Private method to load the download settings. |
|
347 """ |
|
348 if self.__loaded: |
|
349 return |
|
350 |
|
351 size = Preferences.getHelp("DownloadManagerSize") |
|
352 if size.isValid(): |
|
353 self.resize(size) |
|
354 pos = Preferences.getHelp("DownloadManagerPosition") |
|
355 self.move(pos) |
|
356 |
|
357 downloads = Preferences.getHelp("DownloadManagerDownloads") |
|
358 for download in downloads: |
|
359 if not download[0].isEmpty() and \ |
|
360 download[1] != "": |
|
361 from .DownloadItem import DownloadItem |
|
362 itm = DownloadItem(parent=self) |
|
363 itm.setData(download) |
|
364 self.__addItem(itm, append=True) |
|
365 self.cleanupButton.setEnabled( |
|
366 (len(self.__downloads) - self.activeDownloads()) > 0) |
|
367 |
|
368 self.__loaded = True |
|
369 self.__updateActiveItemCount() |
|
370 |
|
371 def cleanup(self): |
|
372 """ |
|
373 Public slot to cleanup the downloads. |
|
374 """ |
|
375 self.on_cleanupButton_clicked() |
|
376 |
|
377 @pyqtSlot() |
|
378 def on_cleanupButton_clicked(self): |
|
379 """ |
|
380 Private slot cleanup the downloads. |
|
381 """ |
|
382 if len(self.__downloads) == 0: |
|
383 return |
|
384 |
|
385 self.__model.removeRows(0, len(self.__downloads)) |
|
386 if len(self.__downloads) == 0 and \ |
|
387 self.__iconProvider is not None: |
|
388 self.__iconProvider = None |
|
389 |
|
390 self.changeOccurred() |
|
391 self.__updateActiveItemCount() |
|
392 |
|
393 def __updateItemCount(self): |
|
394 """ |
|
395 Private method to update the count label. |
|
396 """ |
|
397 count = len(self.__downloads) |
|
398 self.countLabel.setText(self.tr("%n Download(s)", "", count)) |
|
399 |
|
400 def __updateActiveItemCount(self): |
|
401 """ |
|
402 Private method to update the window title. |
|
403 """ |
|
404 count = self.activeDownloads() |
|
405 if count > 0: |
|
406 self.setWindowTitle( |
|
407 self.tr("Downloading %n file(s)", "", count)) |
|
408 else: |
|
409 self.setWindowTitle(self.tr("Downloads")) |
|
410 |
|
411 def __finished(self): |
|
412 """ |
|
413 Private slot to handle a finished download. |
|
414 """ |
|
415 self.__updateActiveItemCount() |
|
416 if self.isVisible(): |
|
417 QApplication.alert(self) |
|
418 |
|
419 def setDownloadDirectory(self, directory): |
|
420 """ |
|
421 Public method to set the current download directory. |
|
422 |
|
423 @param directory current download directory (string) |
|
424 """ |
|
425 self.__downloadDirectory = directory |
|
426 if self.__downloadDirectory != "": |
|
427 self.__downloadDirectory += "/" |
|
428 |
|
429 def downloadDirectory(self): |
|
430 """ |
|
431 Public method to get the current download directory. |
|
432 |
|
433 @return current download directory (string) |
|
434 """ |
|
435 return self.__downloadDirectory |
|
436 |
|
437 def count(self): |
|
438 """ |
|
439 Public method to get the number of downloads. |
|
440 |
|
441 @return number of downloads (integer) |
|
442 """ |
|
443 return len(self.__downloads) |
|
444 |
|
445 def downloads(self): |
|
446 """ |
|
447 Public method to get a reference to the downloads. |
|
448 |
|
449 @return reference to the downloads (list of DownloadItem) |
|
450 """ |
|
451 return self.__downloads |
|
452 |
|
453 def changeOccurred(self): |
|
454 """ |
|
455 Public method to signal a change. |
|
456 """ |
|
457 self.__saveTimer.changeOccurred() |
|
458 self.__updateItemCount() |
|
459 |
|
460 ########################################################################### |
|
461 ## Context menu related methods below |
|
462 ########################################################################### |
|
463 |
|
464 def __currentItem(self): |
|
465 """ |
|
466 Private method to get a reference to the current item. |
|
467 |
|
468 @return reference to the current item (DownloadItem) |
|
469 """ |
|
470 index = self.downloadsView.currentIndex() |
|
471 if index and index.isValid(): |
|
472 row = index.row() |
|
473 return self.__downloads[row] |
|
474 |
|
475 return None |
|
476 |
|
477 def __contextMenuRetry(self): |
|
478 """ |
|
479 Private method to retry of the download. |
|
480 """ |
|
481 itm = self.__currentItem() |
|
482 if itm is not None: |
|
483 itm.retry() |
|
484 |
|
485 def __contextMenuOpen(self): |
|
486 """ |
|
487 Private method to open the downloaded file. |
|
488 """ |
|
489 itm = self.__currentItem() |
|
490 if itm is not None: |
|
491 itm.openFile() |
|
492 |
|
493 def __contextMenuOpenFolder(self): |
|
494 """ |
|
495 Private method to open the folder containing the downloaded file. |
|
496 """ |
|
497 itm = self.__currentItem() |
|
498 if itm is not None: |
|
499 itm.openFolder() |
|
500 |
|
501 def __contextMenuCancel(self): |
|
502 """ |
|
503 Private method to cancel the current download. |
|
504 """ |
|
505 itm = self.__currentItem() |
|
506 if itm is not None: |
|
507 itm.cancelDownload() |
|
508 |
|
509 def __contextMenuGotoPage(self): |
|
510 """ |
|
511 Private method to open the download page. |
|
512 """ |
|
513 itm = self.__currentItem() |
|
514 if itm is not None: |
|
515 url = itm.getPageUrl() |
|
516 Helpviewer.HelpWindow.HelpWindow.mainWindow().openUrl(url, "") |
|
517 |
|
518 def __contextMenuCopyLink(self): |
|
519 """ |
|
520 Private method to copy the download link to the clipboard. |
|
521 """ |
|
522 itm = self.__currentItem() |
|
523 if itm is not None: |
|
524 url = itm.getPageUrl().toString() |
|
525 QApplication.clipboard().setText(url) |
|
526 |
|
527 def __contextMenuSelectAll(self): |
|
528 """ |
|
529 Private method to select all downloads. |
|
530 """ |
|
531 self.downloadsView.selectAll() |
|
532 |
|
533 def __contextMenuRemoveSelected(self): |
|
534 """ |
|
535 Private method to remove the selected downloads from the list. |
|
536 """ |
|
537 self.downloadsView.removeSelected() |