src/eric7/WebBrowser/Download/DownloadManager.py

branch
eric7
changeset 9209
b99e7fd55fd3
parent 8881
54e42bc2437a
child 9221
bf71ee032bb4
equal deleted inserted replaced
9208:3fc8dfeb6ebe 9209:b99e7fd55fd3
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2010 - 2022 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing the download manager class.
8 """
9
10 from PyQt6.QtCore import (
11 pyqtSlot, pyqtSignal, Qt, QModelIndex, QFileInfo, QUrl, QBasicTimer
12 )
13 from PyQt6.QtGui import QCursor, QKeySequence, QShortcut
14 from PyQt6.QtWidgets import (
15 QDialog, QStyle, QFileIconProvider, QMenu, QApplication
16 )
17
18 from EricWidgets import EricMessageBox
19 from EricWidgets.EricApplication import ericApp
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 = EricMessageBox.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=EricMessageBox.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, downloadRequest):
232 """
233 Public method to download a file.
234
235 @param downloadRequest reference to the download object containing the
236 download data.
237 @type QWebEngineDownloadRequest
238 """
239 url = downloadRequest.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 = EricMessageBox.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 EricMessageBox.Abort | EricMessageBox.Ignore,
265 EricMessageBox.Abort)
266 if res == EricMessageBox.Abort:
267 downloadRequest.cancel()
268 return
269
270 window = WebBrowserWindow.getWindow()
271 pageUrl = window.currentBrowser().url() if window else QUrl()
272 from .DownloadItem import DownloadItem
273 itm = DownloadItem(downloadRequest=downloadRequest, pageUrl=pageUrl,
274 parent=self)
275 self.__addItem(itm)
276
277 if Preferences.getWebBrowser("DownloadManagerAutoOpen"):
278 self.show()
279 else:
280 self.__startUpdateTimer()
281
282 def show(self):
283 """
284 Public slot to show the download manager dialog.
285 """
286 self.__startUpdateTimer()
287
288 super().show()
289 self.activateWindow()
290 self.raise_()
291
292 def __addItem(self, itm, append=False):
293 """
294 Private method to add a download to the list of downloads.
295
296 @param itm reference to the download item
297 @type DownloadItem
298 @param append flag indicating to append the item
299 @type bool
300 """
301 itm.statusChanged.connect(lambda: self.__updateRow(itm))
302 itm.downloadFinished.connect(self.__finished)
303
304 # insert at top of window
305 row = self.downloadsCount() if append else 0
306 self.__model.beginInsertRows(QModelIndex(), row, row)
307 if append:
308 self.__downloads.append(itm)
309 else:
310 self.__downloads.insert(0, itm)
311 self.__model.endInsertRows()
312
313 self.downloadsView.setIndexWidget(self.__model.index(row, 0), itm)
314 icon = self.style().standardIcon(QStyle.StandardPixmap.SP_FileIcon)
315 itm.setIcon(icon)
316 self.downloadsView.setRowHeight(
317 row, int(itm.sizeHint().height() * self.__rowHeightMultiplier))
318 # just in case the download finished before the constructor returned
319 self.__updateRow(itm)
320 self.changeOccurred()
321
322 self.downloadsCountChanged.emit()
323
324 def __updateRow(self, itm):
325 """
326 Private slot to update a download item.
327
328 @param itm reference to the download item
329 @type DownloadItem
330 """
331 if itm not in self.__downloads:
332 return
333
334 row = self.__downloads.index(itm)
335
336 if self.__iconProvider is None:
337 self.__iconProvider = QFileIconProvider()
338
339 icon = self.__iconProvider.icon(QFileInfo(itm.fileName()))
340 if icon.isNull():
341 icon = self.style().standardIcon(QStyle.StandardPixmap.SP_FileIcon)
342 itm.setIcon(icon)
343
344 self.downloadsView.setRowHeight(
345 row,
346 int(itm.minimumSizeHint().height() * self.__rowHeightMultiplier))
347
348 remove = False
349
350 if (
351 itm.downloadedSuccessfully() and
352 self.removePolicy() == DownloadManager.RemoveSuccessFullDownload
353 ):
354 remove = True
355
356 if remove:
357 self.__model.removeRow(row)
358
359 self.cleanupButton.setEnabled(
360 (self.downloadsCount() - self.activeDownloadsCount()) > 0)
361
362 # record the change
363 self.changeOccurred()
364
365 def removePolicy(self):
366 """
367 Public method to get the remove policy.
368
369 @return remove policy (integer)
370 """
371 return Preferences.getWebBrowser("DownloadManagerRemovePolicy")
372
373 def setRemovePolicy(self, policy):
374 """
375 Public method to set the remove policy.
376
377 @param policy policy to be set
378 (DownloadManager.RemoveExit, DownloadManager.RemoveNever,
379 DownloadManager.RemoveSuccessFullDownload)
380 """
381 if policy in (DownloadManager.RemoveExit,
382 DownloadManager.RemoveNever,
383 DownloadManager.RemoveSuccessFullDownload):
384
385 if policy == self.removePolicy():
386 return
387
388 Preferences.setWebBrowser("DownloadManagerRemovePolicy",
389 self.policy)
390
391 def save(self):
392 """
393 Public method to save the download settings.
394 """
395 if not self.__loaded:
396 return
397
398 Preferences.setWebBrowser("DownloadManagerSize", self.size())
399 Preferences.setWebBrowser("DownloadManagerPosition", self.pos())
400 if self.removePolicy() == DownloadManager.RemoveExit:
401 return
402
403 from WebBrowser.WebBrowserWindow import WebBrowserWindow
404 if WebBrowserWindow.isPrivate():
405 return
406
407 downloads = []
408 for download in self.__downloads:
409 downloads.append(download.getData())
410 Preferences.setWebBrowser("DownloadManagerDownloads", downloads)
411
412 def __load(self):
413 """
414 Private method to load the download settings.
415 """
416 if self.__loaded:
417 return
418
419 size = Preferences.getWebBrowser("DownloadManagerSize")
420 if size.isValid():
421 self.resize(size)
422 pos = Preferences.getWebBrowser("DownloadManagerPosition")
423 self.move(pos)
424
425 from WebBrowser.WebBrowserWindow import WebBrowserWindow
426 if not WebBrowserWindow.isPrivate():
427 downloads = Preferences.getWebBrowser("DownloadManagerDownloads")
428 for download in downloads:
429 if (
430 not download["URL"].isEmpty() and
431 bool(download["Location"])
432 ):
433 from .DownloadItem import DownloadItem
434 itm = DownloadItem(parent=self)
435 itm.setData(download)
436 self.__addItem(itm, append=True)
437 self.cleanupButton.setEnabled(
438 (self.downloadsCount() - self.activeDownloadsCount()) > 0)
439
440 self.__loaded = True
441
442 self.downloadsCountChanged.emit()
443
444 def closeEvent(self, evt):
445 """
446 Protected event handler for the close event.
447
448 @param evt reference to the close event
449 @type QCloseEvent
450 """
451 self.save()
452
453 def cleanup(self):
454 """
455 Public slot to cleanup the downloads.
456 """
457 self.on_cleanupButton_clicked()
458
459 @pyqtSlot()
460 def on_cleanupButton_clicked(self):
461 """
462 Private slot to cleanup the downloads.
463 """
464 if self.downloadsCount() == 0:
465 return
466
467 self.__model.removeRows(0, self.downloadsCount())
468 if (
469 self.downloadsCount() == 0 and
470 self.__iconProvider is not None
471 ):
472 self.__iconProvider = None
473
474 self.changeOccurred()
475
476 self.downloadsCountChanged.emit()
477
478 def __finished(self, success):
479 """
480 Private slot to handle a finished download.
481
482 @param success flag indicating a successful download
483 @type bool
484 """
485 if self.isVisible():
486 QApplication.alert(self)
487
488 self.downloadsCountChanged.emit()
489
490 if self.activeDownloadsCount() == 0:
491 # all active downloads are done
492 if success and ericApp().activeWindow() is not self:
493 WebBrowserWindow.showNotification(
494 UI.PixmapCache.getPixmap("downloads48"),
495 self.tr("Downloads finished"),
496 self.tr("All files have been downloaded.")
497 )
498 if not Preferences.getWebBrowser("DownloadManagerAutoClose"):
499 self.raise_()
500 self.activateWindow()
501
502 self.__stopUpdateTimer()
503 self.infoLabel.clear()
504 self.setWindowTitle(self.tr("Download Manager"))
505 if Globals.isWindowsPlatform():
506 self.__taskbarButton().progress().hide()
507
508 if Preferences.getWebBrowser("DownloadManagerAutoClose"):
509 self.close()
510
511 def setDownloadDirectory(self, directory):
512 """
513 Public method to set the current download directory.
514
515 @param directory current download directory (string)
516 """
517 self.__downloadDirectory = directory
518 if self.__downloadDirectory != "":
519 self.__downloadDirectory += "/"
520
521 def downloadDirectory(self):
522 """
523 Public method to get the current download directory.
524
525 @return current download directory (string)
526 """
527 return self.__downloadDirectory
528
529 def downloadsCount(self):
530 """
531 Public method to get the number of downloads.
532
533 @return number of downloads
534 @rtype int
535 """
536 return len(self.__downloads)
537
538 def downloads(self):
539 """
540 Public method to get a reference to the downloads.
541
542 @return reference to the downloads (list of DownloadItem)
543 """
544 return self.__downloads
545
546 def changeOccurred(self):
547 """
548 Public method to signal a change.
549 """
550 self.__saveTimer.changeOccurred()
551
552 def __taskbarButton(self):
553 """
554 Private method to get a reference to the task bar button (Windows
555 only).
556
557 @return reference to the task bar button
558 @rtype QWinTaskbarButton or None
559 """
560 if Globals.isWindowsPlatform():
561 from PyQt6.QtWinExtras import QWinTaskbarButton
562 if self.__winTaskbarButton is None:
563 window = WebBrowserWindow.mainWindow()
564 self.__winTaskbarButton = QWinTaskbarButton(
565 window.windowHandle())
566 self.__winTaskbarButton.progress().setRange(0, 100)
567
568 return self.__winTaskbarButton
569
570 def timerEvent(self, evt):
571 """
572 Protected event handler for timer events.
573
574 @param evt reference to the timer event
575 @type QTimerEvent
576 """
577 if evt.timerId() == self.__updateTimer.timerId():
578 if self.activeDownloadsCount() == 0:
579 self.__stopUpdateTimer()
580 self.infoLabel.clear()
581 self.setWindowTitle(self.tr("Download Manager"))
582 if Globals.isWindowsPlatform():
583 self.__taskbarButton().progress().hide()
584 else:
585 progresses = []
586 for itm in self.__downloads:
587 if (
588 itm is None or
589 itm.downloadCanceled() or
590 not itm.downloading()
591 ):
592 continue
593
594 progresses.append((
595 itm.downloadProgress(),
596 itm.remainingTime(),
597 itm.currentSpeed()
598 ))
599
600 if not progresses:
601 return
602
603 remaining = 0
604 progress = 0
605 speed = 0.0
606
607 for progressData in progresses:
608 if progressData[1] > remaining:
609 remaining = progressData[1]
610 progress += progressData[0]
611 speed += progressData[2]
612 progress /= len(progresses)
613
614 if self.isVisible():
615 self.infoLabel.setText(self.tr(
616 "{0}% of %n file(s) ({1}) {2}", "",
617 len(progresses)).format(
618 progress,
619 speedString(speed),
620 timeString(remaining),
621 ))
622 self.setWindowTitle(self.tr("{0}% - Download Manager"))
623
624 if Globals.isWindowsPlatform():
625 self.__taskbarButton().progress().show()
626 self.__taskbarButton().progress().setValue(progress)
627
628 super().timerEvent(evt)
629
630 def __startUpdateTimer(self):
631 """
632 Private slot to start the update timer.
633 """
634 if self.activeDownloadsCount() and not self.__updateTimer.isActive():
635 self.__updateTimer.start(DownloadManager.UpdateTimerTimeout, self)
636
637 def __stopUpdateTimer(self):
638 """
639 Private slot to stop the update timer.
640 """
641 self.__updateTimer.stop()
642
643 ###########################################################################
644 ## Context menu related methods below
645 ###########################################################################
646
647 def __currentItem(self):
648 """
649 Private method to get a reference to the current item.
650
651 @return reference to the current item (DownloadItem)
652 """
653 index = self.downloadsView.currentIndex()
654 if index and index.isValid():
655 row = index.row()
656 return self.__downloads[row]
657
658 return None
659
660 def __contextMenuOpen(self):
661 """
662 Private method to open the downloaded file.
663 """
664 itm = self.__currentItem()
665 if itm is not None:
666 itm.openFile()
667
668 def __contextMenuOpenFolder(self):
669 """
670 Private method to open the folder containing the downloaded file.
671 """
672 itm = self.__currentItem()
673 if itm is not None:
674 itm.openFolder()
675
676 def __contextMenuCancel(self):
677 """
678 Private method to cancel the current download.
679 """
680 itm = self.__currentItem()
681 if itm is not None:
682 itm.cancelDownload()
683
684 def __contextMenuGotoPage(self):
685 """
686 Private method to open the download page.
687 """
688 itm = self.__currentItem()
689 if itm is not None:
690 url = itm.getPageUrl()
691 WebBrowserWindow.mainWindow().openUrl(url, "")
692
693 def __contextMenuCopyLink(self):
694 """
695 Private method to copy the download link to the clipboard.
696 """
697 itm = self.__currentItem()
698 if itm is not None:
699 url = itm.getPageUrl().toDisplayString(
700 QUrl.ComponentFormattingOption.FullyDecoded)
701 QApplication.clipboard().setText(url)
702
703 def __contextMenuSelectAll(self):
704 """
705 Private method to select all downloads.
706 """
707 self.downloadsView.selectAll()
708
709 def __contextMenuRemoveSelected(self):
710 """
711 Private method to remove the selected downloads from the list.
712 """
713 self.downloadsView.removeSelected()

eric ide

mercurial