src/eric7/PluginManager/PluginRepositoryDialog.py

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

eric ide

mercurial