PluginManager/PluginRepositoryDialog.py

branch
Py2 comp.
changeset 3484
645c12de6b0c
parent 3178
f25fc1364c88
parent 3345
071afe8be2a1
child 3656
441956d8fce5
--- a/PluginManager/PluginRepositoryDialog.py	Sun Mar 30 22:00:14 2014 +0200
+++ b/PluginManager/PluginRepositoryDialog.py	Thu Apr 03 23:05:31 2014 +0200
@@ -13,11 +13,12 @@
 import sys
 import os
 import zipfile
+import glob
 
 from PyQt4.QtCore import pyqtSignal, pyqtSlot, Qt, QFile, QIODevice, QUrl, \
-    QProcess
+    QProcess, QPoint
 from PyQt4.QtGui import QWidget, QDialogButtonBox, QAbstractButton, \
-    QTreeWidgetItem, QDialog, QVBoxLayout
+    QTreeWidgetItem, QDialog, QVBoxLayout, QMenu
 from PyQt4.QtNetwork import QNetworkAccessManager, QNetworkRequest, \
     QNetworkReply
 
@@ -41,11 +42,6 @@
 
 from eric5config import getConfig
 
-descrRole = Qt.UserRole
-urlRole = Qt.UserRole + 1
-filenameRole = Qt.UserRole + 2
-authorRole = Qt.UserRole + 3
-
 
 class PluginRepositoryWidget(QWidget, Ui_PluginRepositoryDialog):
     """
@@ -56,6 +52,16 @@
     """
     closeAndInstall = pyqtSignal()
     
+    DescrRole = Qt.UserRole
+    UrlRole = Qt.UserRole + 1
+    FilenameRole = Qt.UserRole + 2
+    AuthorRole = Qt.UserRole + 3
+
+    PluginStatusUpToDate = 0
+    PluginStatusNew = 1
+    PluginStatusLocalUpdate = 2
+    PluginStatusRemoteUpdate = 3
+    
     def __init__(self, parent=None, external=False):
         """
         Constructor
@@ -68,18 +74,18 @@
         self.setupUi(self)
         
         self.__updateButton = self.buttonBox.addButton(
-            self.trUtf8("Update"), QDialogButtonBox.ActionRole)
+            self.tr("Update"), QDialogButtonBox.ActionRole)
         self.__downloadButton = self.buttonBox.addButton(
-            self.trUtf8("Download"), QDialogButtonBox.ActionRole)
+            self.tr("Download"), QDialogButtonBox.ActionRole)
         self.__downloadButton.setEnabled(False)
         self.__downloadInstallButton = self.buttonBox.addButton(
-            self.trUtf8("Download && Install"),
+            self.tr("Download && Install"),
             QDialogButtonBox.ActionRole)
         self.__downloadInstallButton.setEnabled(False)
         self.__downloadCancelButton = self.buttonBox.addButton(
-            self.trUtf8("Cancel"), QDialogButtonBox.ActionRole)
+            self.tr("Cancel"), QDialogButtonBox.ActionRole)
         self.__installButton = \
-            self.buttonBox.addButton(self.trUtf8("Close && Install"),
+            self.buttonBox.addButton(self.tr("Close && Install"),
                                      QDialogButtonBox.ActionRole)
         self.__downloadCancelButton.setEnabled(False)
         self.__installButton.setEnabled(False)
@@ -91,6 +97,18 @@
             self.repositoryList.columnCount(), "")
         self.repositoryList.header().setSortIndicator(0, Qt.AscendingOrder)
         
+        self.__pluginContextMenu = QMenu(self)
+        self.__hideAct = self.__pluginContextMenu.addAction(
+            self.tr("Hide"), self.__hidePlugin)
+        self.__hideSelectedAct = self.__pluginContextMenu.addAction(
+            self.tr("Hide Selected"), self.__hideSelectedPlugins)
+        self.__pluginContextMenu.addSeparator()
+        self.__showAllAct = self.__pluginContextMenu.addAction(
+            self.tr("Show All"), self.__showAllPlugins)
+        self.__pluginContextMenu.addSeparator()
+        self.__pluginContextMenu.addAction(
+            self.tr("Cleanup Downloads"), self.__cleanupDownloads)
+        
         self.pluginRepositoryFile = \
             os.path.join(Utilities.getConfigDir(), "PluginRepository")
         
@@ -112,6 +130,8 @@
         self.__isDownloadInstall = False
         self.__allDownloadedOk = False
         
+        self.__hiddenPlugins = Preferences.getPluginManager("HiddenPlugins")
+        
         self.__populateList()
     
     @pyqtSlot(QAbstractButton)
@@ -133,7 +153,7 @@
         elif button == self.__downloadCancelButton:
             self.__downloadCancel()
         elif button == self.__installButton:
-            self.closeAndInstall.emit()
+            self.__closeAndInstall()
     
     def __formatDescription(self, lines):
         """
