Web Browser (NG): improvement of the download manager

Sun, 08 Apr 2018 15:54:34 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Sun, 08 Apr 2018 15:54:34 +0200
changeset 6221
35ec993034e1
parent 6220
dd4a8b507144
child 6222
baffb22b4467

Web Browser (NG): improvement of the download manager

WebBrowser/Download/DownloadItem.py file | annotate | diff | comparison | revisions
WebBrowser/Download/DownloadManager.py file | annotate | diff | comparison | revisions
WebBrowser/Download/DownloadManager.ui file | annotate | diff | comparison | revisions
WebBrowser/Download/DownloadModel.py file | annotate | diff | comparison | revisions
WebBrowser/Download/DownloadUtilities.py file | annotate | diff | comparison | revisions
WebBrowser/WebBrowserTabWidget.py file | annotate | diff | comparison | revisions
WebBrowser/WebBrowserView.py file | annotate | diff | comparison | revisions
WebBrowser/WebBrowserWindow.py file | annotate | diff | comparison | revisions
changelog file | annotate | diff | comparison | revisions
--- a/WebBrowser/Download/DownloadItem.py	Sat Apr 07 16:40:21 2018 +0200
+++ b/WebBrowser/Download/DownloadItem.py	Sun Apr 08 15:54:34 2018 +0200
@@ -21,7 +21,7 @@
 
 from .Ui_DownloadItem import Ui_DownloadItem
 
-from .DownloadUtilities import timeString, dataString
+from .DownloadUtilities import timeString, dataString, speedString
 from WebBrowser.WebBrowserWindow import WebBrowserWindow
 
 import UI.PixmapCache
@@ -401,6 +401,15 @@
         self.progress.emit(currentValue, totalValue)
         self.__updateInfoLabel()
     
+    def downloadProgress(self):
+        """
+        Public method to get the download progress.
+        
+        @return current download progress
+        @rtype int
+        """
+        return self.progressBar.value()
+    
     def bytesTotal(self):
         """
         Public method to get the total number of bytes of the download.
@@ -471,12 +480,12 @@
             if bytesTotal > 0:
                 remaining = timeString(timeRemaining)
             
-            info = self.tr("{0} of {1} ({2}/sec)\n{3}")\
+            info = self.tr("{0} of {1} ({2}/sec) {3}")\
                 .format(
                     dataString(self.__bytesReceived),
                     bytesTotal == -1 and self.tr("?") or
                     dataString(bytesTotal),
-                    dataString(int(speed)),
+                    speedString(speed),
                     remaining)
         else:
             if self.__bytesReceived == bytesTotal or bytesTotal == -1:
--- a/WebBrowser/Download/DownloadManager.py	Sat Apr 07 16:40:21 2018 +0200
+++ b/WebBrowser/Download/DownloadManager.py	Sun Apr 08 15:54:34 2018 +0200
@@ -9,7 +9,8 @@
 
 from __future__ import unicode_literals
 
-from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QModelIndex, QFileInfo, QUrl
+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
@@ -19,12 +20,14 @@
 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):
@@ -38,6 +41,8 @@
     RemoveExit = 1
     RemoveSuccessFullDownload = 2
     
+    UpdateTimerTimeout = 1000
+    
     downloadsCountChanged = pyqtSignal()
     
     def __init__(self, parent=None):
@@ -50,6 +55,8 @@
         self.setupUi(self)
         self.setWindowFlags(Qt.Window)
         
+        self.__winTaskbarButton = None
+        
         self.__saveTimer = AutoSaver(self, self.save)
         
         self.__model = DownloadModel(self)
@@ -78,6 +85,8 @@
         self.__clearShortcut.activated.connect(self.on_cleanupButton_clicked)
         
         self.__load()
+        
+        self.__updateTimer = QBasicTimer()
     
     def __customContextMenuRequested(self, pos):
         """
@@ -165,6 +174,52 @@
                 return False
         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.
@@ -177,6 +232,8 @@
         if url.isEmpty():
             return
         
+        self.__closeDownloadTab(url)
+        
         # Safe Browsing
         from WebBrowser.SafeBrowsing.SafeBrowsingManager import \
             SafeBrowsingManager
@@ -206,12 +263,15 @@
                            parent=self)
         self.__addItem(itm)
         
-        if itm.canceledFileSelect():
-            return
+        self.__startUpdateTimer()
+    
+    def show(self):
+        """
+        Public slot to show the download manager dialog.
+        """
+        self.__startUpdateTimer()
         
