--- a/PluginManager/PluginRepositoryDialog.py Sat Jan 11 20:03:38 2014 +0100 +++ b/PluginManager/PluginRepositoryDialog.py Sun Jan 12 18:02:33 2014 +0100 @@ -11,11 +11,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 @@ -39,11 +40,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): """ @@ -54,6 +50,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 @@ -89,6 +95,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") @@ -110,6 +128,8 @@ self.__isDownloadInstall = False self.__allDownloadedOk = False + self.__hiddenPlugins = Preferences.getPluginManager("HiddenPlugins") + self.__populateList() @pyqtSlot(QAbstractButton) @@ -131,7 +151,7 @@ elif button == self.__downloadCancelButton: self.__downloadCancel() elif button == self.__installButton: - self.closeAndInstall.emit() + self.__closeAndInstall() def __formatDescription(self, lines): """ @@ -157,6 +177,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): """ @@ -168,11 +203,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): """ @@ -194,6 +232,7 @@ """ self.__downloadButton.setEnabled(len(self.__selectedItems())) self.__downloadInstallButton.setEnabled(len(self.__selectedItems())) + self.__installButton.setEnabled(len(self.__selectedItems())) def __updateList(self): """ @@ -251,10 +290,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() @@ -461,6 +500,10 @@ @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 = \ @@ -484,34 +527,51 @@ 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: @@ -520,7 +580,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): """ @@ -550,6 +621,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):