@@ -159,6 +179,21 @@
         # join lines by a blank
         return ' '.join(newlines)
     
+    @pyqtSlot(QPoint)
+    def on_repositoryList_customContextMenuRequested(self, pos):
+        """
+        Private slot to show the context menu.
+        
+        @param pos position to show the menu (QPoint)
+        """
+        self.__hideAct.setEnabled(
+            self.repositoryList.currentItem() is not None and
+            len(self.__selectedItems()) == 1)
+        self.__hideSelectedAct.setEnabled(
+            len(self.__selectedItems()) > 1)
+        self.__showAllAct.setEnabled(bool(self.__hasHiddenPlugins()))
+        self.__pluginContextMenu.popup(self.repositoryList.mapToGlobal(pos))
+    
     @pyqtSlot(QTreeWidgetItem, QTreeWidgetItem)
     def on_repositoryList_currentItemChanged(self, current, previous):
         """
@@ -170,11 +205,14 @@
         if self.__repositoryMissing or current is None:
             return
         
-        self.urlEdit.setText(current.data(0, urlRole) or "")
+        self.urlEdit.setText(
+            current.data(0, PluginRepositoryWidget.UrlRole) or "")
         self.descriptionEdit.setPlainText(
-            current.data(0, descrRole) and
-            self.__formatDescription(current.data(0, descrRole)) or "")
-        self.authorEdit.setText(current.data(0, authorRole) or "")
+            current.data(0, PluginRepositoryWidget.DescrRole) and
+            self.__formatDescription(
+                current.data(0, PluginRepositoryWidget.DescrRole)) or "")
+        self.authorEdit.setText(
+            current.data(0, PluginRepositoryWidget.AuthorRole) or "")
     
     def __selectedItems(self):
         """
@@ -196,6 +234,7 @@
         """
         self.__downloadButton.setEnabled(len(self.__selectedItems()))
         self.__downloadInstallButton.setEnabled(len(self.__selectedItems()))
+        self.__installButton.setEnabled(len(self.__selectedItems()))
     
     def __updateList(self):
         """
@@ -253,10 +292,10 @@
         for itm in self.repositoryList.selectedItems():
             if itm not in [self.__stableItem, self.__unstableItem,
                            self.__unknownItem]:
-                url = itm.data(0, urlRole)
+                url = itm.data(0, PluginRepositoryWidget.UrlRole)
                 filename = os.path.join(
                     Preferences.getPluginManager("DownloadPath"),
-                    itm.data(0, filenameRole))
+                    itm.data(0, PluginRepositoryWidget.FilenameRole))
                 self.__pluginsToDownload.append((url, filename))
         self.__downloadPlugin()
     
@@ -275,8 +314,8 @@
         if ui and ui.notificationsEnabled():
             ui.showNotification(
                 UI.PixmapCache.getPixmap("plugin48.png"),
-                self.trUtf8("Download Plugin Files"),
-                self.trUtf8("""The requested plugins were downloaded."""))
+                self.tr("Download Plugin Files"),
+                self.tr("""The requested plugins were downloaded."""))
         
         if self.__isDownloadInstall:
             self.closeAndInstall.emit()
@@ -284,8 +323,8 @@
             if ui is None or not ui.notificationsEnabled():
                 E5MessageBox.information(
                     self,
-                    self.trUtf8("Download Plugin Files"),
-                    self.trUtf8("""The requested plugins were downloaded."""))
+                    self.tr("Download Plugin Files"),
+                    self.tr("""The requested plugins were downloaded."""))
             self.downloadProgress.setValue(0)
             
             # repopulate the list to update the refresh icons