-        if not self.isVisible():
-            self.show()
-        
+        super(DownloadManager, self).show()
         self.activateWindow()
         self.raise_()
     
@@ -247,7 +307,6 @@
         # just in case the download finished before the constructor returned
         self.__updateRow(itm)
         self.changeOccurred()
-        self.__updateActiveItemCount()
         
         self.downloadsCountChanged.emit()
     
@@ -363,7 +422,6 @@
                 (self.downloadsCount() - self.activeDownloadsCount()) > 0)
         
         self.__loaded = True
-        self.__updateActiveItemCount()
         
         self.downloadsCountChanged.emit()
     
@@ -385,7 +443,7 @@
     @pyqtSlot()
     def on_cleanupButton_clicked(self):
         """
-        Private slot cleanup the downloads.
+        Private slot to cleanup the downloads.
         """
         if self.downloadsCount() == 0:
             return
@@ -396,33 +454,13 @@
             self.__iconProvider = None
         
         self.changeOccurred()
-        self.__updateActiveItemCount()
         
         self.downloadsCountChanged.emit()
     
-    def __updateItemCount(self):
-        """
-        Private method to update the count label.
-        """
-        count = self.downloadsCount()
-        self.countLabel.setText(self.tr("%n Download(s)", "", count))
-    
-    def __updateActiveItemCount(self):
-        """
-        Private method to update the window title.
-        """
-        count = self.activeDownloadsCount()
-        if count > 0:
-            self.setWindowTitle(
-                self.tr("Downloading %n file(s)", "", count))
-        else:
-            self.setWindowTitle(self.tr("Downloads"))
-    
     def __finished(self):
         """
         Private slot to handle a finished download.
         """
-        self.__updateActiveItemCount()
         if self.isVisible():
             QApplication.alert(self)
         
@@ -468,7 +506,95 @@
         Public method to signal a change.
         """
         self.__saveTimer.changeOccurred()
-        self.__updateItemCount()
+    
+    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
--- a/WebBrowser/Download/DownloadManager.ui	Sat Apr 07 16:40:21 2018 +0200
+++ b/WebBrowser/Download/DownloadManager.ui	Sun Apr 08 15:54:34 2018 +0200
@@ -11,7 +11,7 @@
    </rect>
   </property>
   <property name="windowTitle">
-   <string>Downloads</string>
+   <string>Download Manager</string>
   </property>
   <property name="sizeGripEnabled">
    <bool>true</bool>
@@ -54,9 +54,9 @@
     </layout>
    </item>
    <item row="1" column="1">
-    <widget class="QLabel" name="countLabel">
+    <widget class="QLabel" name="infoLabel">
      <property name="text">
-      <string>0 Items</string>
+      <string/>
      </property>
     </widget>
    </item>
--- a/WebBrowser/Download/DownloadModel.py	Sat Apr 07 16:40:21 2018 +0200
+++ b/WebBrowser/Download/DownloadModel.py	Sun Apr 08 15:54:34 2018 +0200
@@ -20,8 +20,10 @@
         """
         Constructor
         
-        @param manager reference to the download manager (DownloadManager)
-        @param parent reference to the parent object (QObject)
+        @param manager reference to the download manager
+        @type DownloadManager
+        @param parent reference to the parent object
+        @type QObject
         """
         super(DownloadModel, self).__init__(parent)
         
@@ -31,9 +33,12 @@
         """
         Public method to get data from the model.
         
-        @param index index to get data for (QModelIndex)
-        @param role role of the data to retrieve (integer)
+        @param index index to get data for
+        @type QModelIndex
+        @param role role of the data to retrieve
+        @type int
         @return requested data
+        @rtype any
         """
         if index.row() < 0 or index.row() >= self.rowCount(index.parent()):
             return None
@@ -49,8 +54,10 @@
         """
         Public method to get the number of rows of the model.
         
-        @param parent parent index (QModelIndex)
-        @return number of rows (integer)
+        @param parent parent index
+        @type QModelIndex
+        @return number of rows
+        @rtype int
         """
         if parent is None:
             parent = QModelIndex()
@@ -62,12 +69,16 @@
     
     def removeRows(self, row, count, parent=None):
         """
-        Public method to remove bookmarks from the model.
+        Public method to remove downloads from the model.
         
