eric7/PluginManager/PluginRepositoryDialog.py

branch
eric7
changeset 8312
800c432b34c8
parent 8268
6b8128e0c9d1
child 8314
e3642a6a1e71
equal deleted inserted replaced
8311:4e8b98454baa 8312:800c432b34c8
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2007 - 2021 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing a dialog showing the available plugins.
8 """
9
10 import sys
11 import os
12 import zipfile
13 import glob
14 import re
15
16 from PyQt5.QtCore import (
17 pyqtSignal, pyqtSlot, Qt, QFile, QIODevice, QUrl, QProcess, QPoint,
18 QCoreApplication
19 )
20 from PyQt5.QtWidgets import (
21 QWidget, QDialogButtonBox, QAbstractButton, QTreeWidgetItem, QDialog,
22 QVBoxLayout, QMenu
23 )
24 from PyQt5.QtNetwork import (
25 QNetworkAccessManager, QNetworkRequest, QNetworkReply
26 )
27
28 from .Ui_PluginRepositoryDialog import Ui_PluginRepositoryDialog
29
30 from E5Gui import E5MessageBox
31 from E5Gui.E5MainWindow import E5MainWindow
32 from E5Gui.E5Application import e5App
33
34 from E5Network.E5NetworkProxyFactory import proxyAuthenticationRequired
35 try:
36 from E5Network.E5SslErrorHandler import E5SslErrorHandler, E5SslErrorState
37 SSL_AVAILABLE = True
38 except ImportError:
39 SSL_AVAILABLE = False
40
41 import Globals
42 import Utilities
43 import Preferences
44
45 import UI.PixmapCache
46
47 from eric6config import getConfig
48
49
50 class PluginRepositoryWidget(QWidget, Ui_PluginRepositoryDialog):
51 """
52 Class implementing a dialog showing the available plugins.
53
54 @signal closeAndInstall() emitted when the Close & Install button is
55 pressed
56 """
57 closeAndInstall = pyqtSignal()
58
59 DescrRole = Qt.ItemDataRole.UserRole
60 UrlRole = Qt.ItemDataRole.UserRole + 1
61 FilenameRole = Qt.ItemDataRole.UserRole + 2
62 AuthorRole = Qt.ItemDataRole.UserRole + 3
63
64 PluginStatusUpToDate = 0
65 PluginStatusNew = 1
66 PluginStatusLocalUpdate = 2
67 PluginStatusRemoteUpdate = 3
68 PluginStatusError = 4
69
70 def __init__(self, pluginManager, parent=None):
71 """
72 Constructor
73
74 @param pluginManager reference to the plugin manager object
75 @type PluginManager
76 @param parent parent of this dialog
77 @type QWidget
78 """
79 super().__init__(parent)
80 self.setupUi(self)
81
82 if pluginManager is None:
83 # started as external plug-in repository dialog
84 from .PluginManager import PluginManager
85 self.__pluginManager = PluginManager()
86 self.__external = True
87 else:
88 self.__pluginManager = pluginManager
89 self.__external = False
90
91 self.__updateButton = self.buttonBox.addButton(
92 self.tr("Update"), QDialogButtonBox.ButtonRole.ActionRole)
93 self.__downloadButton = self.buttonBox.addButton(
94 self.tr("Download"), QDialogButtonBox.ButtonRole.ActionRole)
95 self.__downloadButton.setEnabled(False)
96 self.__downloadInstallButton = self.buttonBox.addButton(
97 self.tr("Download && Install"),
98 QDialogButtonBox.ButtonRole.ActionRole)
99 self.__downloadInstallButton.setEnabled(False)
100 self.__downloadCancelButton = self.buttonBox.addButton(
101 self.tr("Cancel"), QDialogButtonBox.ButtonRole.ActionRole)
102 self.__downloadCancelButton.setEnabled(False)
103 self.__installButton = self.buttonBox.addButton(
104 self.tr("Close && Install"),
105 QDialogButtonBox.ButtonRole.ActionRole)
106 self.__installButton.setEnabled(False)
107 self.__closeButton = self.buttonBox.button(
108 QDialogButtonBox.StandardButton.Close)
109 self.__closeButton.setEnabled(True)
110
111 self.repositoryUrlEdit.setText(
112 Preferences.getUI("PluginRepositoryUrl6"))
113
114 self.repositoryList.headerItem().setText(
115 self.repositoryList.columnCount(), "")
116 self.repositoryList.header().setSortIndicator(
117 0, Qt.SortOrder.AscendingOrder)
118
119 self.__pluginContextMenu = QMenu(self)
120 self.__hideAct = self.__pluginContextMenu.addAction(
121 self.tr("Hide"), self.__hidePlugin)
122 self.__hideSelectedAct = self.__pluginContextMenu.addAction(
123 self.tr("Hide Selected"), self.__hideSelectedPlugins)
124 self.__pluginContextMenu.addSeparator()
125 self.__showAllAct = self.__pluginContextMenu.addAction(
126 self.tr("Show All"), self.__showAllPlugins)
127 self.__pluginContextMenu.addSeparator()
128 self.__pluginContextMenu.addAction(
129 self.tr("Cleanup Downloads"), self.__cleanupDownloads)
130
131 self.pluginRepositoryFile = os.path.join(Utilities.getConfigDir(),
132 "PluginRepository")
133
134 # attributes for the network objects
135 self.__networkManager = QNetworkAccessManager(self)
136 self.__networkManager.proxyAuthenticationRequired.connect(
137 proxyAuthenticationRequired)
138 if SSL_AVAILABLE:
139 self.__sslErrorHandler = E5SslErrorHandler(self)
140 self.__networkManager.sslErrors.connect(self.__sslErrors)
141 self.__replies = []
142
143 self.__pluginsToDownload = []
144 self.__pluginsDownloaded = []
145 self.__isDownloadInstall = False
146 self.__allDownloadedOk = False
147
148 self.__hiddenPlugins = Preferences.getPluginManager("HiddenPlugins")
149
150 self.__populateList()
151
152 @pyqtSlot(QAbstractButton)
153 def on_buttonBox_clicked(self, button):
154 """
155 Private slot to handle the click of a button of the button box.
156
157 @param button reference to the button pressed (QAbstractButton)
158 """
159 if button == self.__updateButton:
160 self.__updateList()
161 elif button == self.__downloadButton:
162 self.__isDownloadInstall = False
163 self.__downloadPlugins()
164 elif button == self.__downloadInstallButton:
165 self.__isDownloadInstall = True
166 self.__allDownloadedOk = True
167 self.__downloadPlugins()
168 elif button == self.__downloadCancelButton:
169 self.__downloadCancel()
170 elif button == self.__installButton:
171 self.__closeAndInstall()
172
173 def __formatDescription(self, lines):
174 """
175 Private method to format the description.
176
177 @param lines lines of the description (list of strings)
178 @return formatted description (string)
179 """
180 # remove empty line at start and end
181 newlines = lines[:]
182 if len(newlines) and newlines[0] == '':
183 del newlines[0]
184 if len(newlines) and newlines[-1] == '':
185 del newlines[-1]
186
187 # replace empty lines by newline character
188 index = 0
189 while index < len(newlines):
190 if newlines[index] == '':
191 newlines[index] = '\n'
192 index += 1
193
194 # join lines by a blank
195 return ' '.join(newlines)
196
197 def __changeScheme(self, url, newScheme=""):
198 """
199 Private method to change the scheme of the given URL.
200
201 @param url URL to be modified
202 @type str
203 @param newScheme scheme to be set for the given URL
204 @return modified URL
205 @rtype str
206 """
207 if not newScheme:
208 newScheme = self.repositoryUrlEdit.text().split("//", 1)[0]
209
210 return newScheme + "//" + url.split("//", 1)[1]
211
212 @pyqtSlot(QPoint)
213 def on_repositoryList_customContextMenuRequested(self, pos):
214 """
215 Private slot to show the context menu.
216
217 @param pos position to show the menu (QPoint)
218 """
219 self.__hideAct.setEnabled(
220 self.repositoryList.currentItem() is not None and
221 len(self.__selectedItems()) == 1)
222 self.__hideSelectedAct.setEnabled(
223 len(self.__selectedItems()) > 1)
224 self.__showAllAct.setEnabled(bool(self.__hasHiddenPlugins()))
225 self.__pluginContextMenu.popup(self.repositoryList.mapToGlobal(pos))
226
227 @pyqtSlot(QTreeWidgetItem, QTreeWidgetItem)
228 def on_repositoryList_currentItemChanged(self, current, previous):
229 """
230 Private slot to handle the change of the current item.
231
232 @param current reference to the new current item (QTreeWidgetItem)
233 @param previous reference to the old current item (QTreeWidgetItem)
234 """
235 if self.__repositoryMissing or current is None:
236 return
237
238 url = current.data(0, PluginRepositoryWidget.UrlRole)
239 url = "" if url is None else self.__changeScheme(url)
240 self.urlEdit.setText(url)
241 self.descriptionEdit.setPlainText(
242 current.data(0, PluginRepositoryWidget.DescrRole) and
243 self.__formatDescription(
244 current.data(0, PluginRepositoryWidget.DescrRole)) or "")
245 self.authorEdit.setText(
246 current.data(0, PluginRepositoryWidget.AuthorRole) or "")
247
248 def __selectedItems(self):
249 """
250 Private method to get all selected items without the toplevel ones.
251
252 @return list of selected items (list)
253 """
254 ql = self.repositoryList.selectedItems()
255 for index in range(self.repositoryList.topLevelItemCount()):
256 ti = self.repositoryList.topLevelItem(index)
257 if ti in ql:
258 ql.remove(ti)
259 return ql
260
261 @pyqtSlot()
262 def on_repositoryList_itemSelectionChanged(self):
263 """
264 Private slot to handle a change of the selection.
265 """
266 enable = bool(self.__selectedItems())
267 self.__downloadButton.setEnabled(enable)
268 self.__downloadInstallButton.setEnabled(enable)
269 self.__installButton.setEnabled(enable)
270
271 def __updateList(self):
272 """
273 Private slot to download a new list and display the contents.
274 """
275 url = self.repositoryUrlEdit.text()
276 self.__downloadFile(url,
277 self.pluginRepositoryFile,
278 self.__downloadRepositoryFileDone)
279
280 def __downloadRepositoryFileDone(self, status, filename):
281 """
282 Private method called after the repository file was downloaded.
283
284 @param status flaging indicating a successful download (boolean)
285 @param filename full path of the downloaded file (string)
286 """
287 self.__populateList()
288
289 def __downloadPluginDone(self, status, filename):
290 """
291 Private method called, when the download of a plugin is finished.
292
293 @param status flag indicating a successful download (boolean)
294 @param filename full path of the downloaded file (string)
295 """
296 if status:
297 self.__pluginsDownloaded.append(filename)
298 if self.__isDownloadInstall:
299 self.__allDownloadedOk &= status
300
301 self.__pluginsToDownload.pop(0)
302 if len(self.__pluginsToDownload):
303 self.__downloadPlugin()
304 else:
305 self.__downloadPluginsDone()
306
307 def __downloadPlugin(self):
308 """
309 Private method to download the next plugin.
310 """
311 self.__downloadFile(self.__pluginsToDownload[0][0],
312 self.__pluginsToDownload[0][1],
313 self.__downloadPluginDone)
314
315 def __downloadPlugins(self):
316 """
317 Private slot to download the selected plugins.
318 """
319 self.__pluginsDownloaded = []
320 self.__pluginsToDownload = []
321 self.__downloadButton.setEnabled(False)
322 self.__downloadInstallButton.setEnabled(False)
323 self.__installButton.setEnabled(False)
324
325 newScheme = self.repositoryUrlEdit.text().split("//", 1)[0]
326 for itm in self.repositoryList.selectedItems():
327 if itm not in [self.__stableItem, self.__unstableItem,
328 self.__unknownItem, self.__obsoleteItem]:
329 url = self.__changeScheme(
330 itm.data(0, PluginRepositoryWidget.UrlRole),
331 newScheme)
332 filename = os.path.join(
333 Preferences.getPluginManager("DownloadPath"),
334 itm.data(0, PluginRepositoryWidget.FilenameRole))
335 self.__pluginsToDownload.append((url, filename))
336 self.__downloadPlugin()
337
338 def __downloadPluginsDone(self):
339 """
340 Private method called, when the download of the plugins is finished.
341 """
342 self.__downloadButton.setEnabled(len(self.__selectedItems()))
343 self.__downloadInstallButton.setEnabled(len(self.__selectedItems()))
344 self.__installButton.setEnabled(len(self.__selectedItems()))
345 ui = (e5App().getObject("UserInterface")
346 if not self.__external else None)
347 if ui is not None:
348 ui.showNotification(
349 UI.PixmapCache.getPixmap("plugin48"),
350 self.tr("Download Plugin Files"),
351 self.tr("""The requested plugins were downloaded."""))
352
353 if self.__isDownloadInstall:
354 self.closeAndInstall.emit()
355 else:
356 if ui is None:
357 E5MessageBox.information(
358 self,
359 self.tr("Download Plugin Files"),
360 self.tr("""The requested plugins were downloaded."""))
361
362 self.downloadProgress.setValue(0)
363
364 # repopulate the list to update the refresh icons
365 self.__populateList()
366
367 def __resortRepositoryList(self):
368 """
369 Private method to resort the tree.
370 """
371 self.repositoryList.sortItems(
372 self.repositoryList.sortColumn(),
373 self.repositoryList.header().sortIndicatorOrder())
374
375 def __populateList(self):
376 """
377 Private method to populate the list of available plugins.
378 """
379 self.repositoryList.clear()
380 self.__stableItem = None
381 self.__unstableItem = None
382 self.__unknownItem = None
383 self.__obsoleteItem = None
384
385 self.__newItems = 0
386 self.__updateLocalItems = 0
387 self.__updateRemoteItems = 0
388
389 self.downloadProgress.setValue(0)
390
391 if os.path.exists(self.pluginRepositoryFile):
392 self.__repositoryMissing = False
393 f = QFile(self.pluginRepositoryFile)
394 if f.open(QIODevice.OpenModeFlag.ReadOnly):
395 from E5XML.PluginRepositoryReader import PluginRepositoryReader
396 reader = PluginRepositoryReader(f, self.addEntry)
397 reader.readXML()
398 self.repositoryList.resizeColumnToContents(0)
399 self.repositoryList.resizeColumnToContents(1)
400 self.repositoryList.resizeColumnToContents(2)
401 self.__resortRepositoryList()
402 url = Preferences.getUI("PluginRepositoryUrl6")
403 if url != self.repositoryUrlEdit.text():
404 self.repositoryUrlEdit.setText(url)
405 E5MessageBox.warning(
406 self,
407 self.tr("Plugins Repository URL Changed"),
408 self.tr(
409 """The URL of the Plugins Repository has"""
410 """ changed. Select the "Update" button to get"""
411 """ the new repository file."""))
412 else:
413 E5MessageBox.critical(
414 self,
415 self.tr("Read plugins repository file"),
416 self.tr("<p>The plugins repository file <b>{0}</b> "
417 "could not be read. Select Update</p>")
418 .format(self.pluginRepositoryFile))
419 else:
420 self.__repositoryMissing = True
421 QTreeWidgetItem(
422 self.repositoryList,
423 ["", self.tr(
424 "No plugin repository file available.\nSelect Update.")
425 ])
426 self.repositoryList.resizeColumnToContents(1)
427
428 self.newLabel.setText(self.tr("New: <b>{0}</b>")
429 .format(self.__newItems))
430 self.updateLocalLabel.setText(self.tr("Local Updates: <b>{0}</b>")
431 .format(self.__updateLocalItems))
432 self.updateRemoteLabel.setText(self.tr("Remote Updates: <b>{0}</b>")
433 .format(self.__updateRemoteItems))
434
435 def __downloadFile(self, url, filename, doneMethod=None):
436 """
437 Private slot to download the given file.
438
439 @param url URL for the download (string)
440 @param filename local name of the file (string)
441 @param doneMethod method to be called when done
442 """
443 self.__updateButton.setEnabled(False)
444 self.__downloadButton.setEnabled(False)
445 self.__downloadInstallButton.setEnabled(False)
446 self.__closeButton.setEnabled(False)
447 self.__downloadCancelButton.setEnabled(True)
448
449 self.statusLabel.setText(url)
450
451 request = QNetworkRequest(QUrl(url))
452 request.setAttribute(
453 QNetworkRequest.Attribute.CacheLoadControlAttribute,
454 QNetworkRequest.CacheLoadControl.AlwaysNetwork)
455 reply = self.__networkManager.get(request)
456 reply.finished.connect(
457 lambda: self.__downloadFileDone(reply, filename, doneMethod))
458 reply.downloadProgress.connect(self.__downloadProgress)
459 self.__replies.append(reply)
460
461 def __downloadFileDone(self, reply, fileName, doneMethod):
462 """
463 Private method called, after the file has been downloaded
464 from the Internet.
465
466 @param reply reference to the reply object of the download
467 @type QNetworkReply
468 @param fileName local name of the file
469 @type str
470 @param doneMethod method to be called when done
471 @type func
472 """
473 self.__updateButton.setEnabled(True)
474 self.__closeButton.setEnabled(True)
475 self.__downloadCancelButton.setEnabled(False)
476
477 ok = True
478 if reply in self.__replies:
479 self.__replies.remove(reply)
480 if reply.error() != QNetworkReply.NetworkError.NoError:
481 ok = False
482 if (
483 reply.error() !=
484 QNetworkReply.NetworkError.OperationCanceledError
485 ):
486 E5MessageBox.warning(
487 self,
488 self.tr("Error downloading file"),
489 self.tr(
490 """<p>Could not download the requested file"""
491 """ from {0}.</p><p>Error: {1}</p>"""
492 ).format(reply.url().toString(), reply.errorString())
493 )
494 self.downloadProgress.setValue(0)
495 if self.repositoryList.topLevelItemCount():
496 if self.repositoryList.currentItem() is None:
497 self.repositoryList.setCurrentItem(
498 self.repositoryList.topLevelItem(0))
499 else:
500 self.__downloadButton.setEnabled(
501 len(self.__selectedItems()))
502 self.__downloadInstallButton.setEnabled(
503 len(self.__selectedItems()))
504 reply.deleteLater()
505 return
506
507 downloadIODevice = QFile(fileName + ".tmp")
508 downloadIODevice.open(QIODevice.OpenModeFlag.WriteOnly)
509 # read data in chunks
510 chunkSize = 64 * 1024 * 1024
511 while True:
512 data = reply.read(chunkSize)
513 if data is None or len(data) == 0:
514 break
515 downloadIODevice.write(data)
516 downloadIODevice.close()
517 if QFile.exists(fileName):
518 QFile.remove(fileName)
519 downloadIODevice.rename(fileName)
520 reply.deleteLater()
521
522 if doneMethod is not None:
523 doneMethod(ok, fileName)
524
525 def __downloadCancel(self, reply=None):
526 """
527 Private slot to cancel the current download.
528
529 @param reply reference to the network reply
530 @type QNetworkReply
531 """
532 if reply is None and bool(self.__replies):
533 reply = self.__replies[0]
534 self.__pluginsToDownload = []
535 if reply is not None:
536 reply.abort()
537
538 def __downloadProgress(self, done, total):
539 """
540 Private slot to show the download progress.
541
542 @param done number of bytes downloaded so far (integer)
543 @param total total bytes to be downloaded (integer)
544 """
545 if total:
546 self.downloadProgress.setMaximum(total)
547 self.downloadProgress.setValue(done)
548
549 def addEntry(self, name, short, description, url, author, version,
550 filename, status):
551 """
552 Public method to add an entry to the list.
553
554 @param name data for the name field (string)
555 @param short data for the short field (string)
556 @param description data for the description field (list of strings)
557 @param url data for the url field (string)
558 @param author data for the author field (string)
559 @param version data for the version field (string)
560 @param filename data for the filename field (string)
561 @param status status of the plugin (string [stable, unstable, unknown])
562 """
563 pluginName = filename.rsplit("-", 1)[0]
564 if pluginName in self.__hiddenPlugins:
565 return
566
567 if status == "stable":
568 if self.__stableItem is None:
569 self.__stableItem = QTreeWidgetItem(
570 self.repositoryList, [self.tr("Stable")])
571 self.__stableItem.setExpanded(True)
572 parent = self.__stableItem
573 elif status == "unstable":
574 if self.__unstableItem is None:
575 self.__unstableItem = QTreeWidgetItem(
576 self.repositoryList, [self.tr("Unstable")])
577 self.__unstableItem.setExpanded(True)
578 parent = self.__unstableItem
579 elif status == "obsolete":
580 if self.__obsoleteItem is None:
581 self.__obsoleteItem = QTreeWidgetItem(
582 self.repositoryList, [self.tr("Obsolete")])
583 self.__obsoleteItem.setExpanded(True)
584 parent = self.__obsoleteItem
585 else:
586 if self.__unknownItem is None:
587 self.__unknownItem = QTreeWidgetItem(
588 self.repositoryList, [self.tr("Unknown")])
589 self.__unknownItem.setExpanded(True)
590 parent = self.__unknownItem
591 itm = QTreeWidgetItem(parent, [name, version, short])
592
593 itm.setData(0, PluginRepositoryWidget.UrlRole, url)
594 itm.setData(0, PluginRepositoryWidget.FilenameRole, filename)
595 itm.setData(0, PluginRepositoryWidget.AuthorRole, author)
596 itm.setData(0, PluginRepositoryWidget.DescrRole, description)
597
598 updateStatus = self.__updateStatus(filename, version)
599 if updateStatus == PluginRepositoryWidget.PluginStatusUpToDate:
600 itm.setIcon(1, UI.PixmapCache.getIcon("empty"))
601 itm.setToolTip(1, self.tr("up-to-date"))
602 elif updateStatus == PluginRepositoryWidget.PluginStatusNew:
603 itm.setIcon(1, UI.PixmapCache.getIcon("download"))
604 itm.setToolTip(1, self.tr("new download available"))
605 self.__newItems += 1
606 elif updateStatus == PluginRepositoryWidget.PluginStatusLocalUpdate:
607 itm.setIcon(1, UI.PixmapCache.getIcon("updateLocal"))
608 itm.setToolTip(1, self.tr("update installable"))
609 self.__updateLocalItems += 1
610 elif updateStatus == PluginRepositoryWidget.PluginStatusRemoteUpdate:
611 itm.setIcon(1, UI.PixmapCache.getIcon("updateRemote"))
612 itm.setToolTip(1, self.tr("updated download available"))
613 self.__updateRemoteItems += 1
614 elif updateStatus == PluginRepositoryWidget.PluginStatusError:
615 itm.setIcon(1, UI.PixmapCache.getIcon("warning"))
616 itm.setToolTip(1, self.tr("error determining status"))
617
618 def __updateStatus(self, filename, version):
619 """
620 Private method to check the given archive update status.
621
622 @param filename data for the filename field (string)
623 @param version data for the version field (string)
624 @return plug-in update status (integer, one of PluginStatusNew,
625 PluginStatusUpToDate, PluginStatusLocalUpdate,
626 PluginStatusRemoteUpdate)
627 """
628 archive = os.path.join(Preferences.getPluginManager("DownloadPath"),
629 filename)
630
631 # check, if it is an update (i.e. we already have archives
632 # with the same pattern)
633 archivesPattern = archive.rsplit('-', 1)[0] + "-*.zip"
634 if len(glob.glob(archivesPattern)) == 0:
635 # Check against installed/loaded plug-ins
636 pluginName = filename.rsplit('-', 1)[0]
637 pluginDetails = self.__pluginManager.getPluginDetails(pluginName)
638 if (
639 pluginDetails is None or
640 pluginDetails["moduleName"] != pluginName
641 ):
642 return PluginRepositoryWidget.PluginStatusNew
643 if pluginDetails["error"]:
644 return PluginRepositoryWidget.PluginStatusError
645 pluginVersionTuple = Globals.versionToTuple(
646 pluginDetails["version"])[:3]
647 versionTuple = Globals.versionToTuple(version)[:3]
648 if pluginVersionTuple < versionTuple:
649 return PluginRepositoryWidget.PluginStatusRemoteUpdate
650 else:
651 return PluginRepositoryWidget.PluginStatusUpToDate
652
653 # check, if the archive exists
654 if not os.path.exists(archive):
655 return PluginRepositoryWidget.PluginStatusRemoteUpdate
656
657 # check, if the archive is a valid zip file
658 if not zipfile.is_zipfile(archive):
659 return PluginRepositoryWidget.PluginStatusRemoteUpdate
660
661 zipFile = zipfile.ZipFile(archive, "r")
662 try:
663 aversion = zipFile.read("VERSION").decode("utf-8")
664 except KeyError:
665 aversion = ""
666 zipFile.close()
667
668 if aversion == version:
669 # Check against installed/loaded plug-ins
670 pluginName = filename.rsplit('-', 1)[0]
671 pluginDetails = self.__pluginManager.getPluginDetails(pluginName)
672 if pluginDetails is None:
673 return PluginRepositoryWidget.PluginStatusLocalUpdate
674 if (
675 Globals.versionToTuple(pluginDetails["version"])[:3] <
676 Globals.versionToTuple(version)[:3]
677 ):
678 return PluginRepositoryWidget.PluginStatusLocalUpdate
679 else:
680 return PluginRepositoryWidget.PluginStatusUpToDate
681 else:
682 return PluginRepositoryWidget.PluginStatusRemoteUpdate
683
684 def __sslErrors(self, reply, errors):
685 """
686 Private slot to handle SSL errors.
687
688 @param reply reference to the reply object (QNetworkReply)
689 @param errors list of SSL errors (list of QSslError)
690 """
691 ignored = self.__sslErrorHandler.sslErrorsReply(reply, errors)[0]
692 if ignored == E5SslErrorState.NOT_IGNORED:
693 self.__downloadCancel(reply)
694
695 def getDownloadedPlugins(self):
696 """
697 Public method to get the list of recently downloaded plugin files.
698
699 @return list of plugin filenames (list of strings)
700 """
701 return self.__pluginsDownloaded
702
703 @pyqtSlot(bool)
704 def on_repositoryUrlEditButton_toggled(self, checked):
705 """
706 Private slot to set the read only status of the repository URL line
707 edit.
708
709 @param checked state of the push button (boolean)
710 """
711 self.repositoryUrlEdit.setReadOnly(not checked)
712
713 def __closeAndInstall(self):
714 """
715 Private method to close the dialog and invoke the install dialog.
716 """
717 if not self.__pluginsDownloaded and self.__selectedItems():
718 for itm in self.__selectedItems():
719 filename = os.path.join(
720 Preferences.getPluginManager("DownloadPath"),
721 itm.data(0, PluginRepositoryWidget.FilenameRole))
722 self.__pluginsDownloaded.append(filename)
723 self.closeAndInstall.emit()
724
725 def __hidePlugin(self):
726 """
727 Private slot to hide the current plug-in.
728 """
729 itm = self.__selectedItems()[0]
730 pluginName = (itm.data(0, PluginRepositoryWidget.FilenameRole)
731 .rsplit("-", 1)[0])
732 self.__updateHiddenPluginsList([pluginName])
733
734 def __hideSelectedPlugins(self):
735 """
736 Private slot to hide all selected plug-ins.
737 """
738 hideList = []
739 for itm in self.__selectedItems():
740 pluginName = (itm.data(0, PluginRepositoryWidget.FilenameRole)
741 .rsplit("-", 1)[0])
742 hideList.append(pluginName)
743 self.__updateHiddenPluginsList(hideList)
744
745 def __showAllPlugins(self):
746 """
747 Private slot to show all plug-ins.
748 """
749 self.__hiddenPlugins = []
750 self.__updateHiddenPluginsList([])
751
752 def __hasHiddenPlugins(self):
753 """
754 Private method to check, if there are any hidden plug-ins.
755
756 @return flag indicating the presence of hidden plug-ins (boolean)
757 """
758 return bool(self.__hiddenPlugins)
759
760 def __updateHiddenPluginsList(self, hideList):
761 """
762 Private method to store the list of hidden plug-ins to the settings.
763
764 @param hideList list of plug-ins to add to the list of hidden ones
765 (list of string)
766 """
767 if hideList:
768 self.__hiddenPlugins.extend(
769 [p for p in hideList if p not in self.__hiddenPlugins])
770 Preferences.setPluginManager("HiddenPlugins", self.__hiddenPlugins)
771 self.__populateList()
772
773 def __cleanupDownloads(self):
774 """
775 Private slot to cleanup the plug-in downloads area.
776 """
777 PluginRepositoryDownloadCleanup()
778
779
780 class PluginRepositoryDialog(QDialog):
781 """
782 Class for the dialog variant.
783 """
784 def __init__(self, pluginManager, parent=None):
785 """
786 Constructor
787
788 @param pluginManager reference to the plugin manager object
789 @type PluginManager
790 @param parent reference to the parent widget
791 @type QWidget
792 """
793 super().__init__(parent)
794 self.setSizeGripEnabled(True)
795
796 self.__layout = QVBoxLayout(self)
797 self.__layout.setContentsMargins(0, 0, 0, 0)
798 self.setLayout(self.__layout)
799
800 self.cw = PluginRepositoryWidget(pluginManager, self)
801 size = self.cw.size()
802 self.__layout.addWidget(self.cw)
803 self.resize(size)
804 self.setWindowTitle(self.cw.windowTitle())
805
806 self.cw.buttonBox.accepted.connect(self.accept)
807 self.cw.buttonBox.rejected.connect(self.reject)
808 self.cw.closeAndInstall.connect(self.__closeAndInstall)
809
810 def __closeAndInstall(self):
811 """
812 Private slot to handle the closeAndInstall signal.
813 """
814 self.done(QDialog.DialogCode.Accepted + 1)
815
816 def getDownloadedPlugins(self):
817 """
818 Public method to get the list of recently downloaded plugin files.
819
820 @return list of plugin filenames (list of strings)
821 """
822 return self.cw.getDownloadedPlugins()
823
824
825 class PluginRepositoryWindow(E5MainWindow):
826 """
827 Main window class for the standalone dialog.
828 """
829 def __init__(self, parent=None):
830 """
831 Constructor
832
833 @param parent reference to the parent widget (QWidget)
834 """
835 super().__init__(parent)
836 self.cw = PluginRepositoryWidget(None, self)
837 size = self.cw.size()
838 self.setCentralWidget(self.cw)
839 self.resize(size)
840 self.setWindowTitle(self.cw.windowTitle())
841
842 self.setStyle(Preferences.getUI("Style"),
843 Preferences.getUI("StyleSheet"))
844
845 self.cw.buttonBox.accepted.connect(self.close)
846 self.cw.buttonBox.rejected.connect(self.close)
847 self.cw.closeAndInstall.connect(self.__startPluginInstall)
848
849 def __startPluginInstall(self):
850 """
851 Private slot to start the eric plugin installation dialog.
852 """
853 proc = QProcess()
854 applPath = os.path.join(getConfig("ericDir"), "eric6_plugininstall.py")
855
856 args = []
857 args.append(applPath)
858 args += self.cw.getDownloadedPlugins()
859
860 if (
861 not os.path.isfile(applPath) or
862 not proc.startDetached(sys.executable, args)
863 ):
864 E5MessageBox.critical(
865 self,
866 self.tr('Process Generation Error'),
867 self.tr(
868 '<p>Could not start the process.<br>'
869 'Ensure that it is available as <b>{0}</b>.</p>'
870 ).format(applPath),
871 self.tr('OK'))
872
873 self.close()
874
875
876 def PluginRepositoryDownloadCleanup(quiet=False):
877 """
878 Module function to clean up the plug-in downloads area.
879
880 @param quiet flag indicating quiet operations
881 @type bool
882 """
883 pluginsRegister = [] # list of plug-ins contained in the repository
884
885 def registerPlugin(name, short, description, url, author, version,
886 filename, status):
887 """
888 Method to register a plug-in's data.
889
890 @param name data for the name field (string)
891 @param short data for the short field (string)
892 @param description data for the description field (list of strings)
893 @param url data for the url field (string)
894 @param author data for the author field (string)
895 @param version data for the version field (string)
896 @param filename data for the filename field (string)
897 @param status status of the plugin (string [stable, unstable, unknown])
898 """
899 pluginName = os.path.splitext(url.rsplit("/", 1)[1])[0]
900 if pluginName not in pluginsRegister:
901 pluginsRegister.append(pluginName)
902
903 downloadPath = Preferences.getPluginManager("DownloadPath")
904 downloads = {} # plug-in name as key, file name as value
905
906 # step 1: extract plug-ins and downloaded files
907 for pluginFile in os.listdir(downloadPath):
908 if not os.path.isfile(os.path.join(downloadPath, pluginFile)):
909 continue
910
911 try:
912 pluginName, pluginVersion = (
913 pluginFile.replace(".zip", "").rsplit("-", 1)
914 )
915 pluginVersionList = re.split("[._-]", pluginVersion)
916 for index in range(len(pluginVersionList)):
917 try:
918 pluginVersionList[index] = int(pluginVersionList[index])
919 except ValueError:
920 # use default of 0
921 pluginVersionList[index] = 0
922 except ValueError:
923 # rsplit() returned just one entry, i.e. file name doesn't contain
924 # version info separated by '-'
925 # => assume version 0.0.0
926 pluginName = pluginFile.replace(".zip", "")
927 pluginVersionList = [0, 0, 0]
928
929 if pluginName not in downloads:
930 downloads[pluginName] = []
931 downloads[pluginName].append((pluginFile, tuple(pluginVersionList)))
932
933 # step 2: delete old entries
934 hiddenPlugins = Preferences.getPluginManager("HiddenPlugins")
935 for pluginName in downloads:
936 downloads[pluginName].sort(key=lambda x: x[1])
937
938 removeFiles = (
939 [f[0] for f in downloads[pluginName]]
940 if (pluginName in hiddenPlugins and
941 not Preferences.getPluginManager("KeepHidden")) else
942 [f[0] for f in downloads[pluginName][
943 :-Preferences.getPluginManager("KeepGenerations")]]
944 )
945 for removeFile in removeFiles:
946 try:
947 os.remove(os.path.join(downloadPath, removeFile))
948 except OSError as err:
949 if not quiet:
950 E5MessageBox.critical(
951 None,
952 QCoreApplication.translate(
953 "PluginRepositoryWidget",
954 "Cleanup of Plugin Downloads"),
955 QCoreApplication.translate(
956 "PluginRepositoryWidget",
957 """<p>The plugin download <b>{0}</b> could"""
958 """ not be deleted.</p><p>Reason: {1}</p>""")
959 .format(removeFile, str(err)))
960
961 # step 3: delete entries of obsolete plug-ins
962 pluginRepositoryFile = os.path.join(Utilities.getConfigDir(),
963 "PluginRepository")
964 if os.path.exists(pluginRepositoryFile):
965 f = QFile(pluginRepositoryFile)
966 if f.open(QIODevice.OpenModeFlag.ReadOnly):
967 from E5XML.PluginRepositoryReader import PluginRepositoryReader
968 reader = PluginRepositoryReader(f, registerPlugin)
969 reader.readXML()
970
971 for pluginName in downloads:
972 if pluginName not in pluginsRegister:
973 removeFiles = [f[0] for f in downloads[pluginName]]
974 for removeFile in removeFiles:
975 try:
976 os.remove(os.path.join(downloadPath, removeFile))
977 except OSError as err:
978 if not quiet:
979 E5MessageBox.critical(
980 None,
981 QCoreApplication.translate(
982 "PluginRepositoryWidget",
983 "Cleanup of Plugin Downloads"),
984 QCoreApplication.translate(
985 "PluginRepositoryWidget",
986 "<p>The plugin download <b>{0}</b>"
987 " could not be deleted.</p>"
988 "<p>Reason: {1}</p>""")
989 .format(removeFile, str(err)))

eric ide

mercurial