src/eric7/PluginManager/PluginRepositoryDialog.py

branch
eric7
changeset 10314
1f7d52f024b1
parent 10069
435cc5875135
child 10439
21c28b0f9e41
diff -r 8f69edb4ad73 -r 1f7d52f024b1 src/eric7/PluginManager/PluginRepositoryDialog.py
--- a/src/eric7/PluginManager/PluginRepositoryDialog.py	Thu Nov 16 13:58:04 2023 +0100
+++ b/src/eric7/PluginManager/PluginRepositoryDialog.py	Thu Nov 16 15:56:12 2023 +0100
@@ -7,11 +7,14 @@
 Module implementing a dialog showing the available plugins.
 """
 
+import enum
 import glob
 import os
 import re
 import zipfile
 
+from collections import ChainMap, defaultdict
+
 from PyQt6.QtCore import (
     QCoreApplication,
     QFile,
@@ -66,6 +69,18 @@
 from .Ui_PluginRepositoryDialog import Ui_PluginRepositoryDialog
 
 
+class PluginStatus(enum.Enum):
+    """
+    Class defining the various plugin status.
+    """
+
+    UpToDate = 0
+    New = 1
+    LocalUpdate = 2
+    RemoteUpdate = 3
+    Error = 4
+
+
 class PluginRepositoryWidget(QWidget, Ui_PluginRepositoryDialog):
     """
     Class implementing a dialog showing the available plugins.
@@ -81,12 +96,6 @@
     FilenameRole = Qt.ItemDataRole.UserRole + 2
     AuthorRole = Qt.ItemDataRole.UserRole + 3
 
-    PluginStatusUpToDate = 0
-    PluginStatusNew = 1
-    PluginStatusLocalUpdate = 2
-    PluginStatusRemoteUpdate = 3
-    PluginStatusError = 4
-
     def __init__(self, pluginManager, integrated=False, parent=None):
         """
         Constructor
@@ -111,6 +120,14 @@
 
         self.__integratedWidget = integrated
 
+        self.__statusTranslations = {
+            "stable": self.tr("Stable"),
+            "unstable": self.tr("Unstable"),
+            "obsolete": self.tr("Obsolete"),
+            "unknown": self.tr("Unknown"),
+        }
+        self.__initHeaderItemsCache()
+
         if self.__integratedWidget:
             self.layout().setContentsMargins(0, 3, 0, 0)
 
@@ -272,7 +289,8 @@
         """
         Private slot to handle the click of a button of the button box.
 
-        @param button reference to the button pressed (QAbstractButton)
+        @param button reference to the button pressed
+        @type QAbstractButton
         """
         if button == self.__updateButton:
             self.__updateList()
@@ -306,8 +324,10 @@
         """
         Private method to format the description.
 
-        @param lines lines of the description (list of strings)
-        @return formatted description (string)
+        @param lines lines of the description
+        @type list of str
+        @return formatted description
+        @rtype str
         """
         # remove empty line at start and end
         newlines = lines[:]
@@ -333,6 +353,7 @@
         @param url URL to be modified
         @type str
         @param newScheme scheme to be set for the given URL
+        @type str
         @return modified URL
         @rtype str
         """