-        @param row row of the first bookmark to remove (integer)
-        @param count number of bookmarks to remove (integer)
-        @param parent index of the parent bookmark node (QModelIndex)
-        @return flag indicating successful removal (boolean)
+        @param row row of the first download to remove
+        @type int
+        @param count number of downloads to remove
+        @type int
+        @param parent index of the parent download node
+        @type QModelIndex
+        @return flag indicating successful removal
+        @rtype bool
         """
         if parent is None:
             parent = QModelIndex()
@@ -91,8 +102,10 @@
         """
         Public method to get flags for an item.
         
-        @param index index of the node cell (QModelIndex)
-        @return flags (Qt.ItemFlags)
+        @param index index of the node cell
+        @type QModelIndex
+        @return flags
+        @rtype Qt.ItemFlags
         """
         if index.row() < 0 or index.row() >= self.rowCount(index.parent()):
             return Qt.NoItemFlags
@@ -109,8 +122,10 @@
         """
         Public method to return the mime data.
         
-        @param indexes list of indexes (QModelIndexList)
-        @return mime data (QMimeData)
+        @param indexes list of indexes
+        @type QModelIndexList
+        @return mime data
+        @rtype QMimeData
         """
         mimeData = QMimeData()
         urls = []
--- a/WebBrowser/Download/DownloadUtilities.py	Sat Apr 07 16:40:21 2018 +0200
+++ b/WebBrowser/Download/DownloadUtilities.py	Sun Apr 08 15:54:34 2018 +0200
@@ -11,49 +11,82 @@
 
 from PyQt5.QtCore import QCoreApplication
 
-from Globals import translate
-
 
 def timeString(timeRemaining):
     """
     Module function to format the given time.
     
-    @param timeRemaining time to be formatted (float)
-    @return time string (string)
+    @param timeRemaining time to be formatted
+    @type float
+    @return time string
+    @rtype str
     """
-    if timeRemaining > 60:
-        minutes = int(timeRemaining / 60)
-        seconds = int(timeRemaining % 60)
-        remaining = translate(
+    if timeRemaining < 10:
+        return QCoreApplication.translate(
+            "DownloadUtilities", "few seconds remaining")
+    elif timeRemaining < 60:    # < 1 minute
+        return QCoreApplication.translate(
             "DownloadUtilities",
-            "%n:{0:02} minutes remaining", "",
-            minutes).format(seconds)
+            "%n seconds remaining", "", int(timeRemaining))
+    elif timeRemaining < 3600:  # < 1 hour
+        return QCoreApplication.translate(
+            "DownloadUtilities",
+            "%n minutes remaining", "", int(timeRemaining / 60))
     else:
-        seconds = int(timeRemaining)
-        remaining = translate(
+        QCoreApplication.translate(
             "DownloadUtilities",
-            "%n seconds remaining", "", seconds)
-    
-    return remaining
+            "%n hours remaining", "", int(timeRemaining / 3600))
 
 
 def dataString(size):
     """
     Module function to generate a formatted size string.
     