@@ -327,23 +366,23 @@
                     self.repositoryUrlEdit.setText(url)
                     E5MessageBox.warning(
                         self,
-                        self.trUtf8("Plugins Repository URL Changed"),
-                        self.trUtf8(
+                        self.tr("Plugins Repository URL Changed"),
+                        self.tr(
                             """The URL of the Plugins Repository has"""
                             """ changed. Select the "Update" button to get"""
                             """ the new repository file."""))
             else:
                 E5MessageBox.critical(
                     self,
-                    self.trUtf8("Read plugins repository file"),
-                    self.trUtf8("<p>The plugins repository file <b>{0}</b> "
-                                "could not be read. Select Update</p>")
+                    self.tr("Read plugins repository file"),
+                    self.tr("<p>The plugins repository file <b>{0}</b> "
+                            "could not be read. Select Update</p>")
                     .format(self.pluginRepositoryFile))
         else:
             self.__repositoryMissing = True
             QTreeWidgetItem(
                 self.repositoryList,
-                ["", self.trUtf8(
+                ["", self.tr(
                     "No plugin repository file available.\nSelect Update.")
                  ])
             self.repositoryList.resizeColumnToContents(1)
@@ -373,7 +412,7 @@
         request.setAttribute(QNetworkRequest.CacheLoadControlAttribute,
                              QNetworkRequest.AlwaysNetwork)
         reply = self.__networkManager.get(request)
-        reply.finished[()].connect(self.__downloadFileDone)
+        reply.finished.connect(self.__downloadFileDone)
         reply.downloadProgress.connect(self.__downloadProgress)
         self.__replies.append(reply)
     
@@ -395,8 +434,8 @@
             if not self.__downloadCancelled:
                 E5MessageBox.warning(
                     self,
-                    self.trUtf8("Error downloading file"),
-                    self.trUtf8(
+                    self.tr("Error downloading file"),
+                    self.tr(
                         """<p>Could not download the requested file"""
                         """ from {0}.</p><p>Error: {1}</p>"""
                     ).format(self.__downloadURL, reply.errorString())
@@ -463,57 +502,78 @@
         @param filename data for the filename field (string)
         @param status status of the plugin (string [stable, unstable, unknown])
         """
+        pluginName = filename.rsplit("-", 1)[0]
+        if pluginName in self.__hiddenPlugins:
+            return
+        
         if status == "stable":
             if self.__stableItem is None:
                 self.__stableItem = \
                     QTreeWidgetItem(self.repositoryList,
-                                    [self.trUtf8("Stable")])
+                                    [self.tr("Stable")])
                 self.__stableItem.setExpanded(True)
             parent = self.__stableItem
         elif status == "unstable":
             if self.__unstableItem is None:
                 self.__unstableItem = \
                     QTreeWidgetItem(self.repositoryList,
-                                    [self.trUtf8("Unstable")])
+                                    [self.tr("Unstable")])
                 self.__unstableItem.setExpanded(True)
             parent = self.__unstableItem
         else:
             if self.__unknownItem is None:
                 self.__unknownItem = \
                     QTreeWidgetItem(self.repositoryList,
-                                    [self.trUtf8("Unknown")])
+                                    [self.tr("Unknown")])
                 self.__unknownItem.setExpanded(True)
             parent = self.__unknownItem
         itm = QTreeWidgetItem(parent, [name, version, short])
         
-        itm.setData(0, urlRole, url)
-        itm.setData(0, filenameRole, filename)
-        itm.setData(0, authorRole, author)
-        itm.setData(0, descrRole, description)
+        itm.setData(0, PluginRepositoryWidget.UrlRole, url)
+        itm.setData(0, PluginRepositoryWidget.FilenameRole, filename)
+        itm.setData(0, PluginRepositoryWidget.AuthorRole, author)
+        itm.setData(0, PluginRepositoryWidget.DescrRole, description)
         
-        if self.__isUpToDate(filename, version):
+        updateStatus = self.__updateStatus(filename, version)
+        if updateStatus == PluginRepositoryWidget.PluginStatusUpToDate:
             itm.setIcon(1, UI.PixmapCache.getIcon("empty.png"))
-        else:
+            itm.setToolTip(1, self.tr("up-to-date"))
+        elif updateStatus == PluginRepositoryWidget.PluginStatusNew:
             itm.setIcon(1, UI.PixmapCache.getIcon("download.png"))
+            itm.setToolTip(1, self.tr("new download available"))
+        elif updateStatus == PluginRepositoryWidget.PluginStatusLocalUpdate:
+            itm.setIcon(1, UI.PixmapCache.getIcon("updateLocal.png"))
+            itm.setToolTip(1, self.tr("update installable"))
+        elif updateStatus == PluginRepositoryWidget.PluginStatusRemoteUpdate:
+            itm.setIcon(1, UI.PixmapCache.getIcon("updateRemote.png"))
+            itm.setToolTip(1, self.tr("updated download available"))
     
-    def __isUpToDate(self, filename, version):
+    def __updateStatus(self, filename, version):
         """
-        Private method to check, if the given archive is up-to-date.
+        Private method to check, if the given archive update status.
         
         @param filename data for the filename field (string)
         @param version data for the version field (string)
-        @return flag indicating up-to-date (boolean)
+        @return plug-in update status (integer, one of PluginStatusNew,
+            PluginStatusUpToDate, PluginStatusLocalUpdate,
+            PluginStatusRemoteUpdate)
         """
         archive = os.path.join(Preferences.getPluginManager("DownloadPath"),
                                filename)
-
+        
+        # check, if it is an update (i.e. we already have archives
+        # with the same pattern)
+        archivesPattern = archive.rsplit('-', 1)[0] + "-*.zip"
+        if len(glob.glob(archivesPattern)) == 0:
+            return PluginRepositoryWidget.PluginStatusNew
+        
         # check, if the archive exists
         if not os.path.exists(archive):
-            return False
+            return PluginRepositoryWidget.PluginStatusRemoteUpdate
         
         # check, if the archive is a valid zip file
         if not zipfile.is_zipfile(archive):
-            return False
+            return PluginRepositoryWidget.PluginStatusRemoteUpdate
         
         zip = zipfile.ZipFile(archive, "r")
         try:
@@ -522,7 +582,18 @@
             aversion = ""
         zip.close()
         
-        return aversion == version
+        if aversion == version:
+            if not self.__external:
+                # Check against installed/loaded plug-ins
+                pluginManager = e5App().getObject("PluginManager")
+                pluginName = filename.rsplit('-', 1)[0]
+                pluginDetails = pluginManager.getPluginDetails(pluginName)
+                if pluginDetails is None or pluginDetails["version"] < version:
+                    return PluginRepositoryWidget.PluginStatusLocalUpdate
+            
+            return PluginRepositoryWidget.PluginStatusUpToDate
+        else:
+            return PluginRepositoryWidget.PluginStatusRemoteUpdate
     
     def __sslErrors(self, reply, errors):
         """
@@ -552,6 +623,104 @@
         @param checked state of the push button (boolean)
         """
         self.repositoryUrlEdit.setReadOnly(not checked)
+    
+    def __closeAndInstall(self):
+        """
+        Private method to close the dialog and invoke the install dialog.
+        """
+        if not self.__pluginsDownloaded and self.__selectedItems():
+            for itm in self.__selectedItems():
+                filename = os.path.join(
+                    Preferences.getPluginManager("DownloadPath"),
+                    itm.data(0, PluginRepositoryWidget.FilenameRole))
+                self.__pluginsDownloaded.append(filename)
+        self.closeAndInstall.emit()
+    
+    def __hidePlugin(self):
+        """
+        Private slot to hide the current plug-in.
+        """
+        itm = self.__selectedItems()[0]
+        pluginName = (itm.data(0, PluginRepositoryWidget.FilenameRole)
+                      .rsplit("-", 1)[0])
+        self.__updateHiddenPluginsList([pluginName])
+    
+    def __hideSelectedPlugins(self):
+        """
+        Private slot to hide all selected plug-ins.
+        """
+        hideList = []
+        for itm in self.__selectedItems():
+            pluginName = (itm.data(0, PluginRepositoryWidget.FilenameRole)
+                          .rsplit("-", 1)[0])
+            hideList.append(pluginName)
+        self.__updateHiddenPluginsList(hideList)
+    
+    def __showAllPlugins(self):
+        """
+        Private slot to show all plug-ins.
+        """
+        self.__hiddenPlugins = []
+        self.__updateHiddenPluginsList([])
+    
+    def __hasHiddenPlugins(self):
+        """
+        Private method to check, if there are any hidden plug-ins.
+        
+        @return flag indicating the presence of hidden plug-ins (boolean)
+        """
+        return bool(self.__hiddenPlugins)
+    
+    def __updateHiddenPluginsList(self, hideList):
+        """
+        Private method to store the list of hidden plug-ins to the settings.
+        
+        @param hideList list of plug-ins to add to the list of hidden ones
+            (list of string)
+        """
+        if hideList:
+            self.__hiddenPlugins.extend(
+                [p for p in hideList if p not in self.__hiddenPlugins])
+        Preferences.setPluginManager("HiddenPlugins", self.__hiddenPlugins)
+        self.__populateList()
+    
+    def __cleanupDownloads(self):
+        """
+        Private slot to cleanup the plug-in downloads area.
+        """
+        downloadPath = Preferences.getPluginManager("DownloadPath")
+        downloads = {}  # plug-in name as key, file name as value
+        
+        # step 1: extract plug-ins and downloaded files
+        for pluginFile in os.listdir(downloadPath):
+            if not os.path.isfile(os.path.join(downloadPath, pluginFile)):
+                continue
+            
+            pluginName = pluginFile.rsplit("-", 1)[0]
+            if pluginName not in downloads:
+                downloads[pluginName] = []
+            downloads[pluginName].append(pluginFile)
+        
+        # step 2: delete old entries
+        for pluginName in downloads:
+            downloads[pluginName].sort()
+        
+            if pluginName in self.__hiddenPlugins and \
+                    not Preferences.getPluginManager("KeepHidden"):
+                removeFiles = downloads[pluginName]
+            else:
+                removeFiles = downloads[pluginName][
+                    :-Preferences.getPluginManager("KeepGenerations")]
+            for removeFile in removeFiles:
+                try:
+                    os.remove(os.path.join(downloadPath, removeFile))
+                except (IOError, OSError) as err:
+                    E5MessageBox.critical(
+                        self,
+                        self.tr("Cleanup of Plugin Downloads"),
+                        self.tr("""<p>The plugin download <b>{0}</b> could"""
+                                """ not be deleted.</p><p>Reason: {1}</p>""")
+                        .format(removeFile, str(err)))
 
 
 class PluginRepositoryDialog(QDialog):
@@ -575,9 +744,10 @@
         size = self.cw.size()
         self.__layout.addWidget(self.cw)
         self.resize(size)
+        self.setWindowTitle(self.cw.windowTitle())
         
-        self.cw.buttonBox.accepted[()].connect(self.accept)
-        self.cw.buttonBox.rejected[()].connect(self.reject)
+        self.cw.buttonBox.accepted.connect(self.accept)
+        self.cw.buttonBox.rejected.connect(self.reject)
         self.cw.closeAndInstall.connect(self.__closeAndInstall)
         
     def __closeAndInstall(self):
@@ -610,12 +780,13 @@
         size = self.cw.size()
         self.setCentralWidget(self.cw)
         self.resize(size)
+        self.setWindowTitle(self.cw.windowTitle())
         
         self.setStyle(Preferences.getUI("Style"),
                       Preferences.getUI("StyleSheet"))
         
-        self.cw.buttonBox.accepted[()].connect(self.close)
-        self.cw.buttonBox.rejected[()].connect(self.close)
+        self.cw.buttonBox.accepted.connect(self.close)
+        self.cw.buttonBox.rejected.connect(self.close)
         self.cw.closeAndInstall.connect(self.__startPluginInstall)
     
     def __startPluginInstall(self):
@@ -633,11 +804,11 @@
                 not proc.startDetached(sys.executable, args):
             E5MessageBox.critical(
                 self,
-                self.trUtf8('Process Generation Error'),
-                self.trUtf8(
+                self.tr('Process Generation Error'),
+                self.tr(
                     '<p>Could not start the process.<br>'
                     'Ensure that it is available as <b>{0}</b>.</p>'
                 ).format(applPath),
-                self.trUtf8('OK'))
+                self.tr('OK'))
         
         self.close()

eric ide

mercurial