@@ -350,7 +371,8 @@
         """
         Private slot to show the context menu.
 
-        @param pos position to show the menu (QPoint)
+        @param pos position to show the menu
+        @type QPoint
         """
         self.__hideAct.setEnabled(
             self.repositoryList.currentItem() is not None
@@ -365,8 +387,10 @@
         """
         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)
+        @param current reference to the new current item
+        @type QTreeWidgetItem
+        @param previous reference to the old current item
+        @type QTreeWidgetItem
         """
         if self.__repositoryMissing or current is None:
             self.descriptionEdit.clear()
@@ -389,16 +413,20 @@
 
     def __selectedItems(self):
         """
-        Private method to get all selected items without the toplevel ones.
+        Private method to get all selected items without the status and category items.
 
-        @return list of selected items (list)
+        @return list of selected items without header items
+        @rtype list of QTreeWidgetItem
         """
-        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
+        selectedItems = []
+        allCategoryItems = ChainMap(*self.__categoryItems.values())
+        for itm in self.repositoryList.selectedItems():
+            if (
+                itm not in self.__statusItems.values()
+                and itm not in allCategoryItems.values()
+            ):
+                selectedItems.append(itm)
+        return selectedItems
 
     @pyqtSlot()
     def on_repositoryList_itemSelectionChanged(self):
@@ -426,21 +454,14 @@
             url = url.replace("https://", "http://")
         self.__pluginManager.downLoadRepositoryFile(url=url)
 
-    def __downloadRepositoryFileDone(self, status, filename):  # noqa: U100
-        """
-        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)
+        @param status flag indicating a successful download
+        @type bool
+        @param filename full path of the downloaded file
+        @type str
         """
         if status:
             self.__pluginsDownloaded.append(filename)
@@ -480,21 +501,15 @@
             if Preferences.getPluginManager("ForceHttpPluginDownload")
             else self.repositoryUrlEdit.text().split("//", 1)[0]
         )
-        for itm in self.repositoryList.selectedItems():
-            if itm not in [
-                self.__stableItem,
-                self.__unstableItem,
-                self.__unknownItem,
-                self.__obsoleteItem,
-            ]:
-                url = self.__changeScheme(
-                    itm.data(0, PluginRepositoryWidget.UrlRole), newScheme
-                )
-                filename = os.path.join(
-                    Preferences.getPluginManager("DownloadPath"),
-                    itm.data(0, PluginRepositoryWidget.FilenameRole),
-                )
-                self.__pluginsToDownload.append((url, filename))
+        for itm in self.__selectedItems():
+            url = self.__changeScheme(
+                itm.data(0, PluginRepositoryWidget.UrlRole), newScheme
+            )
+            filename = os.path.join(
+                Preferences.getPluginManager("DownloadPath"),
+                itm.data(0, PluginRepositoryWidget.FilenameRole),
+            )
+            self.__pluginsToDownload.append((url, filename))
         if self.__pluginsToDownload:
             self.__downloadPlugin()
 
@@ -532,20 +547,30 @@
         """
         Private method to resort the tree.
         """
-        self.repositoryList.sortItems(
-            self.repositoryList.sortColumn(),
-            self.repositoryList.header().sortIndicatorOrder(),
-        )
+        if self.__integratedWidget:
+            self.repositoryList.sortItems(
+                self.repositoryList.sortColumn(),
+                Qt.SortOrder.AscendingOrder,
+            )
+        else:
+            self.repositoryList.sortItems(
+                self.repositoryList.sortColumn(),
+                self.repositoryList.header().sortIndicatorOrder(),
+            )
+
+    def __initHeaderItemsCache(self):
+        """
+        Private method to initialize the cache variables for the header items.
+        """
+        self.__statusItems = defaultdict(lambda: None)
+        self.__categoryItems = defaultdict(dict)
 
     def __populateList(self):
         """
         Private method to populate the list of available plugins.
         """
+        self.__initHeaderItemsCache()
         self.repositoryList.clear()
-        self.__stableItem = None
-        self.__unstableItem = None
-        self.__unknownItem = None
-        self.__obsoleteItem = None
 
         self.__newItems = 0
         self.__updateLocalItems = 0
@@ -604,9 +629,12 @@
         """
         Private slot to download the given file.
 
-        @param url URL for the download (string)
-        @param filename local name of the file (string)
+        @param url URL for the download
+        @type str
+        @param filename local name of the file
+        @type str
         @param doneMethod method to be called when done
+        @type function
         """
         if self.__online:
             self.__updateButton.setEnabled(False)
