PluginManager/PluginRepositoryDialog.py

changeset 0
de9c2efb9d02
child 7
c679fb30c8f3
diff -r 000000000000 -r de9c2efb9d02 PluginManager/PluginRepositoryDialog.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/PluginManager/PluginRepositoryDialog.py	Mon Dec 28 16:03:33 2009 +0000
@@ -0,0 +1,647 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2007 - 2009 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+
+"""
+Module implementing a dialog showing the available plugins.
+"""
+
+import sys
+import os
+import zipfile
+import cStringIO
+
+from PyQt4.QtGui import *
+from PyQt4.QtCore import *
+from PyQt4.QtNetwork import QHttp, QNetworkProxy
+
+from Ui_PluginRepositoryDialog import Ui_PluginRepositoryDialog
+
+from UI.AuthenticationDialog import AuthenticationDialog
+
+from E4XML.XMLUtilities import make_parser
+from E4XML.XMLErrorHandler import XMLErrorHandler, XMLFatalParseError
+from E4XML.XMLEntityResolver import XMLEntityResolver
+from E4XML.PluginRepositoryHandler import PluginRepositoryHandler
+
+import Utilities
+import Preferences
+
+import UI.PixmapCache
+
+from eric4config import getConfig
+
+descrRole    = Qt.UserRole
+urlRole      = Qt.UserRole + 1
+filenameRole = Qt.UserRole + 2
+authorRole   = Qt.UserRole + 3
+
+class PluginRepositoryWidget(QWidget, Ui_PluginRepositoryDialog):
+    """
+    Class implementing a dialog showing the available plugins.
+    
+    @signal closeAndInstall emitted when the Close & Install button is pressed
+    """
+    def __init__(self, parent = None):
+        """
+        Constructor
+        
+        @param parent parent of this dialog (QWidget)
+        """
+        QWidget.__init__(self, parent)
+        self.setupUi(self)
+        
+        self.__updateButton = \
+            self.buttonBox.addButton(self.trUtf8("Update"), QDialogButtonBox.ActionRole)
+        self.__downloadButton = \
+            self.buttonBox.addButton(self.trUtf8("Download"), QDialogButtonBox.ActionRole)
+        self.__downloadButton.setEnabled(False)
+        self.__downloadCancelButton = \
+            self.buttonBox.addButton(self.trUtf8("Cancel"), QDialogButtonBox.ActionRole)
+        self.__installButton = \
+            self.buttonBox.addButton(self.trUtf8("Close && Install"), 
+                                     QDialogButtonBox.ActionRole)
+        self.__downloadCancelButton.setEnabled(False)
+        self.__installButton.setEnabled(False)
+        
+        self.repositoryList.headerItem().setText(self.repositoryList.columnCount(), "")
+        self.repositoryList.header().setSortIndicator(0, Qt.AscendingOrder)
+        
+        self.pluginRepositoryFile = \
+            os.path.join(Utilities.getConfigDir(), "PluginRepository")
+        
+        self.__http = None
+        self.__doneMethod = None
+        self.__inDownload = False
+        self.__pluginsToDownload = []
+        self.__pluginsDownloaded = []
+        
+        self.__populateList()
+    
+    @pyqtSlot(QAbstractButton)
+    def on_buttonBox_clicked(self, button):
+        """
+        Private slot to handle the click of a button of the button box.
+        """
+        if button == self.__updateButton:
+            self.__updateList()
+        elif button == self.__downloadButton:
+            self.__downloadPlugins()
+        elif button == self.__downloadCancelButton:
+            self.__downloadCancel()
+        elif button == self.__installButton:
+            self.emit(SIGNAL("closeAndInstall"))
+    
+    def __formatDescription(self, lines):
+        """
+        Private method to format the description.
+        
+        @param lines lines of the description (list of strings)
+        @return formatted description (string)
+        """
+        # remove empty line at start and end
+        newlines = lines[:]
+        if len(newlines) and newlines[0] == '':
+            del newlines[0]
+        if len(newlines) and newlines[-1] == '':
+            del newlines[-1]
+        
+        # replace empty lines by newline character
+        index = 0
+        while index < len(newlines):
+            if newlines[index] == '':
+                newlines[index] = '\n'
+            index += 1
+        
+        # join lines by a blank
+        return ' '.join(newlines)
+    
+    @pyqtSlot(QTreeWidgetItem, QTreeWidgetItem)
+    def on_repositoryList_currentItemChanged(self, current, previous):
+        """
+        Private slot to handle the change of the current item.
+        
+        @param current reference to the new current item (QTreeWidgetItem)
+        @param previous reference to the old current item (QTreeWidgetItem)
+        """
+        if self.__repositoryMissing or current is None:
+            return
+        
+        self.urlEdit.setText(current.data(0, urlRole).toString())
+        self.descriptionEdit.setPlainText(
+            self.__formatDescription(current.data(0, descrRole).toStringList()))
+        self.authorEdit.setText(current.data(0, authorRole).toString())
+    
+    def __selectedItems(self):
+        """
+        Private method to get all selected items without the toplevel ones.
+        
+        @return list of selected items (list)
+        """
+        ql = self.repositoryList.selectedItems()
+        for index in range(self.repositoryList.topLevelItemCount()):
+            ti = self.repositoryList.topLevelItem(index)
+            if ti in ql:
+                ql.remove(ti)
+        return ql
+    
+    @pyqtSlot()
+    def on_repositoryList_itemSelectionChanged(self):
+        """
+        Private slot to handle a change of the selection.
+        """
+        self.__downloadButton.setEnabled(len(self.__selectedItems()))
+    
+    def __updateList(self):
+        """
+        Private slot to download a new list and display the contents.
+        """
+        url = Preferences.getUI("PluginRepositoryUrl5")
+        self.__downloadFile(url, 
+                            self.pluginRepositoryFile, 
+                            self.__downloadRepositoryFileDone)
+    
+    def __downloadRepositoryFileDone(self, status, filename):
+        """
+        Private method called after the repository file was downloaded.
+        
+        @param status flaging indicating a successful download (boolean)
+        @param filename full path of the downloaded file (string)
+        """
+        self.__populateList()
+    
+    def __downloadPluginDone(self, status, filename):
+        """
+        Private method called, when the download of a plugin is finished.
+        
+        @param status flaging indicating a successful download (boolean)
+        @param filename full path of the downloaded file (string)
+        """
+        if status:
+            self.__pluginsDownloaded.append(filename)
+        
+        del self.__pluginsToDownload[0]
+        if len(self.__pluginsToDownload):
+            self.__downloadPlugin()
+        else:
+            self.__downloadPluginsDone()
+    
+    def __downloadPlugin(self):
+        """
+        Private method to download the next plugin.
+        """
+        self.__downloadFile(self.__pluginsToDownload[0][0], 
+                            self.__pluginsToDownload[0][1], 
+                            self.__downloadPluginDone)
+    
+    def __downloadPlugins(self):
+        """
+        Private slot to download the selected plugins.
+        """
+        self.__pluginsDownloaded = []
+        self.__pluginsToDownload = []
+        self.__downloadButton.setEnabled(False)
+        self.__installButton.setEnabled(False)
+        for itm in self.repositoryList.selectedItems():
+            if itm not in [self.__stableItem, self.__unstableItem, self.__unknownItem]:
+                url = itm.data(0, urlRole).toString()
+                filename = os.path.join(
+                    Preferences.getPluginManager("DownloadPath"),
+                    itm.data(0, filenameRole).toString())
+                self.__pluginsToDownload.append((url, filename))
+        self.__downloadPlugin()
+    
+    def __downloadPluginsDone(self):
+        """
+        Private method called, when the download of the plugins is finished.
+        """
+        self.__downloadButton.setEnabled(len(self.__selectedItems()))
+        self.__installButton.setEnabled(True)
+        self.__doneMethod = None
+        QMessageBox.information(None,
+            self.trUtf8("Download Plugin Files"),
+            self.trUtf8("""The requested plugins were downloaded."""))
+        self.downloadProgress.setValue(0)
+        
+        # repopulate the list to update the refresh icons
+        self.__populateList()
+    
+    def __resortRepositoryList(self):
+        """
+        Private method to resort the tree.
+        """
+        self.repositoryList.sortItems(self.repositoryList.sortColumn(), 
+                                      self.repositoryList.header().sortIndicatorOrder())
+    
+    def __populateList(self):
+        """
+        Private method to populate the list of available plugins.
+        """
+        self.repositoryList.clear()
+        self.__stableItem = None
+        self.__unstableItem = None
+        self.__unknownItem = None
+        
+        self.downloadProgress.setValue(0)
+        self.__doneMethod = None
+        
+        if os.path.exists(self.pluginRepositoryFile):
+            self.__repositoryMissing = False
+            try:
+                f = open(self.pluginRepositoryFile, "rb")
+                line = f.readline()
+                dtdLine = f.readline()
+                f.close()
+            except IOError:
+                QMessageBox.critical(None,
+                    self.trUtf8("Read plugins repository file"),
+                    self.trUtf8("<p>The plugins repository file <b>{0}</b> "
+                                "could not be read. Select Update</p>")\
+                        .format(self.pluginRepositoryFile))
+                return
+            
+            # now read the file
+            if line.startswith('<?xml'):
+                parser = make_parser(dtdLine.startswith("<!DOCTYPE"))
+                handler = PluginRepositoryHandler(self)
+                er = XMLEntityResolver()
+                eh = XMLErrorHandler()
+                
+                parser.setContentHandler(handler)
+                parser.setEntityResolver(er)
+                parser.setErrorHandler(eh)
+                
+                try:
+                    f = open(self.pluginRepositoryFile, "rb")
+                    try:
+                        try:
+                            parser.parse(f)
+                        except UnicodeEncodeError:
+                            f.seek(0)
+                            buf = cStringIO.StringIO(f.read())
+                            parser.parse(buf)
+                    finally:
+                        f.close()
+                except IOError:
+                    QMessageBox.critical(None,
+                        self.trUtf8("Read plugins repository file"),
+                        self.trUtf8("<p>The plugins repository file <b>{0}</b> "
+                                    "could not be read. Select Update</p>")\
+                            .format(self.pluginRepositoryFile))
+                    return
+                except XMLFatalParseError:
+                    pass
+                
+                eh.showParseMessages()
+                
+                self.repositoryList.resizeColumnToContents(0)
+                self.repositoryList.resizeColumnToContents(1)
+                self.repositoryList.resizeColumnToContents(2)
+                self.__resortRepositoryList()
+            else:
+                QMessageBox.critical(None,
+                    self.trUtf8("Read plugins repository file"),
+                    self.trUtf8("<p>The plugins repository file <b>{0}</b> "
+                                "has an unsupported format.</p>")\
+                        .format(self.pluginRepositoryFile))
+        else:
+            self.__repositoryMissing = True
+            QTreeWidgetItem(self.repositoryList, 
+                ["", 
+                 self.trUtf8("No plugin repository file available.\nSelect Update.")
+                ])
+            self.repositoryList.resizeColumnToContents(1)
+    
+    def __downloadFile(self, url, filename, doneMethod = None):
+        """
+        Private slot to download the given file.
+        
+        @param url URL for the download (string)
+        @param filename local name of the file (string)
+        @param doneMethod method to be called when done
+        """
+        if self.__http is None:
+            self.__http = QHttp()
+            self.connect(self.__http, SIGNAL("done(bool)"), self.__downloadFileDone)
+            self.connect(self.__http, SIGNAL("dataReadProgress(int, int)"), 
+                self.__dataReadProgress)
+            self.connect(self.__http, 
+                SIGNAL('proxyAuthenticationRequired(const QNetworkProxy &, QAuthenticator *)'),
+                self.__proxyAuthenticationRequired)
+            self.connect(self.__http, SIGNAL("sslErrors(const QList<QSslError>&)"), 
+                self.__sslErrors)
+        
+        if Preferences.getUI("UseProxy"):
+            host = Preferences.getUI("ProxyHost")
+            if not host:
+                QMessageBox.critical(None,
+                    self.trUtf8("Error downloading file"),
+                    self.trUtf8("""Proxy usage was activated"""
+                                """ but no proxy host configured."""))
+                return
+            else:
+                pProxyType = Preferences.getUI("ProxyType")
+                if pProxyType == 0:
+                    proxyType = QNetworkProxy.HttpProxy
+                elif pProxyType == 1:
+                    proxyType = QNetworkProxy.HttpCachingProxy
+                elif pProxyType == 2:
+                    proxyType = QNetworkProxy.Socks5Proxy
+                self.__proxy = QNetworkProxy(proxyType, host, 
+                    Preferences.getUI("ProxyPort"),
+                    Preferences.getUI("ProxyUser"),
+                    Preferences.getUI("ProxyPassword"))
+                self.__http.setProxy(self.__proxy)
+        
+        self.__updateButton.setEnabled(False)
+        self.__downloadButton.setEnabled(False)
+        self.__downloadCancelButton.setEnabled(True)
+        
+        self.statusLabel.setText(url)
+        
+        self.__doneMethod = doneMethod
+        self.__downloadURL = url
+        self.__downloadFileName = filename
+        self.__downloadIODevice = QFile(self.__downloadFileName + ".tmp")
+        self.__downloadCancelled = False
+        
+        if QUrl(url).scheme().lower() == 'https':
+            connectionMode = QHttp.ConnectionModeHttps
+        else:
+            connectionMode = QHttp.ConnectionModeHttp
+        self.__http.setHost(QUrl(url).host(), connectionMode, QUrl(url).port(0))
+        self.__http.get(QUrl(url).path(), self.__downloadIODevice)
+    
+    def __downloadFileDone(self, error):
+        """
+        Private method called, after the file has been downloaded
+        from the internet.
+        
+        @param error flag indicating an error condition (boolean)
+        """
+        self.__updateButton.setEnabled(True)
+        self.__downloadCancelButton.setEnabled(False)
+        self.statusLabel.setText("  ")
+        
+        ok = True
+        if error or self.__http.lastResponse().statusCode() != 200:
+            ok = False
+            if not self.__downloadCancelled:
+                if error:
+                    msg = self.__http.errorString()
+                else:
+                    msg = self.__http.lastResponse().reasonPhrase()
+                QMessageBox.warning(None,
+                    self.trUtf8("Error downloading file"),
+                    self.trUtf8(
+                        """<p>Could not download the requested file from {0}.</p>"""
+                        """<p>Error: {1}</p>"""
+                    ).format(self.__downloadURL, msg)
+                )
+            self.downloadProgress.setValue(0)
+            self.__downloadURL = None
+            self.__downloadIODevice.remove()
+            self.__downloadIODevice = None
+            if self.repositoryList.topLevelItemCount():
+                if self.repositoryList.currentItem() is None:
+                    self.repositoryList.setCurrentItem(
+                        self.repositoryList.topLevelItem(0))
+                else:
+                    self.__downloadButton.setEnabled(len(self.__selectedItems()))
+            return
+        
+        if QFile.exists(self.__downloadFileName):
+            QFile.remove(self.__downloadFileName)
+        self.__downloadIODevice.rename(self.__downloadFileName)
+        self.__downloadIODevice = None
+        self.__downloadURL = None
+        
+        if self.__doneMethod is not None:
+            self.__doneMethod(ok, self.__downloadFileName)
+    
+    def __downloadCancel(self):
+        """
+        Private slot to cancel the current download.
+        """
+        if self.__http is not None:
+            self.__downloadCancelled = True
+            self.__pluginsToDownload = []
+            self.__http.abort()
+    
+    def __dataReadProgress(self, done, total):
+        """
+        Private slot to show the download progress.
+        
+        @param done number of bytes downloaded so far (integer)
+        @param total total bytes to be downloaded (integer)
+        """
+        self.downloadProgress.setMaximum(total)
+        self.downloadProgress.setValue(done)
+    
+    def addEntry(self, name, short, description, url, author, version, filename, status):
+        """
+        Public method to add an entry to the list.
+        
+        @param name data for the name field (string)
+        @param short data for the short field (string)
+        @param description data for the description field (list of strings)
+        @param url data for the url field (string)
+        @param author data for the author field (string)
+        @param version data for the version field (string)
+        @param filename data for the filename field (string)
+        @param status status of the plugin (string [stable, unstable, unknown])
+        """
+        if status == "stable":
+            if self.__stableItem is None:
+                self.__stableItem = \
+                    QTreeWidgetItem(self.repositoryList, [self.trUtf8("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.__unstableItem.setExpanded(True)
+            parent = self.__unstableItem
+        else:
+            if self.__unknownItem is None:
+                self.__unknownItem = \
+                    QTreeWidgetItem(self.repositoryList, [self.trUtf8("Unknown")])
+                self.__unknownItem.setExpanded(True)
+            parent = self.__unknownItem
+        itm = QTreeWidgetItem(parent, [name, version, short])
+        
+        itm.setData(0, urlRole, QVariant(url))
+        itm.setData(0, filenameRole, QVariant(filename))
+        itm.setData(0, authorRole, QVariant(author))
+        itm.setData(0, descrRole, QVariant(description))
+        
+        if self.__isUpToDate(filename, version):
+            itm.setIcon(1, UI.PixmapCache.getIcon("empty.png"))
+        else:
+            itm.setIcon(1, UI.PixmapCache.getIcon("download.png"))
+    
+    def __isUpToDate(self, filename, version):
+        """
+        Private method to check, if the given archive is up-to-date.
+        
+        @param filename data for the filename field (string)
+        @param version data for the version field (string)
+        @return flag indicating up-to-date (boolean)
+        """
+        archive = os.path.join(Preferences.getPluginManager("DownloadPath"),
+                               filename)
+
+        # check, if the archive exists
+        if not os.path.exists(archive):
+            return False
+        
+        # check, if the archive is a valid zip file
+        if not zipfile.is_zipfile(archive):
+            return False
+        
+        zip = zipfile.ZipFile(archive, "r")
+        try:
+            aversion = zip.read("VERSION")
+        except KeyError:
+            aversion = ""
+        zip.close()
+        
+        return aversion == version
+    
+    def __proxyAuthenticationRequired(self, proxy, auth):
+        """
+        Private slot to handle a proxy authentication request.
+        
+        @param proxy reference to the proxy object (QNetworkProxy)
+        @param auth reference to the authenticator object (QAuthenticator)
+        """
+        info = self.trUtf8("<b>Connect to proxy '{0}' using:</b>")\
+            .format(Qt.escape(proxy.hostName()))
+        
+        dlg = AuthenticationDialog(info, proxy.user(), True)
+        if dlg.exec_() == QDialog.Accepted:
+            username, password = dlg.getData()
+            auth.setUser(username)
+            auth.setPassword(password)
+            if dlg.shallSave():
+                Preferences.setUI("ProxyUser", username)
+                Preferences.setUI("ProxyPassword", password)
+    
+    def __sslErrors(self, sslErrors):
+        """
+        Private slot to handle SSL errors.
+        
+        @param sslErrors list of SSL errors (list of QSslError)
+        """
+        errorStrings = []
+        for err in sslErrors:
+            errorStrings.append(err.errorString())
+        errorString = '.<br />'.join(errorStrings)
+        ret = QMessageBox.warning(self,
+            self.trUtf8("SSL Errors"),
+            self.trUtf8("""<p>SSL Errors:</p>"""
+                        """<p>{0}</p>"""
+                        """<p>Do you want to ignore these errors?</p>""")\
+                .format(errorString),
+            QMessageBox.StandardButtons(\
+                QMessageBox.No | \
+                QMessageBox.Yes),
+            QMessageBox.No)
+        if ret == QMessageBox.Yes:
+            self.__http.ignoreSslErrors()
+        else:
+            self.__downloadCancelled = True
+            self.__http.abort()
+    
+    def getDownloadedPlugins(self):
+        """
+        Public method to get the list of recently downloaded plugin files.
+        
+        @return list of plugin filenames (list of strings)
+        """
+        return self.__pluginsDownloaded
+
+class PluginRepositoryDialog(QDialog):
+    """
+    Class for the dialog variant.
+    """
+    def __init__(self, parent = None):
+        """
+        Constructor
+        
+        @param parent reference to the parent widget (QWidget)
+        """
+        QDialog.__init__(self, parent)
+        self.setSizeGripEnabled(True)
+        
+        self.__layout = QVBoxLayout(self)
+        self.__layout.setMargin(0)
+        self.setLayout(self.__layout)
+        
+        self.cw = PluginRepositoryWidget(self)
+        size = self.cw.size()
+        self.__layout.addWidget(self.cw)
+        self.resize(size)
+        
+        self.connect(self.cw.buttonBox, SIGNAL("accepted()"), self.accept)
+        self.connect(self.cw.buttonBox, SIGNAL("rejected()"), self.reject)
+        self.connect(self.cw, SIGNAL("closeAndInstall"), self.__closeAndInstall)
+        
+    def __closeAndInstall(self):
+        """
+        Private slot to handle the closeAndInstall signal.
+        """
+        self.done(QDialog.Accepted + 1)
+    
+    def getDownloadedPlugins(self):
+        """
+        Public method to get the list of recently downloaded plugin files.
+        
+        @return list of plugin filenames (list of strings)
+        """
+        return self.cw.getDownloadedPlugins()
+
+class PluginRepositoryWindow(QMainWindow):
+    """
+    Main window class for the standalone dialog.
+    """
+    def __init__(self, parent = None):
+        """
+        Constructor
+        
+        @param parent reference to the parent widget (QWidget)
+        """
+        QMainWindow.__init__(self, parent)
+        self.cw = PluginRepositoryWidget(self)
+        size = self.cw.size()
+        self.setCentralWidget(self.cw)
+        self.resize(size)
+        
+        self.connect(self.cw.buttonBox, SIGNAL("accepted()"), self.close)
+        self.connect(self.cw.buttonBox, SIGNAL("rejected()"), self.close)
+        self.connect(self.cw, SIGNAL("closeAndInstall"), self.__startPluginInstall)
+    
+    def __startPluginInstall(self):
+        """
+        Private slot to start the eric4 plugin installation dialog.
+        """
+        proc = QProcess()
+        applPath = os.path.join(getConfig("ericDir"), "eric4-plugininstall.py")
+        
+        args = []
+        args.append(applPath)
+        args += self.cw.getDownloadedPlugins()
+        
+        if not os.path.isfile(applPath) or not proc.startDetached(sys.executable, args):
+            QMessageBox.critical(self,
+                self.trUtf8('Process Generation Error'),
+                self.trUtf8(
+                    '<p>Could not start the process.<br>'
+                    'Ensure that it is available as <b>{0}</b>.</p>'
+                ).format(applPath),
+                self.trUtf8('OK'))
+        
+        self.close()

eric ide

mercurial