diff -r 96232974dcdb -r 645c12de6b0c PluginManager/PluginRepositoryDialog.py --- 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()