@@ -652,7 +680,7 @@
         @param fileName local name of the file
         @type str
         @param doneMethod method to be called when done
-        @type func
+        @type function
         """
         self.__updateButton.setEnabled(True)
         if not self.__integratedWidget:
@@ -720,69 +748,87 @@
         """
         Private slot to show the download progress.
 
-        @param done number of bytes downloaded so far (integer)
-        @param total total bytes to be downloaded (integer)
+        @param done number of bytes downloaded so far
+        @type int
+        @param total total bytes to be downloaded
+        @type int
         """
         if total:
             self.downloadProgress.setMaximum(total)
             self.downloadProgress.setValue(done)
 
     def addEntry(
-        self, name, short, description, url, author, version, filename, status
+        self,
+        name,
+        short,
+        description,
+        url,
+        author,
+        version,
+        filename,
+        status,
+        category,
     ):
         """
         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])
+        @param name data for the name field
+        @type str
+        @param short data for the short field
+        @type str
+        @param description data for the description field
+        @type list of str
+        @param url data for the url field
+        @type str
+        @param author data for the author field
+        @type str
+        @param version data for the version field
+        @type str
+        @param filename data for the filename field
+        @type str
+        @param status status of the plugin (one of stable, unstable, unknown)
+        @type str
+        @param category category designation of the plugin
+        @type str
         """
         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.tr("Stable")]
-                )
-                self.__stableItem.setExpanded(True)
-            parent = self.__stableItem
-        elif status == "unstable":
-            if self.__unstableItem is None:
-                self.__unstableItem = QTreeWidgetItem(
-                    self.repositoryList, [self.tr("Unstable")]
-                )
-                self.__unstableItem.setExpanded(True)
-            parent = self.__unstableItem
-        elif status == "obsolete":
-            if self.__obsoleteItem is None:
-                self.__obsoleteItem = QTreeWidgetItem(
-                    self.repositoryList, [self.tr("Obsolete")]
-                )
-                self.__obsoleteItem.setExpanded(True)
-            parent = self.__obsoleteItem
-        else:
-            if self.__unknownItem is None:
-                self.__unknownItem = QTreeWidgetItem(
-                    self.repositoryList, [self.tr("Unknown")]
-                )
-                self.__unknownItem.setExpanded(True)
-            parent = self.__unknownItem
+        # 1. determine and create the status item
+        statusItem = self.__statusItems[status]
+        if statusItem is None:
+            statusItem = QTreeWidgetItem(
+                self.repositoryList,
+                [
+                    self.__statusTranslations.get(
+                        status, self.__statusTranslations["unknown"]
+                    )
+                ],
+            )
+            statusItem.setExpanded(True)
+            statusItem.setFirstColumnSpanned(True)
+            self.__statusItems[status] = statusItem
 
+        # 2. determine and create the category item
+        try:
+            categoryItem = self.__categoryItems[status][category]
+        except KeyError:
+            # create the category item
+            categoryItem = QTreeWidgetItem(statusItem, [category])
+            categoryItem.setExpanded(True)
+            categoryItem.setFirstColumnSpanned(True)
+            self.__categoryItems[status][category] = categoryItem
+
+        # 3. create the plugin item
         if self.__integratedWidget:
             entryFormat = "<b>{0}</b> - Version: <i>{1}</i><br/>{2}"
-            itm = QTreeWidgetItem(parent)
+            itm = QTreeWidgetItem(categoryItem)
             itm.setFirstColumnSpanned(True)
             label = QLabel(entryFormat.format(name, version, short))
             self.repositoryList.setItemWidget(itm, 0, label)
         else:
-            itm = QTreeWidgetItem(parent, [name, version, short])
+            itm = QTreeWidgetItem(categoryItem, [name, version, short])
 
         itm.setData(0, PluginRepositoryWidget.UrlRole, url)
         itm.setData(0, PluginRepositoryWidget.FilenameRole, filename)
@@ -791,22 +837,22 @@
 
         iconColumn = 0 if self.__integratedWidget else 1
         updateStatus = self.__updateStatus(filename, version)
-        if updateStatus == PluginRepositoryWidget.PluginStatusUpToDate:
+        if updateStatus == PluginStatus.UpToDate:
             itm.setIcon(iconColumn, EricPixmapCache.getIcon("empty"))
             itm.setToolTip(iconColumn, self.tr("up-to-date"))
-        elif updateStatus == PluginRepositoryWidget.PluginStatusNew:
+        elif updateStatus == PluginStatus.New:
             itm.setIcon(iconColumn, EricPixmapCache.getIcon("download"))
             itm.setToolTip(iconColumn, self.tr("new download available"))
             self.__newItems += 1
-        elif updateStatus == PluginRepositoryWidget.PluginStatusLocalUpdate:
+        elif updateStatus == PluginStatus.LocalUpdate:
             itm.setIcon(iconColumn, EricPixmapCache.getIcon("updateLocal"))
             itm.setToolTip(iconColumn, self.tr("update installable"))
             self.__updateLocalItems += 1
-        elif updateStatus == PluginRepositoryWidget.PluginStatusRemoteUpdate:
+        elif updateStatus == PluginStatus.RemoteUpdate:
             itm.setIcon(iconColumn, EricPixmapCache.getIcon("updateRemote"))
             itm.setToolTip(iconColumn, self.tr("updated download available"))
             self.__updateRemoteItems += 1
-        elif updateStatus == PluginRepositoryWidget.PluginStatusError:
+        elif updateStatus == PluginStatus.Error:
             itm.setIcon(iconColumn, EricPixmapCache.getIcon("warning"))
             itm.setToolTip(iconColumn, self.tr("error determining status"))
 
@@ -814,11 +860,13 @@
         """
         Private method to check the given archive update status.
 
