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 |
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 |
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 """ |
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. |