eric7/WebBrowser/Download/DownloadManager.py

branch
eric7
changeset 8312
800c432b34c8
parent 8235
78e6d29eb773
child 8318
962bce857696
equal deleted inserted replaced
8311:4e8b98454baa 8312:800c432b34c8
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2010 - 2021 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing the download manager class.
8 """
9
10 from PyQt5.QtCore import (
11 pyqtSlot, pyqtSignal, Qt, QModelIndex, QFileInfo, QUrl, QBasicTimer
12 )
13 from PyQt5.QtGui import QCursor, QKeySequence
14 from PyQt5.QtWidgets import (
15 QDialog, QStyle, QFileIconProvider, QMenu, QApplication, QShortcut
16 )
17
18 from E5Gui import E5MessageBox
19 from E5Gui.E5Application import e5App
20
21 from .Ui_DownloadManager import Ui_DownloadManager
22
23 from .DownloadModel import DownloadModel
24 from .DownloadUtilities import speedString, timeString
25
26 from WebBrowser.WebBrowserWindow import WebBrowserWindow
27
28 from Utilities.AutoSaver import AutoSaver
29 import UI.PixmapCache
30 import Preferences
31 import Globals
32
33
34 class DownloadManager(QDialog, Ui_DownloadManager):
35 """
36 Class implementing the download manager.
37
38 @signal downloadsCountChanged() emitted to indicate a change of the
39 count of download items
40 """
41 RemoveNever = 0
42 RemoveExit = 1
43 RemoveSuccessFullDownload = 2
44
45 UpdateTimerTimeout = 1000
46
47 downloadsCountChanged = pyqtSignal()
48
49 def __init__(self, parent=None):
50 """
51 Constructor
52
53 @param parent reference to the parent widget (QWidget)
54 """
55 super().__init__(parent)
56 self.setupUi(self)
57 self.setWindowFlags(Qt.WindowType.Window)
58
59 self.__winTaskbarButton = None
60
61 self.__saveTimer = AutoSaver(self, self.save)
62
63 self.__model = DownloadModel(self)
64 self.__manager = WebBrowserWindow.networkManager()
65
66 self.__iconProvider = None
67 self.__downloads = []
68 self.__downloadDirectory = ""
69 self.__loaded = False
70
71 self.__rowHeightMultiplier = 1.1
72
73 self.setDownloadDirectory(Preferences.getUI("DownloadPath"))
74
75 self.downloadsView.setShowGrid(False)
76 self.downloadsView.verticalHeader().hide()
77 self.downloadsView.horizontalHeader().hide()
78 self.downloadsView.setAlternatingRowColors(True)
79 self.downloadsView.horizontalHeader().setStretchLastSection(True)
80 self.downloadsView.setModel(self.__model)
81 self.downloadsView.setContextMenuPolicy(
82 Qt.ContextMenuPolicy.CustomContextMenu)
83 self.downloadsView.customContextMenuRequested.connect(
84 self.__customContextMenuRequested)
85
86 self.__clearShortcut = QShortcut(QKeySequence("Ctrl+L"), self)
87 self.__clearShortcut.activated.connect(self.on_cleanupButton_clicked)
88
89 self.__load()
90
91 self.__updateTimer = QBasicTimer()
92
93 def __customContextMenuRequested(self, pos):
94 """
95 Private slot to handle the context menu request for the bookmarks tree.
96
97 @param pos position the context menu was requested (QPoint)
98 """
99 menu = QMenu()
100
101 selectedRowsCount = len(
102 self.downloadsView.selectionModel().selectedRows())
103
104 if selectedRowsCount == 1:
105 row = self.downloadsView.selectionModel().selectedRows()[0].row()
106 itm = self.__downloads[row]
107 if itm.downloadedSuccessfully():
108 menu.addAction(
109 UI.PixmapCache.getIcon("open"),
110 self.tr("Open"), self.__contextMenuOpen)
111 elif itm.downloading():
112 menu.addAction(
113 UI.PixmapCache.getIcon("stopLoading"),
114 self.tr("Cancel"), self.__contextMenuCancel)
115 menu.addSeparator()
116 menu.addAction(
117 self.tr("Open Containing Folder"),
118 self.__contextMenuOpenFolder)
119 menu.addSeparator()
120 menu.addAction(
121 self.tr("Go to Download Page"),
122 self.__contextMenuGotoPage)
123 menu.addAction(
124 self.tr("Copy Download Link"),
125 self.__contextMenuCopyLink)
126 menu.addSeparator()
127 menu.addAction(self.tr("Select All"), self.__contextMenuSelectAll)
128 if (
129 selectedRowsCount > 1 or
130 (selectedRowsCount == 1 and
131 not self.__downloads[
132 self.downloadsView.selectionModel().selectedRows()[0].row()]
133 .downloading())
134 ):
135 menu.addSeparator()
136 menu.addAction(
137 self.tr("Remove From List"),
138 self.__contextMenuRemoveSelected)
139
140 menu.exec(QCursor.pos())
141
142 def shutdown(self):
143 """
144 Public method to stop the download manager.
145 """
146 self.save()
147 self.close()
148
149 def activeDownloadsCount(self):
150 """
151 Public method to get the number of active downloads.
152
153 @return number of active downloads (integer)
154 """
155 count = 0
156
157 for download in self.__downloads:
158 if download.downloading():
159 count += 1
160 return count
161
162 def allowQuit(self):
163 """
164 Public method to check, if it is ok to quit.
165
166 @return flag indicating allowance to quit (boolean)
167 """
168 if self.activeDownloadsCount() > 0:
169 res = E5MessageBox.yesNo(
170 self,
171 self.tr(""),
172 self.tr("""There are %n downloads in progress.\n"""
173 """Do you want to quit anyway?""", "",
174 self.activeDownloadsCount()),
175 icon=E5MessageBox.Warning)
176 if not res:
177 self.show()
178 return False
179
180 self.close()
181 return True
182
183 def __testWebBrowserView(self, view, url):
184 """
185 Private method to test a web browser view against an URL.
186
187 @param view reference to the web browser view to be tested
188 @type WebBrowserView
189 @param url URL to test against
190 @type QUrl
191 @return flag indicating, that the view is the one for the URL
192 @rtype bool
193 """
194 if view.tabWidget().count() < 2:
195 return False
196
197 page = view.page()
198 if page.history().count() != 0:
199 return False
200
201 if (
202 not page.url().isEmpty() and
203 page.url().host() == url.host()
204 ):
205 return True
206
207 requestedUrl = page.requestedUrl()
208 if requestedUrl.isEmpty():
209 requestedUrl = QUrl(view.tabWidget().urlBarForView(view).text())
210 return requestedUrl.isEmpty() or requestedUrl.host() == url.host()
211
212 def __closeDownloadTab(self, url):
213 """
214 Private method to close an empty tab, that was opened only for loading
215 the download URL.
216
217 @param url download URL
218 @type QUrl
219 """
220 if self.__testWebBrowserView(
221 WebBrowserWindow.getWindow().currentBrowser(), url):
222 WebBrowserWindow.getWindow().closeCurrentBrowser()
223 return
224
225 for window in WebBrowserWindow.mainWindows():
226 for browser in window.browsers():
227 if self.__testWebBrowserView(browser, url):
228 window.closeBrowser(browser)
229 return
230
231 def download(self, downloadItem):
232 """
233 Public method to download a file.
234
235 @param downloadItem reference to the download object containing the
236 download data.
237 @type QWebEngineDownloadItem
238 """
239 url = downloadItem.url()
240 if url.isEmpty():
241 return
242
243 self.__closeDownloadTab(url)
244
245 # Safe Browsing
246 from WebBrowser.SafeBrowsing.SafeBrowsingManager import (
247 SafeBrowsingManager
248 )
249 if SafeBrowsingManager.isEnabled():
250 threatLists = (
251 WebBrowserWindow.safeBrowsingManager().lookupUrl(url)[0]
252 )
253 if threatLists:
254 threatMessages = (
255 WebBrowserWindow.safeBrowsingManager()
256 .getThreatMessages(threatLists)
257 )
258 res = E5MessageBox.warning(
259 WebBrowserWindow.getWindow(),
260 self.tr("Suspicuous URL detected"),
261 self.tr("<p>The URL <b>{0}</b> was found in the Safe"
262 " Browsing database.</p>{1}").format(
263 url.toString(), "".join(threatMessages)),
264 E5MessageBox.StandardButtons(
265 E5MessageBox.Abort |
266 E5MessageBox.Ignore),
267 E5MessageBox.Abort)
268 if res == E5MessageBox.Abort:
269 downloadItem.cancel()
270 return
271
272 window = WebBrowserWindow.getWindow()
273 pageUrl = window.currentBrowser().url() if window else QUrl()
274 from .DownloadItem import DownloadItem
275 itm = DownloadItem(downloadItem=downloadItem, pageUrl=pageUrl,
276 parent=self)
277 self.__addItem(itm)
278
279 if Preferences.getWebBrowser("DownloadManagerAutoOpen"):
280 self.show()
281 else:
282 self.__startUpdateTimer()
283
284 def show(self):
285 """
286 Public slot to show the download manager dialog.
287 """
288 self.__startUpdateTimer()
289
290 super().show()
291 self.activateWindow()
292 self.raise_()
293
294 def __addItem(self, itm, append=False):
295 """
296 Private method to add a download to the list of downloads.
297
298 @param itm reference to the download item
299 @type DownloadItem
300 @param append flag indicating to append the item
301 @type bool
302 """
303 itm.statusChanged.connect(lambda: self.__updateRow(itm))
304 itm.downloadFinished.connect(self.__finished)
305
306 # insert at top of window
307 row = self.downloadsCount() if append else 0
308 self.__model.beginInsertRows(QModelIndex(), row, row)
309 if append:
310 self.__downloads.append(itm)
311 else:
312 self.__downloads.insert(0, itm)
313 self.__model.endInsertRows()
314
315 self.downloadsView.setIndexWidget(self.__model.index(row, 0), itm)
316 icon = self.style().standardIcon(QStyle.StandardPixmap.SP_FileIcon)
317 itm.setIcon(icon)
318 self.downloadsView.setRowHeight(
319 row, itm.sizeHint().height() * self.__rowHeightMultiplier)
320 # just in case the download finished before the constructor returned
321 self.__updateRow(itm)
322 self.changeOccurred()
323
324 self.downloadsCountChanged.emit()
325
326 def __updateRow(self, itm):
327 """
328 Private slot to update a download item.
329
330 @param itm reference to the download item
331 @type DownloadItem
332 """
333 if itm not in self.__downloads:
334 return
335
336 row = self.__downloads.index(itm)
337
338 if self.__iconProvider is None:
339 self.__iconProvider = QFileIconProvider()
340
341 icon = self.__iconProvider.icon(QFileInfo(itm.fileName()))
342 if icon.isNull():
343 icon = self.style().standardIcon(QStyle.StandardPixmap.SP_FileIcon)
344 itm.setIcon(icon)
345
346 self.downloadsView.setRowHeight(
347 row,
348 itm.minimumSizeHint().height() * self.__rowHeightMultiplier)
349
350 remove = False
351
352 if (
353 itm.downloadedSuccessfully() and
354 self.removePolicy() == DownloadManager.RemoveSuccessFullDownload
355 ):
356 remove = True
357
358 if remove:
359 self.__model.removeRow(row)
360
361 self.cleanupButton.setEnabled(
362 (self.downloadsCount() - self.activeDownloadsCount()) > 0)
363
364 # record the change
365 self.changeOccurred()
366
367 def removePolicy(self):
368 """
369 Public method to get the remove policy.
370
371 @return remove policy (integer)
372 """
373 return Preferences.getWebBrowser("DownloadManagerRemovePolicy")
374
375 def setRemovePolicy(self, policy):
376 """
377 Public method to set the remove policy.
378
379 @param policy policy to be set
380 (DownloadManager.RemoveExit, DownloadManager.RemoveNever,
381 DownloadManager.RemoveSuccessFullDownload)
382 """
383 if policy in (DownloadManager.RemoveExit,
384 DownloadManager.RemoveNever,
385 DownloadManager.RemoveSuccessFullDownload):
386
387 if policy == self.removePolicy():
388 return
389
390 Preferences.setWebBrowser("DownloadManagerRemovePolicy",
391 self.policy)
392
393 def save(self):
394 """
395 Public method to save the download settings.
396 """
397 if not self.__loaded:
398 return
399
400 Preferences.setWebBrowser("DownloadManagerSize", self.size())
401 Preferences.setWebBrowser("DownloadManagerPosition", self.pos())
402 if self.removePolicy() == DownloadManager.RemoveExit:
403 return
404
405 from WebBrowser.WebBrowserWindow import WebBrowserWindow
406 if WebBrowserWindow.isPrivate():
407 return
408
409 downloads = []
410 for download in self.__downloads:
411 downloads.append(download.getData())
412 Preferences.setWebBrowser("DownloadManagerDownloads", downloads)
413
414 def __load(self):
415 """
416 Private method to load the download settings.
417 """
418 if self.__loaded:
419 return
420
421 size = Preferences.getWebBrowser("DownloadManagerSize")
422 if size.isValid():
423 self.resize(size)
424 pos = Preferences.getWebBrowser("DownloadManagerPosition")
425 self.move(pos)
426
427 from WebBrowser.WebBrowserWindow import WebBrowserWindow
428 if not WebBrowserWindow.isPrivate():
429 downloads = Preferences.getWebBrowser("DownloadManagerDownloads")
430 for download in downloads:
431 if (
432 not download["URL"].isEmpty() and
433 bool(download["Location"])
434 ):
435 from .DownloadItem import DownloadItem
436 itm = DownloadItem(parent=self)
437 itm.setData(download)
438 self.__addItem(itm, append=True)
439 self.cleanupButton.setEnabled(
440 (self.downloadsCount() - self.activeDownloadsCount()) > 0)
441
442 self.__loaded = True
443
444 self.downloadsCountChanged.emit()
445
446 def closeEvent(self, evt):
447 """
448 Protected event handler for the close event.
449
450 @param evt reference to the close event
451 @type QCloseEvent
452 """
453 self.save()
454
455 def cleanup(self):
456 """
457 Public slot to cleanup the downloads.
458 """
459 self.on_cleanupButton_clicked()
460
461 @pyqtSlot()
462 def on_cleanupButton_clicked(self):
463 """
464 Private slot to cleanup the downloads.
465 """
466 if self.downloadsCount() == 0:
467 return
468
469 self.__model.removeRows(0, self.downloadsCount())
470 if (
471 self.downloadsCount() == 0 and
472 self.__iconProvider is not None
473 ):
474 self.__iconProvider = None
475
476 self.changeOccurred()
477
478 self.downloadsCountChanged.emit()
479
480 def __finished(self, success):
481 """
482 Private slot to handle a finished download.
483
484 @param success flag indicating a successful download
485 @type bool
486 """
487 if self.isVisible():
488 QApplication.alert(self)
489
490 self.downloadsCountChanged.emit()
491
492 if self.activeDownloadsCount() == 0:
493 # all active downloads are done
494 if success and e5App().activeWindow() is not self:
495 WebBrowserWindow.showNotification(
496 UI.PixmapCache.getPixmap("downloads48"),
497 self.tr("Downloads finished"),
498 self.tr("All files have been downloaded.")
499 )
500 if not Preferences.getWebBrowser("DownloadManagerAutoClose"):
501 self.raise_()
502 self.activateWindow()
503
504 self.__stopUpdateTimer()
505 self.infoLabel.clear()
506 self.setWindowTitle(self.tr("Download Manager"))
507 if Globals.isWindowsPlatform():
508 self.__taskbarButton().progress().hide()
509
510 if Preferences.getWebBrowser("DownloadManagerAutoClose"):
511 self.close()
512
513 def setDownloadDirectory(self, directory):
514 """
515 Public method to set the current download directory.
516
517 @param directory current download directory (string)
518 """
519 self.__downloadDirectory = directory
520 if self.__downloadDirectory != "":
521 self.__downloadDirectory += "/"
522
523 def downloadDirectory(self):
524 """
525 Public method to get the current download directory.
526
527 @return current download directory (string)
528 """
529 return self.__downloadDirectory
530
531 def downloadsCount(self):
532 """
533 Public method to get the number of downloads.
534
535 @return number of downloads
536 @rtype int
537 """
538 return len(self.__downloads)
539
540 def downloads(self):
541 """
542 Public method to get a reference to the downloads.
543
544 @return reference to the downloads (list of DownloadItem)
545 """
546 return self.__downloads
547
548 def changeOccurred(self):
549 """
550 Public method to signal a change.
551 """
552 self.__saveTimer.changeOccurred()
553
554 def __taskbarButton(self):
555 """
556 Private method to get a reference to the task bar button (Windows
557 only).
558
559 @return reference to the task bar button
560 @rtype QWinTaskbarButton or None
561 """
562 if Globals.isWindowsPlatform():
563 from PyQt5.QtWinExtras import QWinTaskbarButton
564 if self.__winTaskbarButton is None:
565 window = WebBrowserWindow.mainWindow()
566 self.__winTaskbarButton = QWinTaskbarButton(
567 window.windowHandle())
568 self.__winTaskbarButton.progress().setRange(0, 100)
569
570 return self.__winTaskbarButton
571
572 def timerEvent(self, evt):
573 """
574 Protected event handler for timer events.
575
576 @param evt reference to the timer event
577 @type QTimerEvent
578 """
579 if evt.timerId() == self.__updateTimer.timerId():
580 if self.activeDownloadsCount() == 0:
581 self.__stopUpdateTimer()
582 self.infoLabel.clear()
583 self.setWindowTitle(self.tr("Download Manager"))
584 if Globals.isWindowsPlatform():
585 self.__taskbarButton().progress().hide()
586 else:
587 progresses = []
588 for itm in self.__downloads:
589 if (
590 itm is None or
591 itm.downloadCanceled() or
592 not itm.downloading()
593 ):
594 continue
595
596 progresses.append((
597 itm.downloadProgress(),
598 itm.remainingTime(),
599 itm.currentSpeed()
600 ))
601
602 if not progresses:
603 return
604
605 remaining = 0
606 progress = 0
607 speed = 0.0
608
609 for progressData in progresses:
610 if progressData[1] > remaining:
611 remaining = progressData[1]
612 progress += progressData[0]
613 speed += progressData[2]
614 progress /= len(progresses)
615
616 if self.isVisible():
617 self.infoLabel.setText(self.tr(
618 "{0}% of %n file(s) ({1}) {2}", "",
619 len(progresses)).format(
620 progress,
621 speedString(speed),
622 timeString(remaining),
623 ))
624 self.setWindowTitle(self.tr("{0}% - Download Manager"))
625
626 if Globals.isWindowsPlatform():
627 self.__taskbarButton().progress().show()
628 self.__taskbarButton().progress().setValue(progress)
629
630 super().timerEvent(evt)
631
632 def __startUpdateTimer(self):
633 """
634 Private slot to start the update timer.
635 """
636 if self.activeDownloadsCount() and not self.__updateTimer.isActive():
637 self.__updateTimer.start(DownloadManager.UpdateTimerTimeout, self)
638
639 def __stopUpdateTimer(self):
640 """
641 Private slot to stop the update timer.
642 """
643 self.__updateTimer.stop()
644
645 ###########################################################################
646 ## Context menu related methods below
647 ###########################################################################
648
649 def __currentItem(self):
650 """
651 Private method to get a reference to the current item.
652
653 @return reference to the current item (DownloadItem)
654 """
655 index = self.downloadsView.currentIndex()
656 if index and index.isValid():
657 row = index.row()
658 return self.__downloads[row]
659
660 return None
661
662 def __contextMenuOpen(self):
663 """
664 Private method to open the downloaded file.
665 """
666 itm = self.__currentItem()
667 if itm is not None:
668 itm.openFile()
669
670 def __contextMenuOpenFolder(self):
671 """
672 Private method to open the folder containing the downloaded file.
673 """
674 itm = self.__currentItem()
675 if itm is not None:
676 itm.openFolder()
677
678 def __contextMenuCancel(self):
679 """
680 Private method to cancel the current download.
681 """
682 itm = self.__currentItem()
683 if itm is not None:
684 itm.cancelDownload()
685
686 def __contextMenuGotoPage(self):
687 """
688 Private method to open the download page.
689 """
690 itm = self.__currentItem()
691 if itm is not None:
692 url = itm.getPageUrl()
693 WebBrowserWindow.mainWindow().openUrl(url, "")
694
695 def __contextMenuCopyLink(self):
696 """
697 Private method to copy the download link to the clipboard.
698 """
699 itm = self.__currentItem()
700 if itm is not None:
701 url = itm.getPageUrl().toDisplayString(
702 QUrl.ComponentFormattingOption.FullyDecoded)
703 QApplication.clipboard().setText(url)
704
705 def __contextMenuSelectAll(self):
706 """
707 Private method to select all downloads.
708 """
709 self.downloadsView.selectAll()
710
711 def __contextMenuRemoveSelected(self):
712 """
713 Private method to remove the selected downloads from the list.
714 """
715 self.downloadsView.removeSelected()

eric ide

mercurial