PluginManager/PluginRepositoryDialog.py

Mon, 05 Nov 2012 16:55:16 +0100

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Mon, 05 Nov 2012 16:55:16 +0100
changeset 2192
61b3849df76d
parent 2101
5bac7dee9e1a
child 2302
f29e9405c851
permissions
-rw-r--r--

Changed a few places to use the new notification system.

# -*- coding: utf-8 -*-

# Copyright (c) 2007 - 2012 Detlev Offenbach <detlev@die-offenbachs.de>
#


"""
Module implementing a dialog showing the available plugins.
"""

import sys
import os
import zipfile

from PyQt4.QtCore import pyqtSignal, pyqtSlot, Qt, QFile, QIODevice, QUrl, QProcess
from PyQt4.QtGui import QWidget, QDialogButtonBox, QAbstractButton, QTreeWidgetItem, \
    QDialog, QVBoxLayout
from PyQt4.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply
try:
    from PyQt4.QtNetwork import QSslError   # __IGNORE_EXCEPTION__ __IGNORE_WARNING__
    SSL_AVAILABLE = True
except ImportError:
    SSL_AVAILABLE = False

from .Ui_PluginRepositoryDialog import Ui_PluginRepositoryDialog

from E5Gui import E5MessageBox
from E5Gui.E5MainWindow import E5MainWindow
from E5Gui.E5Application import e5App

from E5XML.PluginRepositoryReader import PluginRepositoryReader

from E5Network.E5NetworkProxyFactory import proxyAuthenticationRequired

import Utilities
import Preferences

import UI.PixmapCache

