PluginManager/PluginRepositoryDialog.py

changeset 0
de9c2efb9d02
child 7
c679fb30c8f3
equal deleted inserted replaced
-1:000000000000 0:de9c2efb9d02
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2007 - 2009 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6
7 """
8 Module implementing a dialog showing the available plugins.
9 """
10
11 import sys
12 import os
13 import zipfile
14 import cStringIO
15
16 from PyQt4.QtGui import *
17 from PyQt4.QtCore import *
18 from PyQt4.QtNetwork import QHttp, QNetworkProxy
19
20 from Ui_PluginRepositoryDialog import Ui_PluginRepositoryDialog
21
22 from UI.AuthenticationDialog import AuthenticationDialog
23
24 from E4XML.XMLUtilities import make_parser
25 from E4XML.XMLErrorHandler import XMLErrorHandler, XMLFatalParseError
26 from E4XML.XMLEntityResolver import XMLEntityResolver
27 from E4XML.PluginRepositoryHandler import PluginRepositoryHandler
28
29 import Utilities
30 import Preferences
31
32 import UI.PixmapCache
33
34 from eric4config import getConfig
35
36 descrRole = Qt.UserRole
37 urlRole = Qt.UserRole + 1
38 filenameRole = Qt.UserRole + 2
39 authorRole = Qt.UserRole + 3
40
41 class PluginRepositoryWidget(QWidget, Ui_PluginRepositoryDialog):
42 """
43 Class implementing a dialog showing the available plugins.
44
45 @signal closeAndInstall emitted when the Close & Install button is pressed
46 """
47 def __init__(self, parent = None):
48 """
49 Constructor
50
51 @param parent parent of this dialog (QWidget)
52 """
53 QWidget.__init__(self, parent)
54 self.setupUi(self)
55
56 self.__updateButton = \
57 self.buttonBox.addButton(self.trUtf8("Update"), QDialogButtonBox.ActionRole)
58 self.__downloadButton = \
59 self.buttonBox.addButton(self.trUtf8("Download"), QDialogButtonBox.ActionRole)
60 self.__downloadButton.setEnabled(False)
61 self.__downloadCancelButton = \
62 self.buttonBox.addButton(self.trUtf8("Cancel"), QDialogButtonBox.ActionRole)
63 self.__installButton = \
64 self.buttonBox.addButton(self.trUtf8("Close && Install"),
65 QDialogButtonBox.ActionRole)
66 self.__downloadCancelButton.setEnabled(False)
67 self.__installButton.setEnabled(False)
68
69 self.repositoryList.headerItem().setText(self.repositoryList.columnCount(), "")
70 self.repositoryList.header().setSortIndicator(0, Qt.AscendingOrder)
71
72 self.pluginRepositoryFile = \
73 os.path.join(Utilities.getConfigDir(), "PluginRepository")
74
75 self.__http = None
76 self.__doneMethod = None
77 self.__inDownload = False
78 self.__pluginsToDownload = []
79 self.__pluginsDownloaded = []
80
81 self.__populateList()
82
83 @pyqtSlot(QAbstractButton)
84 def on_buttonBox_clicked(self, button):
85 """
86 Private slot to handle the click of a button of the button box.
87 """
88 if button == self.__updateButton:
89 self.__updateList()
90 elif button == self.__downloadButton:
91 self.__downloadPlugins()
92 elif button == self.__downloadCancelButton:
93 self.__downloadCancel()
94 elif button == self.__installButton:
95 self.emit(SIGNAL("closeAndInstall"))
96
97 def __formatDescription(self, lines):
98 """
99 Private method to format the description.
100
101 @param lines lines of the description (list of strings)
102 @return formatted description (string)
103 """
104 # remove empty line at start and end
105 newlines = lines[:]
106 if len(newlines) and newlines[0] == '':
107 del newlines[0]
108 if len(newlines) and newlines[-1] == '':
109 del newlines[-1]
110
111 # replace empty lines by newline character
112 index = 0
113 while index < len(newlines):
114 if newlines[index] == '':
115 newlines[index] = '\n'
116 index += 1
117
118 # join lines by a blank
119 return ' '.join(newlines)
120
121 @pyqtSlot(QTreeWidgetItem, QTreeWidgetItem)
122 def on_repositoryList_currentItemChanged(self, current, previous):
123 """
124 Private slot to handle the change of the current item.
125
126 @param current reference to the new current item (QTreeWidgetItem)
127 @param previous reference to the old current item (QTreeWidgetItem)
128 """
129 if self.__repositoryMissing or current is None:
130 return
131
132 self.urlEdit.setText(current.data(0, urlRole).toString())
133 self.descriptionEdit.setPlainText(
134 self.__formatDescription(current.data(0, descrRole).toStringList()))
135 self.authorEdit.setText(current.data(0, authorRole).toString())
136
137 def __selectedItems(self):
138 """
139 Private method to get all selected items without the toplevel ones.
140
141 @return list of selected items (list)
142 """
143 ql = self.repositoryList.selectedItems()
144 for index in range(self.repositoryList.topLevelItemCount()):
145 ti = self.repositoryList.topLevelItem(index)
146 if ti in ql:
147 ql.remove(ti)
148 return ql
149
150 @pyqtSlot()
151 def on_repositoryList_itemSelectionChanged(self):
152 """
153 Private slot to handle a change of the selection.
154 """
155 self.__downloadButton.setEnabled(len(self.__selectedItems()))
156
157 def __updateList(self):
158 """
159 Private slot to download a new list and display the contents.
160 """
161 url = Preferences.getUI("PluginRepositoryUrl5")
162 self.__downloadFile(url,
163 self.pluginRepositoryFile,
164 self.__downloadRepositoryFileDone)
165
166 def __downloadRepositoryFileDone(self, status, filename):
167 """
168 Private method called after the repository file was downloaded.
169
170 @param status flaging indicating a successful download (boolean)
171 @param filename full path of the downloaded file (string)
172 """
173 self.__populateList()
174
175 def __downloadPluginDone(self, status, filename):
176 """
177 Private method called, when the download of a plugin is finished.
178
179 @param status flaging indicating a successful download (boolean)
180 @param filename full path of the downloaded file (string)
181 """
182 if status:
183 self.__pluginsDownloaded.append(filename)
184
185 del self.__pluginsToDownload[0]
186 if len(self.__pluginsToDownload):
187 self.__downloadPlugin()
188 else:
189 self.__downloadPluginsDone()
190
191 def __downloadPlugin(self):
192 """
193 Private method to download the next plugin.
194 """
195 self.__downloadFile(self.__pluginsToDownload[0][0],
196 self.__pluginsToDownload[0][1],
197 self.__downloadPluginDone)
198
199 def __downloadPlugins(self):
200 """
201 Private slot to download the selected plugins.
202 """
203 self.__pluginsDownloaded = []
204 self.__pluginsToDownload = []
205 self.__downloadButton.setEnabled(False)
206 self.__installButton.setEnabled(False)
207 for itm in self.repositoryList.selectedItems():
208 if itm not in [self.__stableItem, self.__unstableItem, self.__unknownItem]:
209 url = itm.data(0, urlRole).toString()
210 filename = os.path.join(
211 Preferences.getPluginManager("DownloadPath"),
212 itm.data(0, filenameRole).toString())
213 self.__pluginsToDownload.append((url, filename))
214 self.__downloadPlugin()
215
216 def __downloadPluginsDone(self):
217 """
218 Private method called, when the download of the plugins is finished.
219 """
220 self.__downloadButton.setEnabled(len(self.__selectedItems()))
221 self.__installButton.setEnabled(True)
222 self.__doneMethod = None
223 QMessageBox.information(None,
224 self.trUtf8("Download Plugin Files"),
225 self.trUtf8("""The requested plugins were downloaded."""))
226 self.downloadProgress.setValue(0)
227
228 # repopulate the list to update the refresh icons
229 self.__populateList()
230
231 def __resortRepositoryList(self):
232 """
233 Private method to resort the tree.
234 """
235 self.repositoryList.sortItems(self.repositoryList.sortColumn(),
236 self.repositoryList.header().sortIndicatorOrder())
237
238 def __populateList(self):
239 """
240 Private method to populate the list of available plugins.
241 """
242 self.repositoryList.clear()
243 self.__stableItem = None
244 self.__unstableItem = None
245 self.__unknownItem = None
246
247 self.downloadProgress.setValue(0)
248 self.__doneMethod = None
249
250 if os.path.exists(self.pluginRepositoryFile):
251 self.__repositoryMissing = False
252 try:
253 f = open(self.pluginRepositoryFile, "rb")
254 line = f.readline()
255 dtdLine = f.readline()
256 f.close()
257 except IOError:
258 QMessageBox.critical(None,
259 self.trUtf8("Read plugins repository file"),
260 self.trUtf8("<p>The plugins repository file <b>{0}</b> "
261 "could not be read. Select Update</p>")\
262 .format(self.pluginRepositoryFile))
263 return
264
265 # now read the file
266 if line.startswith('<?xml'):
267 parser = make_parser(dtdLine.startswith("<!DOCTYPE"))
268 handler = PluginRepositoryHandler(self)
269 er = XMLEntityResolver()
270 eh = XMLErrorHandler()
271
272 parser.setContentHandler(handler)
273 parser.setEntityResolver(er)
274 parser.setErrorHandler(eh)
275
276 try:
277 f = open(self.pluginRepositoryFile, "rb")
278 try:
279 try:
280 parser.parse(f)
281 except UnicodeEncodeError:
282 f.seek(0)
283 buf = cStringIO.StringIO(f.read())
284 parser.parse(buf)
285 finally:
286 f.close()
287 except IOError:
288 QMessageBox.critical(None,
289 self.trUtf8("Read plugins repository file"),
290 self.trUtf8("<p>The plugins repository file <b>{0}</b> "
291 "could not be read. Select Update</p>")\
292 .format(self.pluginRepositoryFile))
293 return
294 except XMLFatalParseError:
295 pass
296
297 eh.showParseMessages()
298
299 self.repositoryList.resizeColumnToContents(0)
300 self.repositoryList.resizeColumnToContents(1)
301 self.repositoryList.resizeColumnToContents(2)
302 self.__resortRepositoryList()
303 else:
304 QMessageBox.critical(None,
305 self.trUtf8("Read plugins repository file"),
306 self.trUtf8("<p>The plugins repository file <b>{0}</b> "
307 "has an unsupported format.</p>")\
308 .format(self.pluginRepositoryFile))
309 else:
310 self.__repositoryMissing = True
311 QTreeWidgetItem(self.repositoryList,
312 ["",
313 self.trUtf8("No plugin repository file available.\nSelect Update.")
314 ])
315 self.repositoryList.resizeColumnToContents(1)
316
317 def __downloadFile(self, url, filename, doneMethod = None):
318 """
319 Private slot to download the given file.
320
321 @param url URL for the download (string)
322 @param filename local name of the file (string)
323 @param doneMethod method to be called when done
324 """
325 if self.__http is None:
326 self.__http = QHttp()
327 self.connect(self.__http, SIGNAL("done(bool)"), self.__downloadFileDone)
328 self.connect(self.__http, SIGNAL("dataReadProgress(int, int)"),
329 self.__dataReadProgress)
330 self.connect(self.__http,
331 SIGNAL('proxyAuthenticationRequired(const QNetworkProxy &, QAuthenticator *)'),
332 self.__proxyAuthenticationRequired)
333 self.connect(self.__http, SIGNAL("sslErrors(const QList<QSslError>&)"),
334 self.__sslErrors)
335
336 if Preferences.getUI("UseProxy"):
337 host = Preferences.getUI("ProxyHost")
338 if not host:
339 QMessageBox.critical(None,
340 self.trUtf8("Error downloading file"),
341 self.trUtf8("""Proxy usage was activated"""
342 """ but no proxy host configured."""))
343 return
344 else:
345 pProxyType = Preferences.getUI("ProxyType")
346 if pProxyType == 0:
347 proxyType = QNetworkProxy.HttpProxy
348 elif pProxyType == 1:
349 proxyType = QNetworkProxy.HttpCachingProxy
350 elif pProxyType == 2:
351 proxyType = QNetworkProxy.Socks5Proxy
352 self.__proxy = QNetworkProxy(proxyType, host,
353 Preferences.getUI("ProxyPort"),
354 Preferences.getUI("ProxyUser"),
355 Preferences.getUI("ProxyPassword"))
356 self.__http.setProxy(self.__proxy)
357
358 self.__updateButton.setEnabled(False)
359 self.__downloadButton.setEnabled(False)
360 self.__downloadCancelButton.setEnabled(True)
361
362 self.statusLabel.setText(url)
363
364 self.__doneMethod = doneMethod
365 self.__downloadURL = url
366 self.__downloadFileName = filename
367 self.__downloadIODevice = QFile(self.__downloadFileName + ".tmp")
368 self.__downloadCancelled = False
369
370 if QUrl(url).scheme().lower() == 'https':
371 connectionMode = QHttp.ConnectionModeHttps
372 else:
373 connectionMode = QHttp.ConnectionModeHttp
374 self.__http.setHost(QUrl(url).host(), connectionMode, QUrl(url).port(0))
375 self.__http.get(QUrl(url).path(), self.__downloadIODevice)
376
377 def __downloadFileDone(self, error):
378 """
379 Private method called, after the file has been downloaded
380 from the internet.
381
382 @param error flag indicating an error condition (boolean)
383 """
384 self.__updateButton.setEnabled(True)
385 self.__downloadCancelButton.setEnabled(False)
386 self.statusLabel.setText(" ")
387
388 ok = True
389 if error or self.__http.lastResponse().statusCode() != 200:
390 ok = False
391 if not self.__downloadCancelled:
392 if error:
393 msg = self.__http.errorString()
394 else:
395 msg = self.__http.lastResponse().reasonPhrase()
396 QMessageBox.warning(None,
397 self.trUtf8("Error downloading file"),
398 self.trUtf8(
399 """<p>Could not download the requested file from {0}.</p>"""
400 """<p>Error: {1}</p>"""
401 ).format(self.__downloadURL, msg)
402 )
403 self.downloadProgress.setValue(0)
404 self.__downloadURL = None
405 self.__downloadIODevice.remove()
406 self.__downloadIODevice = None
407 if self.repositoryList.topLevelItemCount():
408 if self.repositoryList.currentItem() is None:
409 self.repositoryList.setCurrentItem(
410 self.repositoryList.topLevelItem(0))
411 else:
412 self.__downloadButton.setEnabled(len(self.__selectedItems()))
413 return
414
415 if QFile.exists(self.__downloadFileName):
416 QFile.remove(self.__downloadFileName)
417 self.__downloadIODevice.rename(self.__downloadFileName)
418 self.__downloadIODevice = None
419 self.__downloadURL = None
420
421 if self.__doneMethod is not None:
422 self.__doneMethod(ok, self.__downloadFileName)
423
424 def __downloadCancel(self):
425 """
426 Private slot to cancel the current download.
427 """
428 if self.__http is not None:
429 self.__downloadCancelled = True
430 self.__pluginsToDownload = []
431 self.__http.abort()
432
433 def __dataReadProgress(self, done, total):
434 """
435 Private slot to show the download progress.
436
437 @param done number of bytes downloaded so far (integer)
438 @param total total bytes to be downloaded (integer)
439 """
440 self.downloadProgress.setMaximum(total)
441 self.downloadProgress.setValue(done)
442
443 def addEntry(self, name, short, description, url, author, version, filename, status):
444 """
445 Public method to add an entry to the list.
446
447 @param name data for the name field (string)
448 @param short data for the short field (string)
449 @param description data for the description field (list of strings)
450 @param url data for the url field (string)
451 @param author data for the author field (string)
452 @param version data for the version field (string)
453 @param filename data for the filename field (string)
454 @param status status of the plugin (string [stable, unstable, unknown])
455 """
456 if status == "stable":
457 if self.__stableItem is None:
458 self.__stableItem = \
459 QTreeWidgetItem(self.repositoryList, [self.trUtf8("Stable")])
460 self.__stableItem.setExpanded(True)
461 parent = self.__stableItem
462 elif status == "unstable":
463 if self.__unstableItem is None:
464 self.__unstableItem = \
465 QTreeWidgetItem(self.repositoryList, [self.trUtf8("Unstable")])
466 self.__unstableItem.setExpanded(True)
467 parent = self.__unstableItem
468 else:
469 if self.__unknownItem is None:
470 self.__unknownItem = \
471 QTreeWidgetItem(self.repositoryList, [self.trUtf8("Unknown")])
472 self.__unknownItem.setExpanded(True)
473 parent = self.__unknownItem
474 itm = QTreeWidgetItem(parent, [name, version, short])
475
476 itm.setData(0, urlRole, QVariant(url))
477 itm.setData(0, filenameRole, QVariant(filename))
478 itm.setData(0, authorRole, QVariant(author))
479 itm.setData(0, descrRole, QVariant(description))
480
481 if self.__isUpToDate(filename, version):
482 itm.setIcon(1, UI.PixmapCache.getIcon("empty.png"))
483 else:
484 itm.setIcon(1, UI.PixmapCache.getIcon("download.png"))
485
486 def __isUpToDate(self, filename, version):
487 """
488 Private method to check, if the given archive is up-to-date.
489
490 @param filename data for the filename field (string)
491 @param version data for the version field (string)
492 @return flag indicating up-to-date (boolean)
493 """
494 archive = os.path.join(Preferences.getPluginManager("DownloadPath"),
495 filename)
496
497 # check, if the archive exists
498 if not os.path.exists(archive):
499 return False
500
501 # check, if the archive is a valid zip file
502 if not zipfile.is_zipfile(archive):
503 return False
504
505 zip = zipfile.ZipFile(archive, "r")
506 try:
507 aversion = zip.read("VERSION")
508 except KeyError:
509 aversion = ""
510 zip.close()
511
512 return aversion == version
513
514 def __proxyAuthenticationRequired(self, proxy, auth):
515 """
516 Private slot to handle a proxy authentication request.
517
518 @param proxy reference to the proxy object (QNetworkProxy)
519 @param auth reference to the authenticator object (QAuthenticator)
520 """
521 info = self.trUtf8("<b>Connect to proxy '{0}' using:</b>")\
522 .format(Qt.escape(proxy.hostName()))
523
524 dlg = AuthenticationDialog(info, proxy.user(), True)
525 if dlg.exec_() == QDialog.Accepted:
526 username, password = dlg.getData()
527 auth.setUser(username)
528 auth.setPassword(password)
529 if dlg.shallSave():
530 Preferences.setUI("ProxyUser", username)
531 Preferences.setUI("ProxyPassword", password)
532
533 def __sslErrors(self, sslErrors):
534 """
535 Private slot to handle SSL errors.
536
537 @param sslErrors list of SSL errors (list of QSslError)
538 """
539 errorStrings = []
540 for err in sslErrors:
541 errorStrings.append(err.errorString())
542 errorString = '.<br />'.join(errorStrings)
543 ret = QMessageBox.warning(self,
544 self.trUtf8("SSL Errors"),
545 self.trUtf8("""<p>SSL Errors:</p>"""
546 """<p>{0}</p>"""
547 """<p>Do you want to ignore these errors?</p>""")\
548 .format(errorString),
549 QMessageBox.StandardButtons(\
550 QMessageBox.No | \
551 QMessageBox.Yes),
552 QMessageBox.No)
553 if ret == QMessageBox.Yes:
554 self.__http.ignoreSslErrors()
555 else:
556 self.__downloadCancelled = True
557 self.__http.abort()
558
559 def getDownloadedPlugins(self):
560 """
561 Public method to get the list of recently downloaded plugin files.
562
563 @return list of plugin filenames (list of strings)
564 """
565 return self.__pluginsDownloaded
566
567 class PluginRepositoryDialog(QDialog):
568 """
569 Class for the dialog variant.
570 """
571 def __init__(self, parent = None):
572 """
573 Constructor
574
575 @param parent reference to the parent widget (QWidget)
576 """
577 QDialog.__init__(self, parent)
578 self.setSizeGripEnabled(True)
579
580 self.__layout = QVBoxLayout(self)
581 self.__layout.setMargin(0)
582 self.setLayout(self.__layout)
583
584 self.cw = PluginRepositoryWidget(self)
585 size = self.cw.size()
586 self.__layout.addWidget(self.cw)
587 self.resize(size)
588
589 self.connect(self.cw.buttonBox, SIGNAL("accepted()"), self.accept)
590 self.connect(self.cw.buttonBox, SIGNAL("rejected()"), self.reject)
591 self.connect(self.cw, SIGNAL("closeAndInstall"), self.__closeAndInstall)
592
593 def __closeAndInstall(self):
594 """
595 Private slot to handle the closeAndInstall signal.
596 """
597 self.done(QDialog.Accepted + 1)
598
599 def getDownloadedPlugins(self):
600 """
601 Public method to get the list of recently downloaded plugin files.
602
603 @return list of plugin filenames (list of strings)
604 """
605 return self.cw.getDownloadedPlugins()
606
607 class PluginRepositoryWindow(QMainWindow):
608 """
609 Main window class for the standalone dialog.
610 """
611 def __init__(self, parent = None):
612 """
613 Constructor
614
615 @param parent reference to the parent widget (QWidget)
616 """
617 QMainWindow.__init__(self, parent)
618 self.cw = PluginRepositoryWidget(self)
619 size = self.cw.size()
620 self.setCentralWidget(self.cw)
621 self.resize(size)
622
623 self.connect(self.cw.buttonBox, SIGNAL("accepted()"), self.close)
624 self.connect(self.cw.buttonBox, SIGNAL("rejected()"), self.close)
625 self.connect(self.cw, SIGNAL("closeAndInstall"), self.__startPluginInstall)
626
627 def __startPluginInstall(self):
628 """
629 Private slot to start the eric4 plugin installation dialog.
630 """
631 proc = QProcess()
632 applPath = os.path.join(getConfig("ericDir"), "eric4-plugininstall.py")
633
634 args = []
635 args.append(applPath)
636 args += self.cw.getDownloadedPlugins()
637
638 if not os.path.isfile(applPath) or not proc.startDetached(sys.executable, args):
639 QMessageBox.critical(self,
640 self.trUtf8('Process Generation Error'),
641 self.trUtf8(
642 '<p>Could not start the process.<br>'
643 'Ensure that it is available as <b>{0}</b>.</p>'
644 ).format(applPath),
645 self.trUtf8('OK'))
646
647 self.close()

eric ide

mercurial