diff --git a/images/images.qrc b/images/images.qrc index c43132fd74a..c71b40617cd 100644 --- a/images/images.qrc +++ b/images/images.qrc @@ -286,6 +286,7 @@ themes/default/mActionShowHideLabels.svg themes/default/mActionShowPinnedLabels.svg themes/default/mActionShowPluginManager.svg + themes/default/mActionInstallPluginFromZip.svg themes/default/mActionShowRasterCalculator.png themes/default/mActionShowSelectedLayers.svg themes/default/mActionSimplify.svg diff --git a/images/themes/default/mActionInstallPluginFromZip.svg b/images/themes/default/mActionInstallPluginFromZip.svg new file mode 100644 index 00000000000..ee332d9a86b --- /dev/null +++ b/images/themes/default/mActionInstallPluginFromZip.svg @@ -0,0 +1,89 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + diff --git a/python/pyplugin_installer/installer.py b/python/pyplugin_installer/installer.py index 2acb6adad61..caed3cda195 100644 --- a/python/pyplugin_installer/installer.py +++ b/python/pyplugin_installer/installer.py @@ -24,8 +24,11 @@ """ from builtins import str -from qgis.PyQt.QtCore import Qt, QObject, QSettings, QDir, QUrl -from qgis.PyQt.QtWidgets import QMessageBox, QLabel, QFrame, QApplication +import os +import zipfile + +from qgis.PyQt.QtCore import Qt, QObject, QSettings, QDir, QUrl, QSettings, QFileInfo, QFile +from qgis.PyQt.QtWidgets import QMessageBox, QLabel, QFrame, QApplication, QFileDialog from qgis.PyQt.QtNetwork import QNetworkRequest import qgis @@ -39,6 +42,7 @@ from .qgsplugininstallerinstallingdialog import QgsPluginInstallerInstallingDial from .qgsplugininstallerpluginerrordialog import QgsPluginInstallerPluginErrorDialog from .qgsplugininstallerfetchingdialog import QgsPluginInstallerFetchingDialog from .qgsplugininstallerrepositorydialog import QgsPluginInstallerRepositoryDialog +from .unzip import unzip # public instances: @@ -345,14 +349,14 @@ class QgsPluginInstaller(QObject): error = True infoString = (self.tr("Plugin uninstall failed"), result) try: - exec ("sys.path_importer_cache.clear()") - exec ("import %s" % plugin["id"]) - exec ("reload (%s)" % plugin["id"]) + exec("sys.path_importer_cache.clear()") + exec("import %s" % plugin["id"]) + exec("reload (%s)" % plugin["id"]) except: pass else: try: - exec ("del sys.modules[%s]" % plugin["id"]) + exec("del sys.modules[%s]" % plugin["id"]) except: pass plugins.getAllInstalled() @@ -399,12 +403,12 @@ class QgsPluginInstaller(QObject): except: pass try: - exec ("plugins[%s].unload()" % plugin["id"]) - exec ("del plugins[%s]" % plugin["id"]) + exec("plugins[%s].unload()" % plugin["id"]) + exec("del plugins[%s]" % plugin["id"]) except: pass try: - exec ("del sys.modules[%s]" % plugin["id"]) + exec("del sys.modules[%s]" % plugin["id"]) except: pass plugins.getAllInstalled() @@ -523,3 +527,69 @@ class QgsPluginInstaller(QObject): req.setRawHeader("Content-Type", "application/json") QgsNetworkAccessManager.instance().post(req, params) return True + + def installFromZipFile(self): + settings = QSettings() + lastDirectory = settings.value('/Qgis/plugin-installer/lastZipDirectory', '.') + filePath, _ = QFileDialog.getOpenFileName(iface.mainWindow(), + self.tr('Open file'), + lastDirectory, + self.tr('Plugin packages (*.zip *.ZIP)')) + if filePath == '': + return + + settings.setValue('/Qgis/plugin-installer/lastZipDirectory', + QFileInfo(filePath).absoluteDir().absolutePath()) + + error = False + infoString = None + + with zipfile.ZipFile(filePath, 'r') as zf: + pluginName = os.path.split(zf.namelist()[0])[0] + + pluginFileName = os.path.splitext(os.path.basename(filePath))[0] + + pluginsDirectory = qgis.utils.home_plugin_path + if not QDir(pluginsDirectory).exists(): + QDir().mkpath(pluginsDirectory) + + # If the target directory already exists as a link, + # remove the link without resolving + QFile(os.path.join(pluginsDirectory, pluginFileName)).remove() + + try: + # Test extraction. If fails, then exception will be raised + # and no removing occurs + unzip(str(filePath), str(pluginsDirectory)) + # Removing old plugin files if exist + removeDir(QDir.cleanPath(os.path.join(pluginsDirectory, pluginFileName))) + # Extract new files + unzip(str(filePath), str(pluginsDirectory)) + except: + error = True + infoString = (self.tr("Plugin installation failed"), + self.tr("Failed to unzip the plugin package\n{}.\nProbably it is broken".format(zipFilePath))) + + if infoString is None: + updateAvailablePlugins() + loadPlugin(pluginName) + plugins.getAllInstalled(testLoad=True) + plugins.rebuild() + plugin = plugins.all()[pluginName] + + if settings.contains('/PythonPlugins/' + pluginName): + if settings.value('/PythonPlugins/' + pluginName, False, bool): + startPlugin(pluginName) + reloadPlugin(pluginName) + else: + unloadPlugin(pluginName) + loadPlugin(pluginName) + else: + if startPlugin(pluginName): + settings.setValue('/PythonPlugins/' + pluginName, True) + infoString = (self.tr("Plugin installed successfully"), "") + + if infoString[0]: + level = error and QgsMessageBar.CRITICAL or QgsMessageBar.INFO + msg = "%s:%s" % (infoString[0], infoString[1]) + iface.messageBar().pushMessage(msg, level) diff --git a/src/app/qgisapp.cpp b/src/app/qgisapp.cpp index fa4845c6f7f..8f383d60273 100644 --- a/src/app/qgisapp.cpp +++ b/src/app/qgisapp.cpp @@ -965,11 +965,14 @@ QgisApp::QgisApp( QSplashScreen *splash, bool restorePlugins, bool skipVersionCh mPluginManager->setPythonUtils( mPythonUtils ); endProfile(); } - else if ( mActionShowPythonDialog ) + else if ( mActionShowPythonDialog || mActionInstallFromZip ) { // python is disabled so get rid of the action for python console + // and installing plugin from ZUIP delete mActionShowPythonDialog; + delete mActionInstallFromZip; mActionShowPythonDialog = nullptr; + mActionInstallFromZip = nullptr; } // Set icon size of toolbars @@ -1722,6 +1725,7 @@ void QgisApp::createActions() // Plugin Menu Items connect( mActionManagePlugins, SIGNAL( triggered() ), this, SLOT( showPluginManager() ) ); + connect( mActionInstallFromZip, SIGNAL( triggered() ), this, SLOT( installPluginFromZip() ) ); connect( mActionShowPythonDialog, SIGNAL( triggered() ), this, SLOT( showPythonDialog() ) ); // Settings Menu Items @@ -2612,6 +2616,7 @@ void QgisApp::setTheme( const QString& theThemeName ) mActionToggleFullScreen->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mActionToggleFullScreen.png" ) ) ); mActionProjectProperties->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mActionProjectProperties.png" ) ) ); mActionManagePlugins->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mActionShowPluginManager.svg" ) ) ); + mActionInstallFromZip->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mActionInstallPluginFromZip.svg" ) ) ); mActionShowPythonDialog->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "console/iconRunConsole.png" ) ) ); mActionCheckQgisVersion->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mIconSuccess.svg" ) ) ); mActionOptions->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mActionOptions.svg" ) ) ); @@ -8909,6 +8914,15 @@ void QgisApp::showPluginManager() } } +void QgisApp::installPluginFromZip() +{ + if ( mPythonUtils && mPythonUtils->isEnabled() ) + { + QgsPythonRunner::run( QStringLiteral( "pyplugin_installer.instance().installFromZipFile()" ) ); + } +} + + // implementation of the python runner class QgsPythonRunnerImpl : public QgsPythonRunner { diff --git a/src/app/qgisapp.h b/src/app/qgisapp.h index 4a1406a100f..a151ac07f11 100644 --- a/src/app/qgisapp.h +++ b/src/app/qgisapp.h @@ -863,6 +863,11 @@ class APP_EXPORT QgisApp : public QMainWindow, private Ui::MainWindow void showPluginManager(); //! load python support if possible void loadPythonSupport(); + + /** Install plugin from ZIP file + * @note added in QGIS 3.0 + */ + void installPluginFromZip(); //! Find the QMenu with the given name within plugin menu (ie the user visible text on the menu item) QMenu* getPluginMenu( const QString& menuName ); //! Add the action to the submenu with the given name under the plugin menu diff --git a/src/ui/qgisapp.ui b/src/ui/qgisapp.ui index 28765ca7c6c..9f9b64f6914 100644 --- a/src/ui/qgisapp.ui +++ b/src/ui/qgisapp.ui @@ -187,6 +187,7 @@ &Plugins + @@ -2591,6 +2592,15 @@ Acts on currently active editable layer Copy and Move Feature(s) + + + + :/images/themes/default/mActionInstallPluginFromZip.svg:/images/themes/default/mActionInstallPluginFromZip.svg + + + Install plugin from ZIP... + +