|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2007 - 2021 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing the Plugin installation dialog. |
|
8 """ |
|
9 |
|
10 import os |
|
11 import sys |
|
12 import shutil |
|
13 import zipfile |
|
14 import compileall |
|
15 import glob |
|
16 import contextlib |
|
17 import urllib.parse |
|
18 |
|
19 from PyQt5.QtCore import pyqtSlot, Qt, QDir, QFileInfo |
|
20 from PyQt5.QtWidgets import ( |
|
21 QWidget, QDialogButtonBox, QAbstractButton, QApplication, QDialog, |
|
22 QVBoxLayout |
|
23 ) |
|
24 |
|
25 from E5Gui import E5FileDialog |
|
26 from E5Gui.E5MainWindow import E5MainWindow |
|
27 |
|
28 from .Ui_PluginInstallDialog import Ui_PluginInstallDialog |
|
29 |
|
30 import Utilities |
|
31 import Preferences |
|
32 |
|
33 from Utilities.uic import compileUiFiles |
|
34 |
|
35 |
|
36 class PluginInstallWidget(QWidget, Ui_PluginInstallDialog): |
|
37 """ |
|
38 Class implementing the Plugin installation dialog. |
|
39 """ |
|
40 def __init__(self, pluginManager, pluginFileNames, parent=None): |
|
41 """ |
|
42 Constructor |
|
43 |
|
44 @param pluginManager reference to the plugin manager object |
|
45 @param pluginFileNames list of plugin files suggested for |
|
46 installation (list of strings) |
|
47 @param parent parent of this dialog (QWidget) |
|
48 """ |
|
49 super().__init__(parent) |
|
50 self.setupUi(self) |
|
51 |
|
52 if pluginManager is None: |
|
53 # started as external plugin installer |
|
54 from .PluginManager import PluginManager |
|
55 self.__pluginManager = PluginManager(doLoadPlugins=False) |
|
56 self.__external = True |
|
57 else: |
|
58 self.__pluginManager = pluginManager |
|
59 self.__external = False |
|
60 |
|
61 self.__backButton = self.buttonBox.addButton( |
|
62 self.tr("< Back"), QDialogButtonBox.ButtonRole.ActionRole) |
|
63 self.__nextButton = self.buttonBox.addButton( |
|
64 self.tr("Next >"), QDialogButtonBox.ButtonRole.ActionRole) |
|
65 self.__finishButton = self.buttonBox.addButton( |
|
66 self.tr("Install"), QDialogButtonBox.ButtonRole.ActionRole) |
|
67 |
|
68 self.__closeButton = self.buttonBox.button( |
|
69 QDialogButtonBox.StandardButton.Close) |
|
70 self.__cancelButton = self.buttonBox.button( |
|
71 QDialogButtonBox.StandardButton.Cancel) |
|
72 |
|
73 userDir = self.__pluginManager.getPluginDir("user") |
|
74 if userDir is not None: |
|
75 self.destinationCombo.addItem( |
|
76 self.tr("User plugins directory"), |
|
77 userDir) |
|
78 |
|
79 globalDir = self.__pluginManager.getPluginDir("global") |
|
80 if globalDir is not None and os.access(globalDir, os.W_OK): |
|
81 self.destinationCombo.addItem( |
|
82 self.tr("Global plugins directory"), |
|
83 globalDir) |
|
84 |
|
85 self.__installedDirs = [] |
|
86 self.__installedFiles = [] |
|
87 |
|
88 self.__restartNeeded = False |
|
89 |
|
90 downloadDir = QDir(Preferences.getPluginManager("DownloadPath")) |
|
91 for pluginFileName in pluginFileNames: |
|
92 fi = QFileInfo(pluginFileName) |
|
93 if fi.isRelative(): |
|
94 pluginFileName = QFileInfo( |
|
95 downloadDir, fi.fileName()).absoluteFilePath() |
|
96 self.archivesList.addItem(pluginFileName) |
|
97 self.archivesList.sortItems() |
|
98 |
|
99 self.__currentIndex = 0 |
|
100 self.__selectPage() |
|
101 |
|
102 def restartNeeded(self): |
|
103 """ |
|
104 Public method to check, if a restart of the IDE is required. |
|
105 |
|
106 @return flag indicating a restart is required (boolean) |
|
107 """ |
|
108 return self.__restartNeeded |
|
109 |
|
110 def __createArchivesList(self): |
|
111 """ |
|
112 Private method to create a list of plugin archive names. |
|
113 |
|
114 @return list of plugin archive names (list of strings) |
|
115 """ |
|
116 archivesList = [] |
|
117 for row in range(self.archivesList.count()): |
|
118 archivesList.append(self.archivesList.item(row).text()) |
|
119 return archivesList |
|
120 |
|
121 def __selectPage(self): |
|
122 """ |
|
123 Private method to show the right wizard page. |
|
124 """ |
|
125 self.wizard.setCurrentIndex(self.__currentIndex) |
|
126 if self.__currentIndex == 0: |
|
127 self.__backButton.setEnabled(False) |
|
128 self.__nextButton.setEnabled(self.archivesList.count() > 0) |
|
129 self.__finishButton.setEnabled(False) |
|
130 self.__closeButton.hide() |
|
131 self.__cancelButton.show() |
|
132 elif self.__currentIndex == 1: |
|
133 self.__backButton.setEnabled(True) |
|
134 self.__nextButton.setEnabled(self.destinationCombo.count() > 0) |
|
135 self.__finishButton.setEnabled(False) |
|
136 self.__closeButton.hide() |
|
137 self.__cancelButton.show() |
|
138 else: |
|
139 self.__backButton.setEnabled(True) |
|
140 self.__nextButton.setEnabled(False) |
|
141 self.__finishButton.setEnabled(True) |
|
142 self.__closeButton.hide() |
|
143 self.__cancelButton.show() |
|
144 |
|
145 msg = self.tr( |
|
146 "Plugin ZIP-Archives:\n{0}\n\nDestination:\n{1} ({2})" |
|
147 ).format( |
|
148 "\n".join(self.__createArchivesList()), |
|
149 self.destinationCombo.currentText(), |
|
150 self.destinationCombo.itemData( |
|
151 self.destinationCombo.currentIndex() |
|
152 ) |
|
153 ) |
|
154 self.summaryEdit.setPlainText(msg) |
|
155 |
|
156 @pyqtSlot() |
|
157 def on_addArchivesButton_clicked(self): |
|
158 """ |
|
159 Private slot to select plugin ZIP-archives via a file selection dialog. |
|
160 """ |
|
161 dn = Preferences.getPluginManager("DownloadPath") |
|
162 archives = E5FileDialog.getOpenFileNames( |
|
163 self, |
|
164 self.tr("Select plugin ZIP-archives"), |
|
165 dn, |
|
166 self.tr("Plugin archive (*.zip)")) |
|
167 |
|
168 if archives: |
|
169 matchflags = Qt.MatchFlag.MatchFixedString |
|
170 if not Utilities.isWindowsPlatform(): |
|
171 matchflags |= Qt.MatchFlag.MatchCaseSensitive |
|
172 for archive in archives: |
|
173 if len(self.archivesList.findItems(archive, matchflags)) == 0: |
|
174 # entry not in list already |
|
175 self.archivesList.addItem(archive) |
|
176 self.archivesList.sortItems() |
|
177 |
|
178 self.__nextButton.setEnabled(self.archivesList.count() > 0) |
|
179 |
|
180 @pyqtSlot() |
|
181 def on_archivesList_itemSelectionChanged(self): |
|
182 """ |
|
183 Private slot called, when the selection of the archives list changes. |
|
184 """ |
|
185 self.removeArchivesButton.setEnabled( |
|
186 len(self.archivesList.selectedItems()) > 0) |
|
187 |
|
188 @pyqtSlot() |
|
189 def on_removeArchivesButton_clicked(self): |
|
190 """ |
|
191 Private slot to remove archives from the list. |
|
192 """ |
|
193 for archiveItem in self.archivesList.selectedItems(): |
|
194 itm = self.archivesList.takeItem( |
|
195 self.archivesList.row(archiveItem)) |
|
196 del itm |
|
197 |
|
198 self.__nextButton.setEnabled(self.archivesList.count() > 0) |
|
199 |
|
200 @pyqtSlot(QAbstractButton) |
|
201 def on_buttonBox_clicked(self, button): |
|
202 """ |
|
203 Private slot to handle the click of a button of the button box. |
|
204 |
|
205 @param button reference to the button pressed (QAbstractButton) |
|
206 """ |
|
207 if button == self.__backButton: |
|
208 self.__currentIndex -= 1 |
|
209 self.__selectPage() |
|
210 elif button == self.__nextButton: |
|
211 self.__currentIndex += 1 |
|
212 self.__selectPage() |
|
213 elif button == self.__finishButton: |
|
214 self.__finishButton.setEnabled(False) |
|
215 self.__installPlugins() |
|
216 if not Preferences.getPluginManager("ActivateExternal"): |
|
217 Preferences.setPluginManager("ActivateExternal", True) |
|
218 self.__restartNeeded = True |
|
219 self.__closeButton.show() |
|
220 self.__cancelButton.hide() |
|
221 |
|
222 def __installPlugins(self): |
|
223 """ |
|
224 Private method to install the selected plugin archives. |
|
225 |
|
226 @return flag indicating success (boolean) |
|
227 """ |
|
228 res = True |
|
229 self.summaryEdit.clear() |
|
230 for archive in self.__createArchivesList(): |
|
231 self.summaryEdit.append( |
|
232 self.tr("Installing {0} ...").format(archive)) |
|
233 ok, msg, restart = self.__installPlugin(archive) |
|
234 res = res and ok |
|
235 if ok: |
|
236 self.summaryEdit.append(self.tr(" ok")) |
|
237 else: |
|
238 self.summaryEdit.append(msg) |
|
239 if restart: |
|
240 self.__restartNeeded = True |
|
241 self.summaryEdit.append("\n") |
|
242 if res: |
|
243 self.summaryEdit.append(self.tr( |
|
244 """The plugins were installed successfully.""")) |
|
245 else: |
|
246 self.summaryEdit.append(self.tr( |
|
247 """Some plugins could not be installed.""")) |
|
248 |
|
249 return res |
|
250 |
|
251 def __installPlugin(self, archiveFilename): |
|
252 """ |
|
253 Private slot to install the selected plugin. |
|
254 |
|
255 @param archiveFilename name of the plugin archive |
|
256 file (string) |
|
257 @return flag indicating success (boolean), error message |
|
258 upon failure (string) and flag indicating a restart |
|
259 of the IDE is required (boolean) |
|
260 """ |
|
261 installedPluginName = "" |
|
262 |
|
263 archive = archiveFilename |
|
264 destination = self.destinationCombo.itemData( |
|
265 self.destinationCombo.currentIndex()) |
|
266 |
|
267 # check if archive is a local url |
|
268 url = urllib.parse.urlparse(archive) |
|
269 if url[0].lower() == 'file': |
|
270 archive = url[2] |
|
271 |
|
272 # check, if the archive exists |
|
273 if not os.path.exists(archive): |
|
274 return ( |
|
275 False, |
|
276 self.tr( |
|
277 """<p>The archive file <b>{0}</b> does not exist. """ |
|
278 """Aborting...</p>""").format(archive), |
|
279 False |
|
280 ) |
|
281 |
|
282 # check, if the archive is a valid zip file |
|
283 if not zipfile.is_zipfile(archive): |
|
284 return ( |
|
285 False, |
|
286 self.tr( |
|
287 """<p>The file <b>{0}</b> is not a valid plugin """ |
|
288 """ZIP-archive. Aborting...</p>""").format(archive), |
|
289 False |
|
290 ) |
|
291 |
|
292 # check, if the destination is writeable |
|
293 if not os.access(destination, os.W_OK): |
|
294 return ( |
|
295 False, |
|
296 self.tr( |
|
297 """<p>The destination directory <b>{0}</b> is not """ |
|
298 """writeable. Aborting...</p>""").format(destination), |
|
299 False |
|
300 ) |
|
301 |
|
302 zipFile = zipfile.ZipFile(archive, "r") |
|
303 |
|
304 # check, if the archive contains a valid plugin |
|
305 pluginFound = False |
|
306 pluginFileName = "" |
|
307 for name in zipFile.namelist(): |
|
308 if self.__pluginManager.isValidPluginName(name): |
|
309 installedPluginName = name[:-3] |
|
310 pluginFound = True |
|
311 pluginFileName = name |
|
312 break |
|
313 |
|
314 if not pluginFound: |
|
315 return ( |
|
316 False, |
|
317 self.tr( |
|
318 """<p>The file <b>{0}</b> is not a valid plugin """ |
|
319 """ZIP-archive. Aborting...</p>""").format(archive), |
|
320 False |
|
321 ) |
|
322 |
|
323 # parse the plugin module's plugin header |
|
324 pluginSource = Utilities.decode(zipFile.read(pluginFileName))[0] |
|
325 packageName = "" |
|
326 internalPackages = [] |
|
327 needsRestart = False |
|
328 pyqtApi = 0 |
|
329 doCompile = True |
|
330 for line in pluginSource.splitlines(): |
|
331 if line.startswith("packageName"): |
|
332 tokens = line.split("=") |
|
333 if ( |
|
334 tokens[0].strip() == "packageName" and |
|
335 tokens[1].strip()[1:-1] != "__core__" |
|
336 ): |
|
337 if tokens[1].strip()[0] in ['"', "'"]: |
|
338 packageName = tokens[1].strip()[1:-1] |
|
339 else: |
|
340 if tokens[1].strip() == "None": |
|
341 packageName = "None" |
|
342 elif line.startswith("internalPackages"): |
|
343 tokens = line.split("=") |
|
344 token = tokens[1].strip()[1:-1] |
|
345 # it is a comma separated string |
|
346 internalPackages = [p.strip() for p in token.split(",")] |
|
347 elif line.startswith("needsRestart"): |
|
348 tokens = line.split("=") |
|
349 needsRestart = tokens[1].strip() == "True" |
|
350 elif line.startswith("pyqtApi"): |
|
351 tokens = line.split("=") |
|
352 with contextlib.suppress(ValueError): |
|
353 pyqtApi = int(tokens[1].strip()) |
|
354 elif line.startswith("doNotCompile"): |
|
355 tokens = line.split("=") |
|
356 if tokens[1].strip() == "True": |
|
357 doCompile = False |
|
358 elif line.startswith("# End-Of-Header"): |
|
359 break |
|
360 |
|
361 if not packageName: |
|
362 return ( |
|
363 False, |
|
364 self.tr( |
|
365 """<p>The plugin module <b>{0}</b> does not contain """ |
|
366 """a 'packageName' attribute. Aborting...</p>""" |
|
367 ).format(pluginFileName), |
|
368 False |
|
369 ) |
|
370 |
|
371 if pyqtApi < 2: |
|
372 return ( |
|
373 False, |
|
374 self.tr( |
|
375 """<p>The plugin module <b>{0}</b> does not conform""" |
|
376 """ with the PyQt v2 API. Aborting...</p>""" |
|
377 ).format(pluginFileName), |
|
378 False |
|
379 ) |
|
380 |
|
381 # check, if it is a plugin, that collides with others |
|
382 if ( |
|
383 not os.path.exists(os.path.join(destination, pluginFileName)) and |
|
384 packageName != "None" and |
|
385 os.path.exists(os.path.join(destination, packageName)) |
|
386 ): |
|
387 return ( |
|
388 False, |
|
389 self.tr("""<p>The plugin package <b>{0}</b> exists. """ |
|
390 """Aborting...</p>""").format( |
|
391 os.path.join(destination, packageName)), |
|
392 False |
|
393 ) |
|
394 |
|
395 if ( |
|
396 os.path.exists(os.path.join(destination, pluginFileName)) and |
|
397 packageName != "None" and |
|
398 not os.path.exists(os.path.join(destination, packageName)) |
|
399 ): |
|
400 return ( |
|
401 False, |
|
402 self.tr("""<p>The plugin module <b>{0}</b> exists. """ |
|
403 """Aborting...</p>""").format( |
|
404 os.path.join(destination, pluginFileName)), |
|
405 False |
|
406 ) |
|
407 |
|
408 activatePlugin = False |
|
409 if not self.__external: |
|
410 activatePlugin = ( |
|
411 not self.__pluginManager.isPluginLoaded( |
|
412 installedPluginName) or |
|
413 (self.__pluginManager.isPluginLoaded(installedPluginName) and |
|
414 self.__pluginManager.isPluginActive(installedPluginName)) |
|
415 ) |
|
416 # try to unload a plugin with the same name |
|
417 self.__pluginManager.unloadPlugin(installedPluginName) |
|
418 |
|
419 # uninstall existing plug-in first to get clean conditions |
|
420 if ( |
|
421 packageName != "None" and |
|
422 not os.path.exists( |
|
423 os.path.join(destination, packageName, "__init__.py")) |
|
424 ): |
|
425 # package directory contains just data, don't delete it |
|
426 self.__uninstallPackage(destination, pluginFileName, "") |
|
427 else: |
|
428 self.__uninstallPackage(destination, pluginFileName, packageName) |
|
429 |
|
430 # clean sys.modules |
|
431 reload_ = self.__pluginManager.removePluginFromSysModules( |
|
432 installedPluginName, packageName, internalPackages) |
|
433 |
|
434 # now do the installation |
|
435 self.__installedDirs = [] |
|
436 self.__installedFiles = [] |
|
437 try: |
|
438 if packageName != "None": |
|
439 namelist = sorted(zipFile.namelist()) |
|
440 tot = len(namelist) |
|
441 self.progress.setMaximum(tot) |
|
442 QApplication.processEvents() |
|
443 for prog, name in enumerate(namelist): |
|
444 self.progress.setValue(prog) |
|
445 QApplication.processEvents() |
|
446 if ( |
|
447 name == pluginFileName or |
|
448 name.startswith("{0}/".format(packageName)) or |
|
449 name.startswith("{0}\\".format(packageName)) |
|
450 ): |
|
451 outname = name.replace("/", os.sep) |
|
452 outname = os.path.join(destination, outname) |
|
453 if outname.endswith("/") or outname.endswith("\\"): |
|
454 # it is a directory entry |
|
455 outname = outname[:-1] |
|
456 if not os.path.exists(outname): |
|
457 self.__makedirs(outname) |
|
458 else: |
|
459 # it is a file |
|
460 d = os.path.dirname(outname) |
|
461 if not os.path.exists(d): |
|
462 self.__makedirs(d) |
|
463 with open(outname, "wb") as f: |
|
464 f.write(zipFile.read(name)) |
|
465 self.__installedFiles.append(outname) |
|
466 self.progress.setValue(tot) |
|
467 # now compile user interface files |
|
468 compileUiFiles(os.path.join(destination, packageName), True) |
|
469 else: |
|
470 outname = os.path.join(destination, pluginFileName) |
|
471 with open(outname, "w", encoding="utf-8") as f: |
|
472 f.write(pluginSource) |
|
473 self.__installedFiles.append(outname) |
|
474 except OSError as why: |
|
475 self.__rollback() |
|
476 return ( |
|
477 False, |
|
478 self.tr("Error installing plugin. Reason: {0}") |
|
479 .format(str(why)), |
|
480 False |
|
481 ) |
|
482 except Exception: |
|
483 sys.stderr.write("Unspecific exception installing plugin.\n") |
|
484 self.__rollback() |
|
485 return ( |
|
486 False, |
|
487 self.tr("Unspecific exception installing plugin."), |
|
488 False |
|
489 ) |
|
490 |
|
491 # now compile the plugins |
|
492 if doCompile: |
|
493 dirName = os.path.join(destination, packageName) |
|
494 files = os.path.join(destination, pluginFileName) |
|
495 os.path.join_unicode = False |
|
496 compileall.compile_dir(dirName, quiet=True) |
|
497 compileall.compile_file(files, quiet=True) |
|
498 os.path.join_unicode = True |
|
499 |
|
500 # now load and activate the plugin |
|
501 self.__pluginManager.loadPlugin( |
|
502 installedPluginName, destination, reload_=reload_, install=True) |
|
503 if activatePlugin and not self.__external: |
|
504 self.__pluginManager.activatePlugin(installedPluginName) |
|
505 |
|
506 return True, "", needsRestart |
|
507 |
|
508 def __rollback(self): |
|
509 """ |
|
510 Private method to rollback a failed installation. |
|
511 """ |
|
512 for fname in self.__installedFiles: |
|
513 if os.path.exists(fname): |
|
514 os.remove(fname) |
|
515 for dname in self.__installedDirs: |
|
516 if os.path.exists(dname): |
|
517 shutil.rmtree(dname) |
|
518 |
|
519 def __makedirs(self, name, mode=0o777): |
|
520 """ |
|
521 Private method to create a directory and all intermediate ones. |
|
522 |
|
523 This is an extended version of the Python one in order to |
|
524 record the created directories. |
|
525 |
|
526 @param name name of the directory to create (string) |
|
527 @param mode permission to set for the new directory (integer) |
|
528 """ |
|
529 head, tail = os.path.split(name) |
|
530 if not tail: |
|
531 head, tail = os.path.split(head) |
|
532 if head and tail and not os.path.exists(head): |
|
533 self.__makedirs(head, mode) |
|
534 if tail == os.curdir: |
|
535 # xxx/newdir/. exists if xxx/newdir exists |
|
536 return |
|
537 os.mkdir(name, mode) |
|
538 self.__installedDirs.append(name) |
|
539 |
|
540 def __uninstallPackage(self, destination, pluginFileName, packageName): |
|
541 """ |
|
542 Private method to uninstall an already installed plugin to prepare |
|
543 the update. |
|
544 |
|
545 @param destination name of the plugin directory (string) |
|
546 @param pluginFileName name of the plugin file (string) |
|
547 @param packageName name of the plugin package (string) |
|
548 """ |
|
549 packageDir = ( |
|
550 None |
|
551 if packageName in ("", "None") else |
|
552 os.path.join(destination, packageName) |
|
553 ) |
|
554 pluginFile = os.path.join(destination, pluginFileName) |
|
555 |
|
556 with contextlib.suppress(OSError, os.error): |
|
557 if packageDir and os.path.exists(packageDir): |
|
558 shutil.rmtree(packageDir) |
|
559 |
|
560 fnameo = "{0}o".format(pluginFile) |
|
561 if os.path.exists(fnameo): |
|
562 os.remove(fnameo) |
|
563 |
|
564 fnamec = "{0}c".format(pluginFile) |
|
565 if os.path.exists(fnamec): |
|
566 os.remove(fnamec) |
|
567 |
|
568 pluginDirCache = os.path.join( |
|
569 os.path.dirname(pluginFile), "__pycache__") |
|
570 if os.path.exists(pluginDirCache): |
|
571 pluginFileName = os.path.splitext( |
|
572 os.path.basename(pluginFile))[0] |
|
573 for fnameo in glob.glob( |
|
574 os.path.join(pluginDirCache, |
|
575 "{0}*.pyo".format(pluginFileName))): |
|
576 os.remove(fnameo) |
|
577 for fnamec in glob.glob( |
|
578 os.path.join(pluginDirCache, |
|
579 "{0}*.pyc".format(pluginFileName))): |
|
580 os.remove(fnamec) |
|
581 |
|
582 os.remove(pluginFile) |
|
583 |
|
584 |
|
585 class PluginInstallDialog(QDialog): |
|
586 """ |
|
587 Class for the dialog variant. |
|
588 """ |
|
589 def __init__(self, pluginManager, pluginFileNames, parent=None): |
|
590 """ |
|
591 Constructor |
|
592 |
|
593 @param pluginManager reference to the plugin manager object |
|
594 @param pluginFileNames list of plugin files suggested for |
|
595 installation (list of strings) |
|
596 @param parent reference to the parent widget (QWidget) |
|
597 """ |
|
598 super().__init__(parent) |
|
599 self.setSizeGripEnabled(True) |
|
600 |
|
601 self.__layout = QVBoxLayout(self) |
|
602 self.__layout.setContentsMargins(0, 0, 0, 0) |
|
603 self.setLayout(self.__layout) |
|
604 |
|
605 self.cw = PluginInstallWidget(pluginManager, pluginFileNames, self) |
|
606 size = self.cw.size() |
|
607 self.__layout.addWidget(self.cw) |
|
608 self.resize(size) |
|
609 self.setWindowTitle(self.cw.windowTitle()) |
|
610 |
|
611 self.cw.buttonBox.accepted.connect(self.accept) |
|
612 self.cw.buttonBox.rejected.connect(self.reject) |
|
613 |
|
614 def restartNeeded(self): |
|
615 """ |
|
616 Public method to check, if a restart of the IDE is required. |
|
617 |
|
618 @return flag indicating a restart is required (boolean) |
|
619 """ |
|
620 return self.cw.restartNeeded() |
|
621 |
|
622 |
|
623 class PluginInstallWindow(E5MainWindow): |
|
624 """ |
|
625 Main window class for the standalone dialog. |
|
626 """ |
|
627 def __init__(self, pluginFileNames, parent=None): |
|
628 """ |
|
629 Constructor |
|
630 |
|
631 @param pluginFileNames list of plugin files suggested for |
|
632 installation (list of strings) |
|
633 @param parent reference to the parent widget (QWidget) |
|
634 """ |
|
635 super().__init__(parent) |
|
636 self.cw = PluginInstallWidget(None, pluginFileNames, self) |
|
637 size = self.cw.size() |
|
638 self.setCentralWidget(self.cw) |
|
639 self.resize(size) |
|
640 self.setWindowTitle(self.cw.windowTitle()) |
|
641 |
|
642 self.setStyle(Preferences.getUI("Style"), |
|
643 Preferences.getUI("StyleSheet")) |
|
644 |
|
645 self.cw.buttonBox.accepted.connect(self.close) |
|
646 self.cw.buttonBox.rejected.connect(self.close) |