PluginManager/PluginRepositoryDialog.py

changeset 3200
83bde5e6f146
parent 3190
a9a94491c4fd
child 3246
4cd58a0d6c28
equal deleted inserted replaced
3197:4103c8013c36 3200:83bde5e6f146
9 """ 9 """
10 10
11 import sys 11 import sys
12 import os 12 import os
13 import zipfile 13 import zipfile
14 import glob
14 15
15 from PyQt4.QtCore import pyqtSignal, pyqtSlot, Qt, QFile, QIODevice, QUrl, \ 16 from PyQt4.QtCore import pyqtSignal, pyqtSlot, Qt, QFile, QIODevice, QUrl, \
16 QProcess 17 QProcess, QPoint
17 from PyQt4.QtGui import QWidget, QDialogButtonBox, QAbstractButton, \ 18 from PyQt4.QtGui import QWidget, QDialogButtonBox, QAbstractButton, \
18 QTreeWidgetItem, QDialog, QVBoxLayout 19 QTreeWidgetItem, QDialog, QVBoxLayout, QMenu
19 from PyQt4.QtNetwork import QNetworkAccessManager, QNetworkRequest, \ 20 from PyQt4.QtNetwork import QNetworkAccessManager, QNetworkRequest, \
20 QNetworkReply 21 QNetworkReply
21 22
22 from .Ui_PluginRepositoryDialog import Ui_PluginRepositoryDialog 23 from .Ui_PluginRepositoryDialog import Ui_PluginRepositoryDialog
23 24
37 38
38 import UI.PixmapCache 39 import UI.PixmapCache
39 40
40 from eric5config import getConfig 41 from eric5config import getConfig
41 42
42 descrRole = Qt.UserRole
43 urlRole = Qt.UserRole + 1
44 filenameRole = Qt.UserRole + 2
45 authorRole = Qt.UserRole + 3
46
47 43
48 class PluginRepositoryWidget(QWidget, Ui_PluginRepositoryDialog): 44 class PluginRepositoryWidget(QWidget, Ui_PluginRepositoryDialog):
49 """ 45 """
50 Class implementing a dialog showing the available plugins. 46 Class implementing a dialog showing the available plugins.
51 47
52 @signal closeAndInstall() emitted when the Close & Install button is 48 @signal closeAndInstall() emitted when the Close & Install button is
53 pressed 49 pressed
54 """ 50 """
55 closeAndInstall = pyqtSignal() 51 closeAndInstall = pyqtSignal()
52
53 DescrRole = Qt.UserRole
54 UrlRole = Qt.UserRole + 1
55 FilenameRole = Qt.UserRole + 2
56 AuthorRole = Qt.UserRole + 3
57
58 PluginStatusUpToDate = 0
59 PluginStatusNew = 1
60 PluginStatusLocalUpdate = 2
61 PluginStatusRemoteUpdate = 3
56 62
57 def __init__(self, parent=None, external=False): 63 def __init__(self, parent=None, external=False):
58 """ 64 """
59 Constructor 65 Constructor
60 66
87 93
88 self.repositoryList.headerItem().setText( 94 self.repositoryList.headerItem().setText(
89 self.repositoryList.columnCount(), "") 95 self.repositoryList.columnCount(), "")
90 self.repositoryList.header().setSortIndicator(0, Qt.AscendingOrder) 96 self.repositoryList.header().setSortIndicator(0, Qt.AscendingOrder)
91 97
98 self.__pluginContextMenu = QMenu(self)
99 self.__hideAct = self.__pluginContextMenu.addAction(
100 self.tr("Hide"), self.__hidePlugin)
101 self.__hideSelectedAct = self.__pluginContextMenu.addAction(
102 self.tr("Hide Selected"), self.__hideSelectedPlugins)
103 self.__pluginContextMenu.addSeparator()
104 self.__showAllAct = self.__pluginContextMenu.addAction(
105 self.tr("Show All"), self.__showAllPlugins)
106 self.__pluginContextMenu.addSeparator()
107 self.__pluginContextMenu.addAction(
108 self.tr("Cleanup Downloads"), self.__cleanupDownloads)
109
92 self.pluginRepositoryFile = \ 110 self.pluginRepositoryFile = \
93 os.path.join(Utilities.getConfigDir(), "PluginRepository") 111 os.path.join(Utilities.getConfigDir(), "PluginRepository")
94 112
95 self.__external = external 113 self.__external = external
96 114
107 self.__inDownload = False 125 self.__inDownload = False
108 self.__pluginsToDownload = [] 126 self.__pluginsToDownload = []
109 self.__pluginsDownloaded = [] 127 self.__pluginsDownloaded = []
110 self.__isDownloadInstall = False 128 self.__isDownloadInstall = False
111 self.__allDownloadedOk = False 129 self.__allDownloadedOk = False
130
131 self.__hiddenPlugins = Preferences.getPluginManager("HiddenPlugins")
112 132
113 self.__populateList() 133 self.__populateList()
114 134
115 @pyqtSlot(QAbstractButton) 135 @pyqtSlot(QAbstractButton)
116 def on_buttonBox_clicked(self, button): 136 def on_buttonBox_clicked(self, button):
129 self.__allDownloadedOk = True 149 self.__allDownloadedOk = True
130 self.__downloadPlugins() 150 self.__downloadPlugins()
131 elif button == self.__downloadCancelButton: 151 elif button == self.__downloadCancelButton:
132 self.__downloadCancel() 152 self.__downloadCancel()
133 elif button == self.__installButton: 153 elif button == self.__installButton:
134 self.closeAndInstall.emit() 154 self.__closeAndInstall()
135 155
136 def __formatDescription(self, lines): 156 def __formatDescription(self, lines):
137 """ 157 """
138 Private method to format the description. 158 Private method to format the description.
139 159
155 index += 1 175 index += 1
156 176
157 # join lines by a blank 177 # join lines by a blank
158 return ' '.join(newlines) 178 return ' '.join(newlines)
159 179
180 @pyqtSlot(QPoint)
181 def on_repositoryList_customContextMenuRequested(self, pos):
182 """
183 Private slot to show the context menu.
184
185 @param pos position to show the menu (QPoint)
186 """
187 self.__hideAct.setEnabled(
188 self.repositoryList.currentItem() is not None and
189 len(self.__selectedItems()) == 1)
190 self.__hideSelectedAct.setEnabled(
191 len(self.__selectedItems()) > 1)
192 self.__showAllAct.setEnabled(bool(self.__hasHiddenPlugins()))
193 self.__pluginContextMenu.popup(self.repositoryList.mapToGlobal(pos))
194
160 @pyqtSlot(QTreeWidgetItem, QTreeWidgetItem) 195 @pyqtSlot(QTreeWidgetItem, QTreeWidgetItem)
161 def on_repositoryList_currentItemChanged(self, current, previous): 196 def on_repositoryList_currentItemChanged(self, current, previous):
162 """ 197 """
163 Private slot to handle the change of the current item. 198 Private slot to handle the change of the current item.
164 199
166 @param previous reference to the old current item (QTreeWidgetItem) 201 @param previous reference to the old current item (QTreeWidgetItem)
167 """ 202 """
168 if self.__repositoryMissing or current is None: 203 if self.__repositoryMissing or current is None:
169 return 204 return
170 205
171 self.urlEdit.setText(current.data(0, urlRole) or "") 206 self.urlEdit.setText(
207 current.data(0, PluginRepositoryWidget.UrlRole) or "")
172 self.descriptionEdit.setPlainText( 208 self.descriptionEdit.setPlainText(
173 current.data(0, descrRole) and 209 current.data(0, PluginRepositoryWidget.DescrRole) and
174 self.__formatDescription(current.data(0, descrRole)) or "") 210 self.__formatDescription(
175 self.authorEdit.setText(current.data(0, authorRole) or "") 211 current.data(0, PluginRepositoryWidget.DescrRole)) or "")
212 self.authorEdit.setText(
213 current.data(0, PluginRepositoryWidget.AuthorRole) or "")
176 214
177 def __selectedItems(self): 215 def __selectedItems(self):
178 """ 216 """
179 Private method to get all selected items without the toplevel ones. 217 Private method to get all selected items without the toplevel ones.
180 218
192 """ 230 """
193 Private slot to handle a change of the selection. 231 Private slot to handle a change of the selection.
194 """ 232 """
195 self.__downloadButton.setEnabled(len(self.__selectedItems())) 233 self.__downloadButton.setEnabled(len(self.__selectedItems()))
196 self.__downloadInstallButton.setEnabled(len(self.__selectedItems())) 234 self.__downloadInstallButton.setEnabled(len(self.__selectedItems()))
235 self.__installButton.setEnabled(len(self.__selectedItems()))
197 236
198 def __updateList(self): 237 def __updateList(self):
199 """ 238 """
200 Private slot to download a new list and display the contents. 239 Private slot to download a new list and display the contents.
201 """ 240 """
249 self.__downloadInstallButton.setEnabled(False) 288 self.__downloadInstallButton.setEnabled(False)
250 self.__installButton.setEnabled(False) 289 self.__installButton.setEnabled(False)
251 for itm in self.repositoryList.selectedItems(): 290 for itm in self.repositoryList.selectedItems():
252 if itm not in [self.__stableItem, self.__unstableItem, 291 if itm not in [self.__stableItem, self.__unstableItem,
253 self.__unknownItem]: 292 self.__unknownItem]:
254 url = itm.data(0, urlRole) 293 url = itm.data(0, PluginRepositoryWidget.UrlRole)
255 filename = os.path.join( 294 filename = os.path.join(
256 Preferences.getPluginManager("DownloadPath"), 295 Preferences.getPluginManager("DownloadPath"),
257 itm.data(0, filenameRole)) 296 itm.data(0, PluginRepositoryWidget.FilenameRole))
258 self.__pluginsToDownload.append((url, filename)) 297 self.__pluginsToDownload.append((url, filename))
259 self.__downloadPlugin() 298 self.__downloadPlugin()
260 299
261 def __downloadPluginsDone(self): 300 def __downloadPluginsDone(self):
262 """ 301 """
459 @param author data for the author field (string) 498 @param author data for the author field (string)
460 @param version data for the version field (string) 499 @param version data for the version field (string)
461 @param filename data for the filename field (string) 500 @param filename data for the filename field (string)
462 @param status status of the plugin (string [stable, unstable, unknown]) 501 @param status status of the plugin (string [stable, unstable, unknown])
463 """ 502 """
503 pluginName = filename.rsplit("-", 1)[0]
504 if pluginName in self.__hiddenPlugins:
505 return
506
464 if status == "stable": 507 if status == "stable":
465 if self.__stableItem is None: 508 if self.__stableItem is None:
466 self.__stableItem = \ 509 self.__stableItem = \
467 QTreeWidgetItem(self.repositoryList, 510 QTreeWidgetItem(self.repositoryList,
468 [self.tr("Stable")]) 511 [self.tr("Stable")])
482 [self.tr("Unknown")]) 525 [self.tr("Unknown")])
483 self.__unknownItem.setExpanded(True) 526 self.__unknownItem.setExpanded(True)
484 parent = self.__unknownItem 527 parent = self.__unknownItem
485 itm = QTreeWidgetItem(parent, [name, version, short]) 528 itm = QTreeWidgetItem(parent, [name, version, short])
486 529
487 itm.setData(0, urlRole, url) 530 itm.setData(0, PluginRepositoryWidget.UrlRole, url)
488 itm.setData(0, filenameRole, filename) 531 itm.setData(0, PluginRepositoryWidget.FilenameRole, filename)
489 itm.setData(0, authorRole, author) 532 itm.setData(0, PluginRepositoryWidget.AuthorRole, author)
490 itm.setData(0, descrRole, description) 533 itm.setData(0, PluginRepositoryWidget.DescrRole, description)
491 534
492 if self.__isUpToDate(filename, version): 535 updateStatus = self.__updateStatus(filename, version)
536 if updateStatus == PluginRepositoryWidget.PluginStatusUpToDate:
493 itm.setIcon(1, UI.PixmapCache.getIcon("empty.png")) 537 itm.setIcon(1, UI.PixmapCache.getIcon("empty.png"))
494 else: 538 itm.setToolTip(1, self.tr("up-to-date"))
539 elif updateStatus == PluginRepositoryWidget.PluginStatusNew:
495 itm.setIcon(1, UI.PixmapCache.getIcon("download.png")) 540 itm.setIcon(1, UI.PixmapCache.getIcon("download.png"))
496 541 itm.setToolTip(1, self.tr("new download available"))
497 def __isUpToDate(self, filename, version): 542 elif updateStatus == PluginRepositoryWidget.PluginStatusLocalUpdate:
498 """ 543 itm.setIcon(1, UI.PixmapCache.getIcon("updateLocal.png"))
499 Private method to check, if the given archive is up-to-date. 544 itm.setToolTip(1, self.tr("update installable"))
545 elif updateStatus == PluginRepositoryWidget.PluginStatusRemoteUpdate:
546 itm.setIcon(1, UI.PixmapCache.getIcon("updateRemote.png"))
547 itm.setToolTip(1, self.tr("updated download available"))
548
549 def __updateStatus(self, filename, version):
550 """
551 Private method to check, if the given archive update status.
500 552
501 @param filename data for the filename field (string) 553 @param filename data for the filename field (string)
502 @param version data for the version field (string) 554 @param version data for the version field (string)
503 @return flag indicating up-to-date (boolean) 555 @return plug-in update status (integer, one of PluginStatusNew,
556 PluginStatusUpToDate, PluginStatusLocalUpdate,
557 PluginStatusRemoteUpdate)
504 """ 558 """
505 archive = os.path.join(Preferences.getPluginManager("DownloadPath"), 559 archive = os.path.join(Preferences.getPluginManager("DownloadPath"),
506 filename) 560 filename)
507 561
562 # check, if it is an update (i.e. we already have archives
563 # with the same pattern)
564 archivesPattern = archive.rsplit('-', 1)[0] + "-*.zip"
565 if len(glob.glob(archivesPattern)) == 0:
566 return PluginRepositoryWidget.PluginStatusNew
567
508 # check, if the archive exists 568 # check, if the archive exists
509 if not os.path.exists(archive): 569 if not os.path.exists(archive):
510 return False 570 return PluginRepositoryWidget.PluginStatusRemoteUpdate
511 571
512 # check, if the archive is a valid zip file 572 # check, if the archive is a valid zip file
513 if not zipfile.is_zipfile(archive): 573 if not zipfile.is_zipfile(archive):
514 return False 574 return PluginRepositoryWidget.PluginStatusRemoteUpdate
515 575
516 zip = zipfile.ZipFile(archive, "r") 576 zip = zipfile.ZipFile(archive, "r")
517 try: 577 try:
518 aversion = zip.read("VERSION").decode("utf-8") 578 aversion = zip.read("VERSION").decode("utf-8")
519 except KeyError: 579 except KeyError:
520 aversion = "" 580 aversion = ""
521 zip.close() 581 zip.close()
522 582
523 return aversion == version 583 if aversion == version:
584 if not self.__external:
585 # Check against installed/loaded plug-ins
586 pluginManager = e5App().getObject("PluginManager")
587 pluginName = filename.rsplit('-', 1)[0]
588 pluginDetails = pluginManager.getPluginDetails(pluginName)
589 if pluginDetails is None or pluginDetails["version"] < version:
590 return PluginRepositoryWidget.PluginStatusLocalUpdate
591
592 return PluginRepositoryWidget.PluginStatusUpToDate
593 else:
594 return PluginRepositoryWidget.PluginStatusRemoteUpdate
524 595
525 def __sslErrors(self, reply, errors): 596 def __sslErrors(self, reply, errors):
526 """ 597 """
527 Private slot to handle SSL errors. 598 Private slot to handle SSL errors.
528 599
548 edit. 619 edit.
549 620
550 @param checked state of the push button (boolean) 621 @param checked state of the push button (boolean)
551 """ 622 """
552 self.repositoryUrlEdit.setReadOnly(not checked) 623 self.repositoryUrlEdit.setReadOnly(not checked)
624
625 def __closeAndInstall(self):
626 """
627 Private method to close the dialog and invoke the install dialog.
628 """
629 if not self.__pluginsDownloaded and self.__selectedItems():
630 for itm in self.__selectedItems():
631 filename = os.path.join(
632 Preferences.getPluginManager("DownloadPath"),
633 itm.data(0, PluginRepositoryWidget.FilenameRole))
634 self.__pluginsDownloaded.append(filename)
635 self.closeAndInstall.emit()
636
637 def __hidePlugin(self):
638 """
639 Private slot to hide the current plug-in.
640 """
641 itm = self.__selectedItems()[0]
642 pluginName = (itm.data(0, PluginRepositoryWidget.FilenameRole)
643 .rsplit("-", 1)[0])
644 self.__updateHiddenPluginsList([pluginName])
645
646 def __hideSelectedPlugins(self):
647 """
648 Private slot to hide all selected plug-ins.
649 """
650 hideList = []
651 for itm in self.__selectedItems():
652 pluginName = (itm.data(0, PluginRepositoryWidget.FilenameRole)
653 .rsplit("-", 1)[0])
654 hideList.append(pluginName)
655 self.__updateHiddenPluginsList(hideList)
656
657 def __showAllPlugins(self):
658 """
659 Private slot to show all plug-ins.
660 """
661 self.__hiddenPlugins = []
662 self.__updateHiddenPluginsList([])
663
664 def __hasHiddenPlugins(self):
665 """
666 Private method to check, if there are any hidden plug-ins.
667
668 @return flag indicating the presence of hidden plug-ins (boolean)
669 """
670 return bool(self.__hiddenPlugins)
671
672 def __updateHiddenPluginsList(self, hideList):
673 """
674 Private method to store the list of hidden plug-ins to the settings.
675
676 @param hideList list of plug-ins to add to the list of hidden ones
677 (list of string)
678 """
679 if hideList:
680 self.__hiddenPlugins.extend(
681 [p for p in hideList if p not in self.__hiddenPlugins])
682 Preferences.setPluginManager("HiddenPlugins", self.__hiddenPlugins)
683 self.__populateList()
684
685 def __cleanupDownloads(self):
686 """
687 Private slot to cleanup the plug-in downloads area.
688 """
689 downloadPath = Preferences.getPluginManager("DownloadPath")
690 downloads = {} # plug-in name as key, file name as value
691
692 # step 1: extract plug-ins and downloaded files
693 for pluginFile in os.listdir(downloadPath):
694 if not os.path.isfile(os.path.join(downloadPath, pluginFile)):
695 continue
696
697 pluginName = pluginFile.rsplit("-", 1)[0]
698 if pluginName not in downloads:
699 downloads[pluginName] = []
700 downloads[pluginName].append(pluginFile)
701
702 # step 2: delete old entries
703 for pluginName in downloads:
704 downloads[pluginName].sort()
705
706 if pluginName in self.__hiddenPlugins and \
707 not Preferences.getPluginManager("KeepHidden"):
708 removeFiles = downloads[pluginName]
709 else:
710 removeFiles = downloads[pluginName][
711 :-Preferences.getPluginManager("KeepGenerations")]
712 for removeFile in removeFiles:
713 try:
714 os.remove(os.path.join(downloadPath, removeFile))
715 except (IOError, OSError) as err:
716 E5MessageBox.critical(
717 self,
718 self.tr("Cleanup of Plugin Downloads"),
719 self.tr("""<p>The plugin download <b>{0}</b> could"""
720 """ not be deleted.</p><p>Reason: {1}</p>""")
721 .format(removeFile, str(err)))
553 722
554 723
555 class PluginRepositoryDialog(QDialog): 724 class PluginRepositoryDialog(QDialog):
556 """ 725 """
557 Class for the dialog variant. 726 Class for the dialog variant.

eric ide

mercurial