from eric5config 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
    """
    closeAndInstall = pyqtSignal()
    
    def __init__(self, parent=None, external=False):
        """
        Constructor
        
        @param parent parent of this dialog (QWidget)
        """
        super().__init__(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.__downloadInstallButton = \
            self.buttonBox.addButton(self.trUtf8("Download && Install"),
            QDialogButtonBox.ActionRole)
        self.__downloadInstallButton.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.repositoryUrlEdit.setText(Preferences.getUI("PluginRepositoryUrl5"))
        
        self.repositoryList.headerItem().setText(self.repositoryList.columnCount(), "")
        self.repositoryList.header().setSortIndicator(0, Qt.AscendingOrder)
        
        self.pluginRepositoryFile = \
            os.path.join(Utilities.getConfigDir(), "PluginRepository")
        
        self.__external = external
        
        # attributes for the network objects
        self.__networkManager = QNetworkAccessManager(self)
        self.__networkManager.proxyAuthenticationRequired.connect(
            proxyAuthenticationRequired)
        if SSL_AVAILABLE:
            self.__networkManager.sslErrors.connect(self.__sslErrors)
        self.__replies = []
        
        self.__doneMethod = None
        self.__inDownload = False
        self.__pluginsToDownload = []
        self.__pluginsDownloaded = []
        self.__isDownloadInstall = False
        self.__allDownloadedOk = False
        
        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.__isDownloadInstall = False
            self.__downloadPlugins()
        elif button == self.__downloadInstallButton:
            self.__isDownloadInstall = True
            self.__allDownloadedOk = True
            self.__downloadPlugins()
        elif button == self.__downloadCancelButton:
            self.__downloadCancel()
        elif button == self.__installButton:
            self.closeAndInstall.emit()
    
    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) or "")
        self.descriptionEdit.setPlainText(current.data(0, descrRole) and \
            self.__formatDescription(current.data(0, descrRole)) or "")
        self.authorEdit.setText(current.data(0, authorRole) or "")
    
    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()))
        self.__downloadInstallButton.setEnabled(len(self.__selectedItems()))
    
    def __updateList(self):
        """
        Private slot to download a new list and display the contents.
        """
        url = self.repositoryUrlEdit.text()
        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 flag indicating a successful download (boolean)
        @param filename full path of the downloaded file (string)
        """
        if status:
            self.__pluginsDownloaded.append(filename)
        if self.__isDownloadInstall:
            self.__allDownloadedOk &= status
        
        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.__downloadInstallButton.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)
                filename = os.path.join(
                    Preferences.getPluginManager("DownloadPath"),
                    itm.data(0, filenameRole))
                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.__downloadInstallButton.setEnabled(len(self.__selectedItems()))
        self.__installButton.setEnabled(True)
        self.__doneMethod = None
        if not self.__external:
            ui = e5App().getObject("UserInterface")
        else:
            ui = None
        if ui and ui.notificationsEnabled():
            ui.showNotification(UI.PixmapCache.getPixmap("plugin48.png"),
                self.trUtf8("Download Plugin Files"),
                self.trUtf8("""The requested plugins were downloaded."""))
        
        if self.__isDownloadInstall:
            self.closeAndInstall.emit()
        else:
            if ui is None or not ui.notificationsEnabled():
                E5MessageBox.information(self,
                    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
            f = QFile(self.pluginRepositoryFile)
            if f.open(QIODevice.ReadOnly):
                reader = PluginRepositoryReader(f, self)
                reader.readXML()
                self.repositoryList.resizeColumnToContents(0)
                self.repositoryList.resizeColumnToContents(1)
                self.repositoryList.resizeColumnToContents(2)
                self.__resortRepositoryList()
                url = Preferences.getUI("PluginRepositoryUrl5")
                if url != self.repositoryUrlEdit.text():
                    self.repositoryUrlEdit.setText(url)
                    E5MessageBox.warning(self,
                        self.trUtf8("Plugins Repository URL Changed"),
                        self.trUtf8("""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>")\
                        .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
        """
        self.__updateButton.setEnabled(False)
        self.__downloadButton.setEnabled(False)
        self.__downloadInstallButton.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
        
        request = QNetworkRequest(QUrl(url))
        request.setAttribute(QNetworkRequest.CacheLoadControlAttribute,
                             QNetworkRequest.AlwaysNetwork)
        reply = self.__networkManager.get(request)
        reply.finished[()].connect(self.__downloadFileDone)
        reply.downloadProgress.connect(self.__downloadProgress)
        self.__replies.append(reply)
    
    def __downloadFileDone(self):
        """
        Private method called, after the file has been downloaded
        from the internet.
        """
        self.__updateButton.setEnabled(True)
        self.__downloadCancelButton.setEnabled(False)
        self.statusLabel.setText("  ")
        
        ok = True
        reply = self.sender()
        if reply in self.__replies:
            self.__replies.remove(reply)
        if reply.error() != QNetworkReply.NoError:
            ok = False
            if not self.__downloadCancelled:
                E5MessageBox.warning(self,
                    self.trUtf8("Error downloading file"),
                    self.trUtf8(
                        """<p>Could not download the requested file from {0}.</p>"""
                        """<p>Error: {1}</p>"""
                    ).format(self.__downloadURL, reply.errorString())
                )
            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()))
                    self.__downloadInstallButton.setEnabled(len(self.__selectedItems()))
            return
        
        self.__downloadIODevice.open(QIODevice.WriteOnly)
        self.__downloadIODevice.write(reply.readAll())
        self.__downloadIODevice.close()
        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.__replies:
            reply = self.__replies[0]
            self.__downloadCancelled = True
            self.__pluginsToDownload = []
            reply.abort()
    
    def __downloadProgress(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)
        """
        if total:
            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, url)
        itm.setData(0, filenameRole, filename)
        itm.setData(0, authorRole, author)
        itm.setData(0, descrRole, 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").decode("utf-8")
        except KeyError:
            aversion = ""
        zip.close()
        
        return aversion == version
    
    def __sslErrors(self, reply, errors):
        """
        Private slot to handle SSL errors.
        
        @param reply reference to the reply object (QNetworkReply)
        @param errors list of SSL errors (list of QSslError)
        """
        errorStrings = []
        if errors:
            for err in errors:
                errorStrings.append(err.errorString())
            errorString = '.<br />'.join(errorStrings)
            ret = E5MessageBox.yesNo(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),
            icon=E5MessageBox.Warning)
        else:
            ret = True
        if ret:
            reply.ignoreSslErrors()
        else:
            self.__downloadCancelled = True
            reply.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
    
    @pyqtSlot(bool)
    def on_repositoryUrlEditButton_toggled(self, checked):
        """
        Private slot to set the read only status of the repository URL line edit.
        
        @param checked state of the push button (boolean)
        """
        self.repositoryUrlEdit.setReadOnly(not checked)


class PluginRepositoryDialog(QDialog):
    """
    Class for the dialog variant.
    """
    def __init__(self, parent=None):
        """
        Constructor
        
        @param parent reference to the parent widget (QWidget)
        """
        super().__init__(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.cw.buttonBox.accepted[()].connect(self.accept)
        self.cw.buttonBox.rejected[()].connect(self.reject)
        self.cw.closeAndInstall.connect(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(E5MainWindow):
    """
    Main window class for the standalone dialog.
    """
    def __init__(self, parent=None):
        """
        Constructor
        
        @param parent reference to the parent widget (QWidget)
        """
        super().__init__(parent)
        self.cw = PluginRepositoryWidget(self, external=True)
        size = self.cw.size()
        self.setCentralWidget(self.cw)
        self.resize(size)
        
        self.setStyle(Preferences.getUI("Style"), Preferences.getUI("StyleSheet"))
        
        self.cw.buttonBox.accepted[()].connect(self.close)
        self.cw.buttonBox.rejected[()].connect(self.close)
        self.cw.closeAndInstall.connect(self.__startPluginInstall)
    
    def __startPluginInstall(self):
        """
        Private slot to start the eric5 plugin installation dialog.
        """
        proc = QProcess()
        applPath = os.path.join(getConfig("ericDir"), "eric5_plugininstall.py")
        
        args = []
        args.append(applPath)
        args += self.cw.getDownloadedPlugins()
        
        if not os.path.isfile(applPath) or not proc.startDetached(sys.executable, args):
            E5MessageBox.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