src/eric7/WebBrowser/Download/DownloadManager.py

Fri, 04 Nov 2022 13:52:26 +0100

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Fri, 04 Nov 2022 13:52:26 +0100
branch
eric7
changeset 9473
3f23dbf37dbe
parent 9413
80c06d472826
child 9482
a2bc06a54d9d
permissions
-rw-r--r--

Resorted the import statements using isort.

# -*- coding: utf-8 -*-

# Copyright (c) 2010 - 2022 Detlev Offenbach <detlev@die-offenbachs.de>
#

"""
Module implementing the download manager class.
"""

from PyQt6.QtCore import (
    QBasicTimer,
    QFileInfo,
    QModelIndex,
    Qt,
    QUrl,
    pyqtSignal,
    pyqtSlot,
)
from PyQt6.QtGui import QCursor, QKeySequence, QShortcut
from PyQt6.QtWidgets import QApplication, QDialog, QFileIconProvider, QMenu, QStyle

from eric7 import Globals, Preferences
from eric7.EricGui import EricPixmapCache
from eric7.EricWidgets import EricMessageBox
from eric7.EricWidgets.EricApplication import ericApp
from eric7.Utilities.AutoSaver import AutoSaver
from eric7.WebBrowser.WebBrowserWindow import WebBrowserWindow

from .DownloadModel import DownloadModel
from .DownloadUtilities import speedString, timeString
from .Ui_DownloadManager import Ui_DownloadManager


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().__init__(parent)
        self.setupUi(self)
        self.setWindowFlags(Qt.WindowType.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.ContextMenuPolicy.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(
                    EricPixmapCache.getIcon("open"),
                    self.tr("Open"),
                    self.__contextMenuOpen,
                )
            elif itm.downloading():
                menu.addAction(
                    EricPixmapCache.getIcon("stopLoading"),
                    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 = EricMessageBox.yesNo(
                self,
                self.tr(""),
                self.tr(
                    """There are %n downloads in progress.\n"""
                    """Do you want to quit anyway?""",
                    "",
                    self.activeDownloadsCount(),
                ),
                icon=EricMessageBox.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, downloadRequest):
        """
        Public method to download a file.

        @param downloadRequest reference to the download object containing the
        download data.
        @type QWebEngineDownloadRequest
        """
        url = downloadRequest.url()
        if url.isEmpty():
            return

        self.__closeDownloadTab(url)

        # Safe Browsing
        from eric7.WebBrowser.SafeBrowsing.SafeBrowsingManager import (
            SafeBrowsingManager,
        )

        if SafeBrowsingManager.isEnabled():
            threatLists = WebBrowserWindow.safeBrowsingManager().lookupUrl(url)[0]
            if threatLists:
                threatMessages = (
                    WebBrowserWindow.safeBrowsingManager().getThreatMessages(
                        threatLists
                    )
                )
                res = EricMessageBox.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)),
                    EricMessageBox.Abort | EricMessageBox.Ignore,
                    EricMessageBox.Abort,
                )
                if res == EricMessageBox.Abort:
                    downloadRequest.cancel()
                    return

        window = WebBrowserWindow.getWindow()
        pageUrl = window.currentBrowser().url() if window else QUrl()
        from .DownloadItem import DownloadItem

        itm = DownloadItem(
            downloadRequest=downloadRequest, 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().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
        row = self.downloadsCount() if append else 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.StandardPixmap.SP_FileIcon)
        itm.setIcon(icon)
        self.downloadsView.setRowHeight(
            row, int(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.StandardPixmap.SP_FileIcon)
        itm.setIcon(icon)

        self.downloadsView.setRowHeight(
            row, int(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)
        """
        if 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 eric7.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 eric7.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 ericApp().activeWindow() is not self:
                WebBrowserWindow.showNotification(
                    EricPixmapCache.getPixmap("downloads48"),
                    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 PyQt6.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 /= 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().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.ComponentFormattingOption.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()

eric ide

mercurial