--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/WebBrowser/Download/DownloadManager.py Sun Apr 14 15:09:21 2019 +0200 @@ -0,0 +1,703 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2010 - 2019 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the download manager class. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QModelIndex, QFileInfo, \ + QUrl, QBasicTimer +from PyQt5.QtGui import QCursor, QKeySequence +from PyQt5.QtWidgets import QDialog, QStyle, QFileIconProvider, QMenu, \ + QApplication, QShortcut + +from E5Gui import E5MessageBox +from E5Gui.E5Application import e5App + +from .Ui_DownloadManager import Ui_DownloadManager + +from .DownloadModel import DownloadModel +from .DownloadUtilities import speedString, timeString + +from WebBrowser.WebBrowserWindow import WebBrowserWindow + +from Utilities.AutoSaver import AutoSaver +import UI.PixmapCache +import Preferences +import Globals + + +class DownloadManager(QDialog, Ui_DownloadManager): + """ + Class implementing the download manager. + + @signal downloadsCountChanged() emitted to indicate a change of the + count of download items + """ + RemoveNever = 0 + RemoveExit = 1 + RemoveSuccessFullDownload = 2 + + UpdateTimerTimeout = 1000 + + downloadsCountChanged = pyqtSignal() + + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent widget (QWidget) + """ + super(DownloadManager, self).__init__(parent) + self.setupUi(self) + self.setWindowFlags(Qt.Window) + + self.__winTaskbarButton = None + + self.__saveTimer = AutoSaver(self, self.save) + + self.__model = DownloadModel(self) + self.__manager = WebBrowserWindow.networkManager() + + self.__iconProvider = None + self.__downloads = [] + self.__downloadDirectory = "" + self.__loaded = False + + self.__rowHeightMultiplier = 1.1 + + self.setDownloadDirectory(Preferences.getUI("DownloadPath")) + + self.downloadsView.setShowGrid(False) + self.downloadsView.verticalHeader().hide() + self.downloadsView.horizontalHeader().hide() + self.downloadsView.setAlternatingRowColors(True) + self.downloadsView.horizontalHeader().setStretchLastSection(True) + self.downloadsView.setModel(self.__model) + self.downloadsView.setContextMenuPolicy(Qt.CustomContextMenu) + self.downloadsView.customContextMenuRequested.connect( + self.__customContextMenuRequested) + + self.__clearShortcut = QShortcut(QKeySequence("Ctrl+L"), self) + self.__clearShortcut.activated.connect(self.on_cleanupButton_clicked) + + self.__load() + + self.__updateTimer = QBasicTimer() + + def __customContextMenuRequested(self, pos): + """ + Private slot to handle the context menu request for the bookmarks tree. + + @param pos position the context menu was requested (QPoint) + """ + menu = QMenu() + + selectedRowsCount = len( + self.downloadsView.selectionModel().selectedRows()) + + if selectedRowsCount == 1: + row = self.downloadsView.selectionModel().selectedRows()[0].row() + itm = self.__downloads[row] + if itm.downloadedSuccessfully(): + menu.addAction( + UI.PixmapCache.getIcon("open.png"), + self.tr("Open"), self.__contextMenuOpen) + elif itm.downloading(): + menu.addAction( + UI.PixmapCache.getIcon("stopLoading.png"), + self.tr("Cancel"), self.__contextMenuCancel) + menu.addSeparator() + menu.addAction( + self.tr("Open Containing Folder"), + self.__contextMenuOpenFolder) + menu.addSeparator() + menu.addAction( + self.tr("Go to Download Page"), + self.__contextMenuGotoPage) + menu.addAction( + self.tr("Copy Download Link"), + self.__contextMenuCopyLink) + menu.addSeparator() + menu.addAction(self.tr("Select All"), self.__contextMenuSelectAll) + if selectedRowsCount > 1 or \ + (selectedRowsCount == 1 and + not self.__downloads[ + self.downloadsView.selectionModel().selectedRows()[0].row()] + .downloading()): + menu.addSeparator() + menu.addAction( + self.tr("Remove From List"), + self.__contextMenuRemoveSelected) + + menu.exec_(QCursor.pos()) + + def shutdown(self): + """ + Public method to stop the download manager. + """ + self.save() + self.close() + + def activeDownloadsCount(self): + """ + Public method to get the number of active downloads. + + @return number of active downloads (integer) + """ + count = 0 + + for download in self.__downloads: + if download.downloading(): + count += 1 + return count + + def allowQuit(self): + """ + Public method to check, if it is ok to quit. + + @return flag indicating allowance to quit (boolean) + """ + if self.activeDownloadsCount() > 0: + res = E5MessageBox.yesNo( + self, + self.tr(""), + self.tr("""There are %n downloads in progress.\n""" + """Do you want to quit anyway?""", "", + self.activeDownloadsCount()), + icon=E5MessageBox.Warning) + if not res: + self.show() + return False + + self.close() + return True + + def __testWebBrowserView(self, view, url): + """ + Private method to test a web browser view against an URL. + + @param view reference to the web browser view to be tested + @type WebBrowserView + @param url URL to test against + @type QUrl + @return flag indicating, that the view is the one for the URL + @rtype bool + """ + if view.tabWidget().count() < 2: + return False + + page = view.page() + if page.history().count() != 0: + return False + + if not page.url().isEmpty() and \ + page.url().host() == url.host(): + return True + + requestedUrl = page.requestedUrl() + if requestedUrl.isEmpty(): + requestedUrl = QUrl(view.tabWidget().urlBarForView(view).text()) + return requestedUrl.isEmpty() or requestedUrl.host() == url.host() + + def __closeDownloadTab(self, url): + """ + Private method to close an empty tab, that was opened only for loading + the download URL. + + @param url download URL + @type QUrl + """ + if self.__testWebBrowserView( + WebBrowserWindow.getWindow().currentBrowser(), url): + WebBrowserWindow.getWindow().closeCurrentBrowser() + return + + for window in WebBrowserWindow.mainWindows(): + for browser in window.browsers(): + if self.__testWebBrowserView(browser, url): + window.closeBrowser(browser) + return + + def download(self, downloadItem): + """ + Public method to download a file. + + @param downloadItem reference to the download object containing the + download data. + @type QWebEngineDownloadItem + """ + url = downloadItem.url() + if url.isEmpty(): + return + + self.__closeDownloadTab(url) + + # Safe Browsing + from WebBrowser.SafeBrowsing.SafeBrowsingManager import \ + SafeBrowsingManager + if SafeBrowsingManager.isEnabled(): + threatLists = \ + WebBrowserWindow.safeBrowsingManager().lookupUrl(url)[0] + if threatLists: + threatMessages = WebBrowserWindow.safeBrowsingManager()\ + .getThreatMessages(threatLists) + res = E5MessageBox.warning( + WebBrowserWindow.getWindow(), + self.tr("Suspicuous URL detected"), + self.tr("<p>The URL <b>{0}</b> was found in the Safe" + " Browsing database.</p>{1}").format( + url.toString(), "".join(threatMessages)), + E5MessageBox.StandardButtons( + E5MessageBox.Abort | + E5MessageBox.Ignore), + E5MessageBox.Abort) + if res == E5MessageBox.Abort: + downloadItem.cancel() + return + + window = WebBrowserWindow.getWindow() + if window: + pageUrl = window.currentBrowser().url() + else: + pageUrl = QUrl() + from .DownloadItem import DownloadItem + itm = DownloadItem(downloadItem=downloadItem, pageUrl=pageUrl, + parent=self) + self.__addItem(itm) + + if Preferences.getWebBrowser("DownloadManagerAutoOpen"): + self.show() + else: + self.__startUpdateTimer() + + def show(self): + """ + Public slot to show the download manager dialog. + """ + self.__startUpdateTimer() + + super(DownloadManager, self).show() + self.activateWindow() + self.raise_() + + def __addItem(self, itm, append=False): + """ + Private method to add a download to the list of downloads. + + @param itm reference to the download item + @type DownloadItem + @param append flag indicating to append the item + @type bool + """ + itm.statusChanged.connect(lambda: self.__updateRow(itm)) + itm.downloadFinished.connect(self.__finished) + + # insert at top of window + if append: + row = self.downloadsCount() + else: + row = 0 + self.__model.beginInsertRows(QModelIndex(), row, row) + if append: + self.__downloads.append(itm) + else: + self.__downloads.insert(0, itm) + self.__model.endInsertRows() + + self.downloadsView.setIndexWidget(self.__model.index(row, 0), itm) + icon = self.style().standardIcon(QStyle.SP_FileIcon) + itm.setIcon(icon) + self.downloadsView.setRowHeight( + row, itm.sizeHint().height() * self.__rowHeightMultiplier) + # just in case the download finished before the constructor returned + self.__updateRow(itm) + self.changeOccurred() + + self.downloadsCountChanged.emit() + + def __updateRow(self, itm): + """ + Private slot to update a download item. + + @param itm reference to the download item + @type DownloadItem + """ + if itm not in self.__downloads: + return + + row = self.__downloads.index(itm) + + if self.__iconProvider is None: + self.__iconProvider = QFileIconProvider() + + icon = self.__iconProvider.icon(QFileInfo(itm.fileName())) + if icon.isNull(): + icon = self.style().standardIcon(QStyle.SP_FileIcon) + itm.setIcon(icon) + + self.downloadsView.setRowHeight( + row, + itm.minimumSizeHint().height() * self.__rowHeightMultiplier) + + remove = False + + if itm.downloadedSuccessfully() and \ + self.removePolicy() == DownloadManager.RemoveSuccessFullDownload: + remove = True + + if remove: + self.__model.removeRow(row) + + self.cleanupButton.setEnabled( + (self.downloadsCount() - self.activeDownloadsCount()) > 0) + + # record the change + self.changeOccurred() + + def removePolicy(self): + """ + Public method to get the remove policy. + + @return remove policy (integer) + """ + return Preferences.getWebBrowser("DownloadManagerRemovePolicy") + + def setRemovePolicy(self, policy): + """ + Public method to set the remove policy. + + @param policy policy to be set + (DownloadManager.RemoveExit, DownloadManager.RemoveNever, + DownloadManager.RemoveSuccessFullDownload) + """ + assert policy in (DownloadManager.RemoveExit, + DownloadManager.RemoveNever, + DownloadManager.RemoveSuccessFullDownload) + + if policy == self.removePolicy(): + return + + Preferences.setWebBrowser("DownloadManagerRemovePolicy", self.policy) + + def save(self): + """ + Public method to save the download settings. + """ + if not self.__loaded: + return + + Preferences.setWebBrowser("DownloadManagerSize", self.size()) + Preferences.setWebBrowser("DownloadManagerPosition", self.pos()) + if self.removePolicy() == DownloadManager.RemoveExit: + return + + from WebBrowser.WebBrowserWindow import WebBrowserWindow + if WebBrowserWindow.isPrivate(): + return + + downloads = [] + for download in self.__downloads: + downloads.append(download.getData()) + Preferences.setWebBrowser("DownloadManagerDownloads", downloads) + + def __load(self): + """ + Private method to load the download settings. + """ + if self.__loaded: + return + + size = Preferences.getWebBrowser("DownloadManagerSize") + if size.isValid(): + self.resize(size) + pos = Preferences.getWebBrowser("DownloadManagerPosition") + self.move(pos) + + from WebBrowser.WebBrowserWindow import WebBrowserWindow + if not WebBrowserWindow.isPrivate(): + downloads = Preferences.getWebBrowser("DownloadManagerDownloads") + for download in downloads: + if not download["URL"].isEmpty() and \ + bool(download["Location"]): + from .DownloadItem import DownloadItem + itm = DownloadItem(parent=self) + itm.setData(download) + self.__addItem(itm, append=True) + self.cleanupButton.setEnabled( + (self.downloadsCount() - self.activeDownloadsCount()) > 0) + + self.__loaded = True + + self.downloadsCountChanged.emit() + + def closeEvent(self, evt): + """ + Protected event handler for the close event. + + @param evt reference to the close event + @type QCloseEvent + """ + self.save() + + def cleanup(self): + """ + Public slot to cleanup the downloads. + """ + self.on_cleanupButton_clicked() + + @pyqtSlot() + def on_cleanupButton_clicked(self): + """ + Private slot to cleanup the downloads. + """ + if self.downloadsCount() == 0: + return + + self.__model.removeRows(0, self.downloadsCount()) + if self.downloadsCount() == 0 and \ + self.__iconProvider is not None: + self.__iconProvider = None + + self.changeOccurred() + + self.downloadsCountChanged.emit() + + def __finished(self, success): + """ + Private slot to handle a finished download. + + @param success flag indicating a successful download + @type bool + """ + if self.isVisible(): + QApplication.alert(self) + + self.downloadsCountChanged.emit() + + if self.activeDownloadsCount() == 0: + # all active downloads are done + if success and e5App().activeWindow() is not self: + if WebBrowserWindow.notificationsEnabled(): + WebBrowserWindow.showNotification( + UI.PixmapCache.getPixmap("downloads48.png"), + self.tr("Downloads finished"), + self.tr("All files have been downloaded.") + ) + if not Preferences.getWebBrowser("DownloadManagerAutoClose"): + self.raise_() + self.activateWindow() + + self.__stopUpdateTimer() + self.infoLabel.clear() + self.setWindowTitle(self.tr("Download Manager")) + if Globals.isWindowsPlatform(): + self.__taskbarButton().progress().hide() + + if Preferences.getWebBrowser("DownloadManagerAutoClose"): + self.close() + + def setDownloadDirectory(self, directory): + """ + Public method to set the current download directory. + + @param directory current download directory (string) + """ + self.__downloadDirectory = directory + if self.__downloadDirectory != "": + self.__downloadDirectory += "/" + + def downloadDirectory(self): + """ + Public method to get the current download directory. + + @return current download directory (string) + """ + return self.__downloadDirectory + + def downloadsCount(self): + """ + Public method to get the number of downloads. + + @return number of downloads + @rtype int + """ + return len(self.__downloads) + + def downloads(self): + """ + Public method to get a reference to the downloads. + + @return reference to the downloads (list of DownloadItem) + """ + return self.__downloads + + def changeOccurred(self): + """ + Public method to signal a change. + """ + self.__saveTimer.changeOccurred() + + def __taskbarButton(self): + """ + Private method to get a reference to the task bar button (Windows + only). + + @return reference to the task bar button + @rtype QWinTaskbarButton or None + """ + if Globals.isWindowsPlatform(): + from PyQt5.QtWinExtras import QWinTaskbarButton + if self.__winTaskbarButton is None: + window = WebBrowserWindow.mainWindow() + self.__winTaskbarButton = QWinTaskbarButton( + window.windowHandle()) + self.__winTaskbarButton.progress().setRange(0, 100) + + return self.__winTaskbarButton + + def timerEvent(self, evt): + """ + Protected event handler for timer events. + + @param evt reference to the timer event + @type QTimerEvent + """ + if evt.timerId() == self.__updateTimer.timerId(): + if self.activeDownloadsCount() == 0: + self.__stopUpdateTimer() + self.infoLabel.clear() + self.setWindowTitle(self.tr("Download Manager")) + if Globals.isWindowsPlatform(): + self.__taskbarButton().progress().hide() + else: + progresses = [] + for itm in self.__downloads: + if itm is None or \ + itm.downloadCanceled() or \ + not itm.downloading(): + continue + + progresses.append(( + itm.downloadProgress(), + itm.remainingTime(), + itm.currentSpeed() + )) + + if not progresses: + return + + remaining = 0 + progress = 0 + speed = 0.0 + + for progressData in progresses: + if progressData[1] > remaining: + remaining = progressData[1] + progress += progressData[0] + speed += progressData[2] + progress = progress / len(progresses) + + if self.isVisible(): + self.infoLabel.setText(self.tr( + "{0}% of %n file(s) ({1}) {2}", "", + len(progresses)).format( + progress, + speedString(speed), + timeString(remaining), + )) + self.setWindowTitle(self.tr("{0}% - Download Manager")) + + if Globals.isWindowsPlatform(): + self.__taskbarButton().progress().show() + self.__taskbarButton().progress().setValue(progress) + + super(DownloadManager, self).timerEvent(evt) + + def __startUpdateTimer(self): + """ + Private slot to start the update timer. + """ + if self.activeDownloadsCount() and not self.__updateTimer.isActive(): + self.__updateTimer.start(DownloadManager.UpdateTimerTimeout, self) + + def __stopUpdateTimer(self): + """ + Private slot to stop the update timer. + """ + self.__updateTimer.stop() + + ########################################################################### + ## Context menu related methods below + ########################################################################### + + def __currentItem(self): + """ + Private method to get a reference to the current item. + + @return reference to the current item (DownloadItem) + """ + index = self.downloadsView.currentIndex() + if index and index.isValid(): + row = index.row() + return self.__downloads[row] + + return None + + def __contextMenuOpen(self): + """ + Private method to open the downloaded file. + """ + itm = self.__currentItem() + if itm is not None: + itm.openFile() + + def __contextMenuOpenFolder(self): + """ + Private method to open the folder containing the downloaded file. + """ + itm = self.__currentItem() + if itm is not None: + itm.openFolder() + + def __contextMenuCancel(self): + """ + Private method to cancel the current download. + """ + itm = self.__currentItem() + if itm is not None: + itm.cancelDownload() + + def __contextMenuGotoPage(self): + """ + Private method to open the download page. + """ + itm = self.__currentItem() + if itm is not None: + url = itm.getPageUrl() + WebBrowserWindow.mainWindow().openUrl(url, "") + + def __contextMenuCopyLink(self): + """ + Private method to copy the download link to the clipboard. + """ + itm = self.__currentItem() + if itm is not None: + url = itm.getPageUrl().toDisplayString(QUrl.FullyDecoded) + QApplication.clipboard().setText(url) + + def __contextMenuSelectAll(self): + """ + Private method to select all downloads. + """ + self.downloadsView.selectAll() + + def __contextMenuRemoveSelected(self): + """ + Private method to remove the selected downloads from the list. + """ + self.downloadsView.removeSelected()