-    @param size size to be formatted (integer)
-    @return formatted data string (string)
+    @param size size to be formatted
+    @type int
+    @return formatted data string
+    @rtype str
     """
-    unit = ""
     if size < 1024:
-        unit = QCoreApplication.translate("DownloadUtilities", "Bytes")
+        return QCoreApplication.translate(
+            "DownloadUtilities", "{0:.1f} Bytes").format(size)
     elif size < 1024 * 1024:
         size /= 1024
-        unit = QCoreApplication.translate("DownloadUtilities", "KiB")
+        return QCoreApplication.translate(
+            "DownloadUtilities", "{0:.1f} KiB").format(size)
     elif size < 1024 * 1024 * 1024:
         size /= 1024 * 1024
-        unit = QCoreApplication.translate("DownloadUtilities", "MiB")
+        return QCoreApplication.translate(
+            "DownloadUtilities", "{0:.2f} MiB").format(size)
     else:
         size /= 1024 * 1024 * 1024
-        unit = QCoreApplication.translate("DownloadUtilities", "GiB")
-    return "{0:.1f} {1}".format(size, unit)
+        return QCoreApplication.translate(
+            "DownloadUtilities", "{0:.2f} GiB").format(size)
+
+
+def speedString(speed):
+    """
+    Module function to generate a formatted speed string.
+    
+    @param speed speed to be formatted
+    @type float
+    @return formatted speed string
+    @rtype str
+    """
+    if speed < 0:
+        return QCoreApplication.translate("DownloadUtilities", "Unknown speed")
+    
+    speed /= 1024       # kB
+    if speed < 1024:
+        return QCoreApplication.translate(
+            "DownloadUtilities", "{0:.1f} KiB/s").format(speed)
+    
+    speed /= 1024       # MB
+    if speed < 1024:
+        return QCoreApplication.translate(
+            "DownloadUtilities", "{0:.2f} MiB/s").format(speed)
+    
+    speed /= 1024       # GB
+    if speed < 1024:
+        return QCoreApplication.translate(
+            "DownloadUtilities", "{0:.2f} GiB/s").format(speed)
--- a/WebBrowser/WebBrowserTabWidget.py	Sat Apr 07 16:40:21 2018 +0200
+++ b/WebBrowser/WebBrowserTabWidget.py	Sun Apr 08 15:54:34 2018 +0200
@@ -560,6 +560,16 @@
         for index in range(self.count() - 1, -1, -1):
             self.closeBrowserAt(index, shutdown=shutdown)
     
+    def closeBrowserView(self, browser):
+        """
+        Public method to close the given browser.
+        
+        @param browser reference to the web browser view to be closed
+        @type WebBrowserView
+        """
+        index = self.indexOf(browser)
+        self.closeBrowserAt(index)
+    
     def closeBrowserAt(self, index, shutdown=False):
         """
         Public slot to close a browser based on its index.
@@ -1050,6 +1060,22 @@
         """
         return self.__stackedUrlBar.currentWidget()
     
+    def urlBarForView(self, view):
+        """
+        Public method to get a reference to the UrlBar associated with the
+        given view.
+        
+        @param view reference to the view to get the urlbar for
+        @type WebBrowserView
+        @return reference to the associated urlbar
+        @rtype UrlBar
+        """
+        for urlbar in self.__stackedUrlBar.urlBars():
+            if urlbar.browser() is view:
+                return urlbar
+        
+        return None
+    
     def __lineEditReturnPressed(self, edit):
         """
         Private slot to handle the entering of an URL.
--- a/WebBrowser/WebBrowserView.py	Sat Apr 07 16:40:21 2018 +0200
+++ b/WebBrowser/WebBrowserView.py	Sun Apr 08 15:54:34 2018 +0200
@@ -95,6 +95,7 @@
         self.__createNewPage()
         
         self.__mw = mainWindow
+        self.__tabWidget = parent
         self.__isLoading = False
         self.__progress = 0
         self.__siteIconLoader = None
@@ -182,6 +183,16 @@
         """
         return self.__mw
     
+    def tabWidget(self):
+        """
+        Public method to get a reference to the tab widget containing this
+        view.
+        
+        @return reference to the tab widget
+        @rtype WebBrowserTabWidget
+        """
+        return self.__tabWidget
+    
     def load(self, url):
         """
         Public method to load a web site.
--- a/WebBrowser/WebBrowserWindow.py	Sat Apr 07 16:40:21 2018 +0200
+++ b/WebBrowser/WebBrowserWindow.py	Sun Apr 08 15:54:34 2018 +0200
@@ -2915,6 +2915,7 @@
         
         if WebBrowserWindow._downloadManager is not None and \
                 not self.downloadManager().allowQuit():
+            self.downloadManager().show()
             return False
         
         WebBrowserWindow._performingShutdown = True
@@ -3138,6 +3139,21 @@
         """
         return cls._isPrivate
     
+    def closeCurrentBrowser(self):
+        """
+        Public method to close the current web browser.
+        """
+        self.__tabWidget.closeBrowser()
+    
+    def closeBrowser(self, browser):
+        """
+        Public method to close the given browser.
+        
+        @param browser reference to the web browser view to be closed
+        @type WebBrowserView
+        """
+        self.__tabWidget.closeBrowserView(browser)
+    
     def currentBrowser(self):
         """
         Public method to get a reference to the current web browser.
--- a/changelog	Sat Apr 07 16:40:21 2018 +0200
+++ b/changelog	Sun Apr 08 15:54:34 2018 +0200
@@ -2,6 +2,8 @@
 ----------
 Version 18.05:
 - bug fixes
+- Web Browser (NG)
+  -- improvement of the download manager
 - Third Party packages
   -- updated coverage.py to 4.5.1
 

eric ide

mercurial