-        @param filename data for the filename field (string)
-        @param version data for the version field (string)
-        @return plug-in update status (integer, one of PluginStatusNew,
-            PluginStatusUpToDate, PluginStatusLocalUpdate,
-            PluginStatusRemoteUpdate)
+        @param filename data for the filename field
+        @type str
+        @param version data for the version field
+        @type str
+        @return plug-in update status
+        @rtype int (one of PluginStatusNew, PluginStatusUpToDate,
+            PluginStatusLocalUpdate, PluginStatusRemoteUpdate)
         """
         archive = os.path.join(Preferences.getPluginManager("DownloadPath"), filename)
 
@@ -830,23 +878,23 @@
             pluginName = filename.rsplit("-", 1)[0]
             pluginDetails = self.__pluginManager.getPluginDetails(pluginName)
             if pluginDetails is None or pluginDetails["moduleName"] != pluginName:
-                return PluginRepositoryWidget.PluginStatusNew
+                return PluginStatus.New
             if pluginDetails["error"]:
-                return PluginRepositoryWidget.PluginStatusError
+                return PluginStatus.Error
             pluginVersionTuple = Globals.versionToTuple(pluginDetails["version"])[:3]
             versionTuple = Globals.versionToTuple(version)[:3]
             if pluginVersionTuple < versionTuple:
-                return PluginRepositoryWidget.PluginStatusRemoteUpdate
+                return PluginStatus.RemoteUpdate
             else:
-                return PluginRepositoryWidget.PluginStatusUpToDate
+                return PluginStatus.UpToDate
 
         # check, if the archive exists
         if not os.path.exists(archive):
-            return PluginRepositoryWidget.PluginStatusRemoteUpdate
+            return PluginStatus.RemoteUpdate
 
         # check, if the archive is a valid zip file
         if not zipfile.is_zipfile(archive):
-            return PluginRepositoryWidget.PluginStatusRemoteUpdate
+            return PluginStatus.RemoteUpdate
 
         zipFile = zipfile.ZipFile(archive, "r")
         try:
@@ -860,23 +908,25 @@
             pluginName = filename.rsplit("-", 1)[0]
             pluginDetails = self.__pluginManager.getPluginDetails(pluginName)
             if pluginDetails is None:
-                return PluginRepositoryWidget.PluginStatusLocalUpdate
+                return PluginStatus.LocalUpdate
             if (
                 Globals.versionToTuple(pluginDetails["version"])[:3]
                 < Globals.versionToTuple(version)[:3]
             ):
-                return PluginRepositoryWidget.PluginStatusLocalUpdate
+                return PluginStatus.LocalUpdate
             else:
-                return PluginRepositoryWidget.PluginStatusUpToDate
+                return PluginStatus.UpToDate
         else:
-            return PluginRepositoryWidget.PluginStatusRemoteUpdate
+            return PluginStatus.RemoteUpdate
 
     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)
+        @param reply reference to the reply object
+        @type QNetworkReply
+        @param errors list of SSL errors
+        @type list of QSslError
         """
         ignored = self.__sslErrorHandler.sslErrorsReply(reply, errors)[0]
         if ignored == EricSslErrorState.NOT_IGNORED:
