1# -*- coding: utf-8 -*- 2 3# Copyright (c) 2007 - 2021 Detlev Offenbach <detlev@die-offenbachs.de> 4# 5 6""" 7Module implementing a dialog for plugin deinstallation. 8""" 9 10import sys 11import os 12import importlib 13import shutil 14import glob 15 16from PyQt5.QtCore import pyqtSlot, pyqtSignal 17from PyQt5.QtWidgets import QWidget, QDialog, QDialogButtonBox, QVBoxLayout 18 19from E5Gui import E5MessageBox 20from E5Gui.E5MainWindow import E5MainWindow 21from E5Gui.E5Application import e5App 22 23from .Ui_PluginUninstallDialog import Ui_PluginUninstallDialog 24 25import Preferences 26import UI.PixmapCache 27 28 29class 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 msh = self.minimumSizeHint() 67 self.resize(max(self.width(), msh.width()), msh.height()) 68 69 @pyqtSlot(int) 70 def on_pluginDirectoryCombo_currentIndexChanged(self, index): 71 """ 72 Private slot to populate the plugin name combo upon a change of the 73 plugin area. 74 75 @param index index of the selected item (integer) 76 """ 77 pluginDirectory = self.pluginDirectoryCombo.itemData(index) 78 pluginNames = sorted(self.__pluginManager.getPluginModules( 79 pluginDirectory)) 80 self.pluginNameCombo.clear() 81 for pluginName in pluginNames: 82 fname = "{0}.py".format(os.path.join(pluginDirectory, pluginName)) 83 self.pluginNameCombo.addItem(pluginName, fname) 84 self.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setEnabled( 85 self.pluginNameCombo.currentText() != "") 86 87 @pyqtSlot() 88 def on_buttonBox_accepted(self): 89 """ 90 Private slot to handle the accepted signal of the button box. 91 """ 92 if self.__uninstallPlugin(): 93 self.accepted.emit() 94 95 def __uninstallPlugin(self): 96 """ 97 Private slot to uninstall the selected plugin. 98 99 @return flag indicating success (boolean) 100 """ 101 pluginDirectory = self.pluginDirectoryCombo.itemData( 102 self.pluginDirectoryCombo.currentIndex()) 103 pluginName = self.pluginNameCombo.currentText() 104 pluginFile = self.pluginNameCombo.itemData( 105 self.pluginNameCombo.currentIndex()) 106 107 if not self.__pluginManager.unloadPlugin(pluginName): 108 E5MessageBox.critical( 109 self, 110 self.tr("Plugin Uninstallation"), 111 self.tr( 112 """<p>The plugin <b>{0}</b> could not be unloaded.""" 113 """ Aborting...</p>""").format(pluginName)) 114 return False 115 116 if pluginDirectory not in sys.path: 117 sys.path.insert(2, pluginDirectory) 118 spec = importlib.util.spec_from_file_location(pluginName, pluginFile) 119 module = importlib.util.module_from_spec(spec) 120 spec.loader.exec_module(module) 121 if not hasattr(module, "packageName"): 122 E5MessageBox.critical( 123 self, 124 self.tr("Plugin Uninstallation"), 125 self.tr( 126 """<p>The plugin <b>{0}</b> has no 'packageName'""" 127 """ attribute. Aborting...</p>""").format(pluginName)) 128 return False 129 130 package = getattr(module, "packageName", None) 131 if package is None: 132 package = "None" 133 packageDir = "" 134 else: 135 packageDir = os.path.join(pluginDirectory, package) 136 if ( 137 hasattr(module, "prepareUninstall") and 138 not self.keepConfigurationCheckBox.isChecked() 139 ): 140 module.prepareUninstall() 141 internalPackages = [] 142 if hasattr(module, "internalPackages"): 143 # it is a comma separated string 144 internalPackages = [p.strip() for p in 145 module.internalPackages.split(",")] 146 del module 147 148 # clean sys.modules 149 self.__pluginManager.removePluginFromSysModules( 150 pluginName, package, internalPackages) 151 152 try: 153 if packageDir and os.path.exists(packageDir): 154 shutil.rmtree(packageDir) 155 156 fnameo = "{0}o".format(pluginFile) 157 if os.path.exists(fnameo): 158 os.remove(fnameo) 159 160 fnamec = "{0}c".format(pluginFile) 161 if os.path.exists(fnamec): 162 os.remove(fnamec) 163 164 pluginDirCache = os.path.join( 165 os.path.dirname(pluginFile), "__pycache__") 166 if os.path.exists(pluginDirCache): 167 pluginFileName = os.path.splitext( 168 os.path.basename(pluginFile))[0] 169 for fnameo in glob.glob(os.path.join( 170 pluginDirCache, "{0}*.pyo".format(pluginFileName))): 171 os.remove(fnameo) 172 for fnamec in glob.glob(os.path.join( 173 pluginDirCache, "{0}*.pyc".format(pluginFileName))): 174 os.remove(fnamec) 175 176 os.remove(pluginFile) 177 except OSError as err: 178 E5MessageBox.critical( 179 self, 180 self.tr("Plugin Uninstallation"), 181 self.tr( 182 """<p>The plugin package <b>{0}</b> could not be""" 183 """ removed. Aborting...</p>""" 184 """<p>Reason: {1}</p>""").format(packageDir, str(err))) 185 return False 186 187 if not self.__external: 188 ui = e5App().getObject("UserInterface") 189 ui.showNotification( 190 UI.PixmapCache.getPixmap("plugin48"), 191 self.tr("Plugin Uninstallation"), 192 self.tr( 193 """<p>The plugin <b>{0}</b> was uninstalled""" 194 """ successfully from {1}.</p>""") 195 .format(pluginName, pluginDirectory)) 196 return True 197 198 E5MessageBox.information( 199 self, 200 self.tr("Plugin Uninstallation"), 201 self.tr( 202 """<p>The plugin <b>{0}</b> was uninstalled successfully""" 203 """ from {1}.</p>""") 204 .format(pluginName, pluginDirectory)) 205 return True 206 207 208class PluginUninstallDialog(QDialog): 209 """ 210 Class for the dialog variant. 211 """ 212 def __init__(self, pluginManager, parent=None): 213 """ 214 Constructor 215 216 @param pluginManager reference to the plugin manager object 217 @param parent reference to the parent widget (QWidget) 218 """ 219 super().__init__(parent) 220 self.setSizeGripEnabled(True) 221 222 self.__layout = QVBoxLayout(self) 223 self.__layout.setContentsMargins(0, 0, 0, 0) 224 self.setLayout(self.__layout) 225 226 self.cw = PluginUninstallWidget(pluginManager, self) 227 size = self.cw.size() 228 self.__layout.addWidget(self.cw) 229 self.resize(size) 230 self.setWindowTitle(self.cw.windowTitle()) 231 232 self.cw.buttonBox.accepted.connect(self.accept) 233 self.cw.buttonBox.rejected.connect(self.reject) 234 235 236class PluginUninstallWindow(E5MainWindow): 237 """ 238 Main window class for the standalone dialog. 239 """ 240 def __init__(self, parent=None): 241 """ 242 Constructor 243 244 @param parent reference to the parent widget (QWidget) 245 """ 246 super().__init__(parent) 247 self.cw = PluginUninstallWidget(None, self) 248 size = self.cw.size() 249 self.setCentralWidget(self.cw) 250 self.resize(size) 251 self.setWindowTitle(self.cw.windowTitle()) 252 253 self.setStyle(Preferences.getUI("Style"), 254 Preferences.getUI("StyleSheet")) 255 256 self.cw.buttonBox.accepted.connect(self.close) 257 self.cw.buttonBox.rejected.connect(self.close) 258