|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2007 - 2022 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing a dialog for plugin deinstallation. |
|
8 """ |
|
9 |
|
10 import sys |
|
11 import os |
|
12 import importlib |
|
13 import shutil |
|
14 import glob |
|
15 |
|
16 from PyQt6.QtCore import pyqtSlot, pyqtSignal, Qt |
|
17 from PyQt6.QtWidgets import QWidget, QDialog, QVBoxLayout, QListWidgetItem |
|
18 |
|
19 from EricWidgets import EricMessageBox |
|
20 from EricWidgets.EricMainWindow import EricMainWindow |
|
21 from EricWidgets.EricApplication import ericApp |
|
22 |
|
23 from .Ui_PluginUninstallDialog import Ui_PluginUninstallDialog |
|
24 |
|
25 import Preferences |
|
26 import UI.PixmapCache |
|
27 |
|
28 |
|
29 class PluginUninstallWidget(QWidget, Ui_PluginUninstallDialog): |
|
30 """ |
|
31 Class implementing a dialog for plugin deinstallation. |
|
32 |
|
33 @signal accepted() emitted to indicate the removal of a plug-in |
|
34 """ |
|
35 accepted = pyqtSignal() |
|
36 |
|
37 def __init__(self, pluginManager, parent=None): |
|
38 """ |
|
39 Constructor |
|
40 |
|
41 @param pluginManager reference to the plugin manager object |
|
42 @param parent parent of this dialog (QWidget) |
|
43 """ |
|
44 super().__init__(parent) |
|
45 self.setupUi(self) |
|
46 |
|
47 if pluginManager is None: |
|
48 # started as external plugin deinstaller |
|
49 from .PluginManager import PluginManager |
|
50 self.__pluginManager = PluginManager(doLoadPlugins=False) |
|
51 self.__external = True |
|
52 else: |
|
53 self.__pluginManager = pluginManager |
|
54 self.__external = False |
|
55 |
|
56 self.pluginDirectoryCombo.addItem( |
|
57 self.tr("User plugins directory"), |
|
58 self.__pluginManager.getPluginDir("user")) |
|
59 |
|
60 globalDir = self.__pluginManager.getPluginDir("global") |
|
61 if globalDir is not None and os.access(globalDir, os.W_OK): |
|
62 self.pluginDirectoryCombo.addItem( |
|
63 self.tr("Global plugins directory"), |
|
64 globalDir) |
|
65 |
|
66 @pyqtSlot(int) |
|
67 def on_pluginDirectoryCombo_currentIndexChanged(self, index): |
|
68 """ |
|
69 Private slot to populate the plugin name combo upon a change of the |
|
70 plugin area. |
|
71 |
|
72 @param index index of the selected item (integer) |
|
73 """ |
|
74 pluginDirectory = self.pluginDirectoryCombo.itemData(index) |
|
75 pluginNames = sorted(self.__pluginManager.getPluginModules( |
|
76 pluginDirectory)) |
|
77 |
|
78 self.pluginsList.clear() |
|
79 for pluginName in pluginNames: |
|
80 fname = "{0}.py".format(os.path.join(pluginDirectory, pluginName)) |
|
81 itm = QListWidgetItem(pluginName) |
|
82 itm.setData(Qt.ItemDataRole.UserRole, fname) |
|
83 itm.setFlags(Qt.ItemFlag.ItemIsEnabled | |
|
84 Qt.ItemFlag.ItemIsUserCheckable) |
|
85 itm.setCheckState(Qt.CheckState.Unchecked) |
|
86 self.pluginsList.addItem(itm) |
|
87 |
|
88 @pyqtSlot() |
|
89 def on_buttonBox_accepted(self): |
|
90 """ |
|
91 Private slot to handle the accepted signal of the button box. |
|
92 """ |
|
93 if self.__uninstallPlugins(): |
|
94 self.accepted.emit() |
|
95 |
|
96 def __getCheckedPlugins(self): |
|
97 """ |
|
98 Private method to get the list of plugins to be uninstalled. |
|
99 |
|
100 @return list of tuples with the plugin name and plugin file name |
|
101 @rtype list of tuples of (str, str) |
|
102 """ |
|
103 plugins = [] |
|
104 for row in range(self.pluginsList.count()): |
|
105 itm = self.pluginsList.item(row) |
|
106 if itm.checkState() == Qt.CheckState.Checked: |
|
107 plugins.append((itm.text(), |
|
108 itm.data(Qt.ItemDataRole.UserRole))) |
|
109 return plugins |
|
110 |
|
111 def __uninstallPlugins(self): |
|
112 """ |
|
113 Private method to uninstall the selected plugins. |
|
114 |
|
115 @return flag indicating success |
|
116 @rtype bool |
|
117 """ |
|
118 checkedPlugins = self.__getCheckedPlugins() |
|
119 uninstallCount = 0 |
|
120 for pluginName, pluginFile in checkedPlugins: |
|
121 if self.__uninstallPlugin(pluginName, pluginFile): |
|
122 uninstallCount += 1 |
|
123 return uninstallCount == len(checkedPlugins) |
|
124 |
|
125 def __uninstallPlugin(self, pluginName, pluginFile): |
|
126 """ |
|
127 Private method to uninstall a given plugin. |
|
128 |
|
129 @param pluginName name of the plugin |
|
130 @type str |
|
131 @param pluginFile file name of the plugin |
|
132 @type str |
|
133 @return flag indicating success |
|
134 @rtype bool |
|
135 """ |
|
136 pluginDirectory = self.pluginDirectoryCombo.itemData( |
|
137 self.pluginDirectoryCombo.currentIndex()) |
|
138 |
|
139 if not self.__pluginManager.unloadPlugin(pluginName): |
|
140 EricMessageBox.critical( |
|
141 self, |
|
142 self.tr("Plugin Uninstallation"), |
|
143 self.tr( |
|
144 """<p>The plugin <b>{0}</b> could not be unloaded.""" |
|
145 """ Aborting...</p>""").format(pluginName)) |
|
146 return False |
|
147 |
|
148 if pluginDirectory not in sys.path: |
|
149 sys.path.insert(2, pluginDirectory) |
|
150 spec = importlib.util.spec_from_file_location(pluginName, pluginFile) |
|
151 module = importlib.util.module_from_spec(spec) |
|
152 spec.loader.exec_module(module) |
|
153 if not hasattr(module, "packageName"): |
|
154 EricMessageBox.critical( |
|
155 self, |
|
156 self.tr("Plugin Uninstallation"), |
|
157 self.tr( |
|
158 """<p>The plugin <b>{0}</b> has no 'packageName'""" |
|
159 """ attribute. Aborting...</p>""").format(pluginName)) |
|
160 return False |
|
161 |
|
162 package = getattr(module, "packageName", None) |
|
163 if package is None: |
|
164 package = "None" |
|
165 packageDir = "" |
|
166 else: |
|
167 packageDir = os.path.join(pluginDirectory, package) |
|
168 if ( |
|
169 hasattr(module, "prepareUninstall") and |
|
170 not self.keepConfigurationCheckBox.isChecked() |
|
171 ): |
|
172 module.prepareUninstall() |
|
173 internalPackages = [] |
|
174 if hasattr(module, "internalPackages"): |
|
175 # it is a comma separated string |
|
176 internalPackages = [p.strip() for p in |
|
177 module.internalPackages.split(",")] |
|
178 del module |
|
179 |
|
180 # clean sys.modules |
|
181 self.__pluginManager.removePluginFromSysModules( |
|
182 pluginName, package, internalPackages) |
|
183 |
|
184 try: |
|
185 if packageDir and os.path.exists(packageDir): |
|
186 shutil.rmtree(packageDir) |
|
187 |
|
188 fnameo = "{0}o".format(pluginFile) |
|
189 if os.path.exists(fnameo): |
|
190 os.remove(fnameo) |
|
191 |
|
192 fnamec = "{0}c".format(pluginFile) |
|
193 if os.path.exists(fnamec): |
|
194 os.remove(fnamec) |
|
195 |
|
196 pluginDirCache = os.path.join( |
|
197 os.path.dirname(pluginFile), "__pycache__") |
|
198 if os.path.exists(pluginDirCache): |
|
199 pluginFileName = os.path.splitext( |
|
200 os.path.basename(pluginFile))[0] |
|
201 for fnameo in glob.glob(os.path.join( |
|
202 pluginDirCache, "{0}*.pyo".format(pluginFileName))): |
|
203 os.remove(fnameo) |
|
204 for fnamec in glob.glob(os.path.join( |
|
205 pluginDirCache, "{0}*.pyc".format(pluginFileName))): |
|
206 os.remove(fnamec) |
|
207 |
|
208 os.remove(pluginFile) |
|
209 except OSError as err: |
|
210 EricMessageBox.critical( |
|
211 self, |
|
212 self.tr("Plugin Uninstallation"), |
|
213 self.tr( |
|
214 """<p>The plugin package <b>{0}</b> could not be""" |
|
215 """ removed. Aborting...</p>""" |
|
216 """<p>Reason: {1}</p>""").format(packageDir, str(err))) |
|
217 return False |
|
218 |
|
219 if not self.__external: |
|
220 ui = ericApp().getObject("UserInterface") |
|
221 ui.showNotification( |
|
222 UI.PixmapCache.getPixmap("plugin48"), |
|
223 self.tr("Plugin Uninstallation"), |
|
224 self.tr( |
|
225 """<p>The plugin <b>{0}</b> was uninstalled""" |
|
226 """ successfully from {1}.</p>""") |
|
227 .format(pluginName, pluginDirectory)) |
|
228 return True |
|
229 |
|
230 EricMessageBox.information( |
|
231 self, |
|
232 self.tr("Plugin Uninstallation"), |
|
233 self.tr( |
|
234 """<p>The plugin <b>{0}</b> was uninstalled successfully""" |
|
235 """ from {1}.</p>""") |
|
236 .format(pluginName, pluginDirectory)) |
|
237 return True |
|
238 |
|
239 |
|
240 class PluginUninstallDialog(QDialog): |
|
241 """ |
|
242 Class for the dialog variant. |
|
243 """ |
|
244 def __init__(self, pluginManager, parent=None): |
|
245 """ |
|
246 Constructor |
|
247 |
|
248 @param pluginManager reference to the plugin manager object |
|
249 @param parent reference to the parent widget (QWidget) |
|
250 """ |
|
251 super().__init__(parent) |
|
252 self.setSizeGripEnabled(True) |
|
253 |
|
254 self.__layout = QVBoxLayout(self) |
|
255 self.__layout.setContentsMargins(0, 0, 0, 0) |
|
256 self.setLayout(self.__layout) |
|
257 |
|
258 self.cw = PluginUninstallWidget(pluginManager, self) |
|
259 size = self.cw.size() |
|
260 self.__layout.addWidget(self.cw) |
|
261 self.resize(size) |
|
262 self.setWindowTitle(self.cw.windowTitle()) |
|
263 |
|
264 self.cw.buttonBox.accepted.connect(self.accept) |
|
265 self.cw.buttonBox.rejected.connect(self.reject) |
|
266 |
|
267 |
|
268 class PluginUninstallWindow(EricMainWindow): |
|
269 """ |
|
270 Main window class for the standalone dialog. |
|
271 """ |
|
272 def __init__(self, parent=None): |
|
273 """ |
|
274 Constructor |
|
275 |
|
276 @param parent reference to the parent widget (QWidget) |
|
277 """ |
|
278 super().__init__(parent) |
|
279 self.cw = PluginUninstallWidget(None, self) |
|
280 size = self.cw.size() |
|
281 self.setCentralWidget(self.cw) |
|
282 self.resize(size) |
|
283 self.setWindowTitle(self.cw.windowTitle()) |
|
284 |
|
285 self.setStyle(Preferences.getUI("Style"), |
|
286 Preferences.getUI("StyleSheet")) |
|
287 |
|
288 self.cw.buttonBox.accepted.connect(self.close) |
|
289 self.cw.buttonBox.rejected.connect(self.close) |