@@ -886,7 +936,8 @@
         """
         Public method to get the list of recently downloaded plugin files.
 
-        @return list of plugin filenames (list of strings)
+        @return list of plugin filenames
+        @rtype list of str
         """
         return self.__pluginsDownloaded
 
@@ -896,7 +947,8 @@
         Private slot to set the read only status of the repository URL line
         edit.
 
-        @param checked state of the push button (boolean)
+        @param checked state of the push button
+        @type bool
         """
         self.repositoryUrlEdit.setReadOnly(not checked)
 
@@ -944,7 +996,8 @@
         """
         Private method to check, if there are any hidden plug-ins.
 
-        @return flag indicating the presence of hidden plug-ins (boolean)
+        @return flag indicating the presence of hidden plug-ins
+        @rtype bool
         """
         return bool(self.__hiddenPlugins)
 
@@ -953,7 +1006,7 @@
         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)
+        @type list of str
         """
         if hideList:
             self.__hiddenPlugins.extend(
@@ -1010,7 +1063,8 @@
         """
         Public method to get the list of recently downloaded plugin files.
 
-        @return list of plugin filenames (list of strings)
+        @return list of plugin filenames
+        @rtype list of str
         """
         return self.cw.getDownloadedPlugins()
 
@@ -1024,7 +1078,8 @@
         """
         Constructor
 
-        @param parent reference to the parent widget (QWidget)
+        @param parent reference to the parent widget
+        @type QWidget
         """
         super().__init__(parent)
         self.cw = PluginRepositoryWidget(None, parent=self)
@@ -1076,19 +1131,37 @@
     pluginsRegister = []  # list of plug-ins contained in the repository
 
     def registerPlugin(
-        name, short, description, url, author, version, filename, status  # noqa: U100
+        name,  # noqa: U100
+        short,  # noqa: U100
+        description,  # noqa: U100
+        url,
+        author,  # noqa: U100
+        version,  # noqa: U100
+        filename,  # noqa: U100
+        status,  # noqa: U100
+        category,  # noqa: U100
     ):
         """
         Method to register a plug-in's data.
 
-        @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])
+        @param name data for the name field
+        @type str
+        @param short data for the short field
+        @type str
+        @param description data for the description field
+        @type list of str
+        @param url data for the url field
+        @type str
+        @param author data for the author field
+        @type str
+        @param version data for the version field
+        @type str
+        @param filename data for the filename field
+        @type str
+        @param status status of the plugin (one of stable, unstable, unknown)
+        @type str
+        @param category category designation of the plugin
+        @type str
         """
         pluginName = os.path.splitext(url.rsplit("/", 1)[1])[0]
         if pluginName not in pluginsRegister:

eric ide

mercurial