diff --git a/src/framework/extensions/extensionstypes.h b/src/framework/extensions/extensionstypes.h index ef49931c311b5..8ee7d6d4b3ea8 100644 --- a/src/framework/extensions/extensionstypes.h +++ b/src/framework/extensions/extensionstypes.h @@ -218,6 +218,7 @@ struct Manifest { }; Uri uri; + io::path_t path; Type type = Type::Undefined; String title; String description; @@ -226,6 +227,7 @@ struct Manifest { String version; int apiversion = DEFAULT_API_VERSION; bool legacyPlugin = false; + bool isRemovable = false; std::vector actions; diff --git a/src/framework/extensions/iextensioninstaller.h b/src/framework/extensions/iextensioninstaller.h index 63786115cda30..e117114fb3d3d 100644 --- a/src/framework/extensions/iextensioninstaller.h +++ b/src/framework/extensions/iextensioninstaller.h @@ -1,6 +1,7 @@ #pragma once #include "modularity/imoduleinterface.h" +#include "extensionstypes.h" #include "global/types/ret.h" #include "global/io/path.h" @@ -15,5 +16,6 @@ class IExtensionInstaller : MODULE_EXPORT_INTERFACE virtual Ret isFileSupported(const io::path_t path) const = 0; virtual Ret installExtension(const io::path_t path) = 0; + virtual Ret uninstallExtension(const Uri& uri) = 0; }; } diff --git a/src/framework/extensions/internal/extensioninstaller.cpp b/src/framework/extensions/internal/extensioninstaller.cpp index 69d06ab51005f..81dafeeffd794 100644 --- a/src/framework/extensions/internal/extensioninstaller.cpp +++ b/src/framework/extensions/internal/extensioninstaller.cpp @@ -2,6 +2,7 @@ #include "global/io/fileinfo.h" #include "global/serialization/zipreader.h" +#include "global/translation.h" #include "global/uuid.h" #include "../extensionstypes.h" @@ -36,11 +37,40 @@ Ret ExtensionInstaller::installExtension(const io::path_t srcPath) ExtensionsLoader loader; Manifest m = loader.parseManifest(data); - bool hasSame = provider()->manifest(m.uri).isValid(); - if (hasSame) { + Manifest existingManifest = provider()->manifest(m.uri); + bool alreadyInstalled = existingManifest.isValid(); + if (alreadyInstalled && existingManifest.version == m.version) { LOGI() << "already installed: " << m.uri; + + interactive()->info(trc("extensions", "The extension is already installed."), std::string(), + { interactive()->buttonData(IInteractive::Button::Ok) }); + return make_ok(); } + + if (alreadyInstalled && !existingManifest.isRemovable) { + interactive()->error(trc("extensions", "This extension cannot be updated."), std::string(), + { interactive()->buttonData(IInteractive::Button::Ok) }); + + return make_ok(); + } + + if (alreadyInstalled) { + std::string text = qtrc("extensions", "Another version of the extension “%1” is already installed (version %2). " + "Do you want to replace it with version %3?") + .arg(existingManifest.title, existingManifest.version, m.version).toStdString(); + + IInteractive::Result result = interactive()->question(trc("extensions", "Update Extension"), text, { + interactive()->buttonData(IInteractive::Button::Cancel), + interactive()->buttonData(IInteractive::Button::Ok) + }); + + if (result.button() == int(IInteractive::Button::Ok)) { + this->uninstallExtension(existingManifest.uri); + } else { + return make_ok(); + } + } } // unpack @@ -66,3 +96,24 @@ Ret ExtensionInstaller::installExtension(const io::path_t srcPath) return ret; } + +Ret ExtensionInstaller::uninstallExtension(const Uri& uri) +{ + Manifest manifest = provider()->manifest(uri); + if (!manifest.isValid()) { + return make_ret(Ret::Code::UnknownError); + } else if (!manifest.isRemovable) { + return make_ret(Ret::Code::NotSupported); + } + + io::path_t path = manifest.path; + + Ret ret = fileSystem()->remove(io::dirpath(path)); + if (!ret) { + LOGE() << "Failed to delete the folder: " << path; + return ret; + } + + provider()->reloadExtensions(); + return make_ok(); +} diff --git a/src/framework/extensions/internal/extensioninstaller.h b/src/framework/extensions/internal/extensioninstaller.h index d0bde37840ecf..20655e69b1fce 100644 --- a/src/framework/extensions/internal/extensioninstaller.h +++ b/src/framework/extensions/internal/extensioninstaller.h @@ -5,17 +5,22 @@ #include "modularity/ioc.h" #include "../iextensionsconfiguration.h" #include "../iextensionsprovider.h" +#include "global/iinteractive.h" +#include "io/ifilesystem.h" namespace muse::extensions { class ExtensionInstaller : public IExtensionInstaller { muse::GlobalInject configuration; muse::GlobalInject provider; + muse::GlobalInject interactive; + muse::GlobalInject fileSystem; public: ExtensionInstaller() = default; Ret isFileSupported(const io::path_t path) const override; Ret installExtension(const io::path_t path) override; + Ret uninstallExtension(const Uri& uri) override; }; } diff --git a/src/framework/extensions/internal/extensionsloader.cpp b/src/framework/extensions/internal/extensionsloader.cpp index 304080e8366ef..0b63f2c21ccc4 100644 --- a/src/framework/extensions/internal/extensionsloader.cpp +++ b/src/framework/extensions/internal/extensionsloader.cpp @@ -58,7 +58,7 @@ ManifestList ExtensionsLoader::loadManifestList(const io::path_t& defPath, const retList.push_back(m); } - for (const Manifest& m : externalManifests) { + for (Manifest& m : externalManifests) { if (!m.isValid()) { continue; } @@ -67,6 +67,7 @@ ManifestList ExtensionsLoader::loadManifestList(const io::path_t& defPath, const continue; } + m.isRemovable = true; retList.push_back(m); } @@ -80,6 +81,7 @@ ManifestList ExtensionsLoader::manifestList(const io::path_t& rootPath) const for (const io::path_t& path : paths) { LOGD() << "parsing manifest: " << path; Manifest manifest = parseManifest(path); + manifest.path = path; resolvePaths(manifest, io::FileInfo(path).dirPath()); manifests.push_back(manifest); } diff --git a/src/framework/extensions/internal/extensionsprovider.cpp b/src/framework/extensions/internal/extensionsprovider.cpp index b729b867e5cad..b807c42f1376e 100644 --- a/src/framework/extensions/internal/extensionsprovider.cpp +++ b/src/framework/extensions/internal/extensionsprovider.cpp @@ -23,6 +23,7 @@ #include "global/containers.h" #include "global/async/async.h" +#include "global/io/path.h" #include "extensionsloader.h" #include "legacy/extpluginsloader.h" diff --git a/src/framework/extensions/internal/extensionsprovider.h b/src/framework/extensions/internal/extensionsprovider.h index 6e76aea3c77cf..69911d3c3459d 100644 --- a/src/framework/extensions/internal/extensionsprovider.h +++ b/src/framework/extensions/internal/extensionsprovider.h @@ -29,6 +29,7 @@ #include "../iextensionsprovider.h" #include "../iextensionsexecpointsregister.h" #include "global/iinteractive.h" +#include "io/ifilesystem.h" namespace muse::extensions { class ExtensionsProvider : public IExtensionsProvider, public Injectable, public async::Asyncable @@ -36,6 +37,7 @@ class ExtensionsProvider : public IExtensionsProvider, public Injectable, public Inject configuration = { this }; Inject execPointsRegister = { this }; Inject interactive = { this }; + Inject fileSystem = { this }; public: ExtensionsProvider(const modularity::ContextPtr& iocCtx) diff --git a/src/framework/extensions/internal/legacy/extpluginsloader.cpp b/src/framework/extensions/internal/legacy/extpluginsloader.cpp index 0fa433f1a6ca4..03393c2eed71b 100644 --- a/src/framework/extensions/internal/legacy/extpluginsloader.cpp +++ b/src/framework/extensions/internal/legacy/extpluginsloader.cpp @@ -76,10 +76,11 @@ ManifestList ExtPluginsLoader::loadManifestList(const io::path_t& defPath, const retList.push_back(m); } - for (const Manifest& m : externalManifests) { + for (Manifest& m : externalManifests) { if (!m.isValid()) { continue; } + m.isRemovable = true; retList.push_back(m); } @@ -95,6 +96,7 @@ ManifestList ExtPluginsLoader::manifestList(const io::path_t& rootPath) const if (!manifest.isValid()) { continue; } + manifest.path = path; resolvePaths(manifest, io::FileInfo(path).dirPath()); manifests.push_back(manifest); } diff --git a/src/framework/extensions/qml/Muse/Extensions/ExtensionsListPanel.qml b/src/framework/extensions/qml/Muse/Extensions/ExtensionsListPanel.qml index 73aa033d61234..2c23535f3e098 100644 --- a/src/framework/extensions/qml/Muse/Extensions/ExtensionsListPanel.qml +++ b/src/framework/extensions/qml/Muse/Extensions/ExtensionsListPanel.qml @@ -187,6 +187,7 @@ Item { background: flickable isEnabled: Boolean(selectedPlugin) ? selectedPlugin.enabled : false + isRemovable: Boolean(selectedPlugin) ? selectedPlugin.isRemovable : false additionalInfoModel: [ {"title": qsTrc("extensions", "Version:"), "value": Boolean(selectedPlugin) ? selectedPlugin.version : "" }, @@ -198,6 +199,12 @@ Item { extensionsModel.selectExecPoint(selectedPlugin.uri, index) } + onRemoveRequest: function() { + extensionsModel.removeExtension(selectedPlugin.uri) + prv.resetSelectedPlugin() + panel.close() + } + onEditShortcutRequested: { Qt.callLater(extensionsModel.editShortcut, selectedPlugin.uri) panel.close() diff --git a/src/framework/extensions/qml/Muse/Extensions/internal/EnablePanel.qml b/src/framework/extensions/qml/Muse/Extensions/internal/EnablePanel.qml index 7b4343b0e29ea..984556e804914 100644 --- a/src/framework/extensions/qml/Muse/Extensions/internal/EnablePanel.qml +++ b/src/framework/extensions/qml/Muse/Extensions/internal/EnablePanel.qml @@ -32,9 +32,11 @@ InfoPanel { property var execPointsModel: null property int currentExecPointIndex: 0 + property bool isRemovable: false signal editShortcutRequested() signal execPointSelected(int index) + signal removeRequest() buttonsPanel: RowLayout { id: buttons @@ -84,37 +86,62 @@ InfoPanel { } } - - - FlatButton { - id: mainButton + RowLayout { Layout.alignment: Qt.AlignRight + spacing: 22 + + FlatButton { + id: removeButton + + visible: root.isRemovable + navigation.name: text + "Button" + navigation.panel: root.contentNavigation + navigation.column: 2 + accessible.ignored: true + navigation.onActiveChanged: { + if (!navigation.active) { + accessible.ignored = false + } + } - navigation.name: text + "Button" - navigation.panel: root.contentNavigation - navigation.column: 3 - accessible.ignored: true - navigation.onActiveChanged: { - if (!navigation.active) { - accessible.ignored = false + text: qsTrc("workspace", "Remove") + + onClicked: { + root.removeRequest() } } - text: !root.isEnabled ? qsTrc("extensions", "Enable") : qsTrc("extensions", "Disable") - Component.onCompleted: { - root.mainButton = mainButton - } - onClicked: { - //! NOTE temporary - // The function with the choice of the call point is not ready yet. - // Therefore, here is the previous solution with the button, - // but in fact the choice is made from the list - // 0 - disabled - // 1 - enabled (manual call) - // (here we switch to the opposite state) - root.execPointSelected(root.isEnabled ? 0 : 1) + FlatButton { + id: mainButton + + navigation.name: text + "Button" + navigation.panel: root.contentNavigation + navigation.column: 3 + accessible.ignored: true + navigation.onActiveChanged: { + if (!navigation.active) { + accessible.ignored = false + } + } + + text: !root.isEnabled ? qsTrc("extensions", "Enable") : qsTrc("extensions", "Disable") + + Component.onCompleted: { + root.mainButton = mainButton + } + + onClicked: { + //! NOTE temporary + // The function with the choice of the call point is not ready yet. + // Therefore, here is the previous solution with the button, + // but in fact the choice is made from the list + // 0 - disabled + // 1 - enabled (manual call) + // (here we switch to the opposite state) + root.execPointSelected(root.isEnabled ? 0 : 1) + } } } } diff --git a/src/framework/extensions/view/extensionslistmodel.cpp b/src/framework/extensions/view/extensionslistmodel.cpp index 5c90b0bf2f807..47daf56f51de6 100644 --- a/src/framework/extensions/view/extensionslistmodel.cpp +++ b/src/framework/extensions/view/extensionslistmodel.cpp @@ -44,6 +44,7 @@ ExtensionsListModel::ExtensionsListModel(QObject* parent) { rCategory, "category" }, { rVersion, "version" }, { rShortcuts, "shortcuts" }, + { rIsRemovable, "isRemovable" } }; } @@ -120,6 +121,9 @@ QVariant ExtensionsListModel::data(const QModelIndex& index, int role) const //: No keyboard shortcut is assigned to this plugin. return muse::qtrc("extensions", "Not defined"); } + case rIsRemovable: { + return plugin.isRemovable; + } } return QVariant(); @@ -219,6 +223,11 @@ void ExtensionsListModel::reloadPlugins() provider()->reloadExtensions(); } +void ExtensionsListModel::removeExtension(const QString& uri) +{ + installer()->uninstallExtension(Uri(uri.toStdString())); +} + QVariantList ExtensionsListModel::categories() const { QVariantList result; diff --git a/src/framework/extensions/view/extensionslistmodel.h b/src/framework/extensions/view/extensionslistmodel.h index 7dba9d2b7ca72..bba32da4acd33 100644 --- a/src/framework/extensions/view/extensionslistmodel.h +++ b/src/framework/extensions/view/extensionslistmodel.h @@ -29,6 +29,7 @@ #include "modularity/ioc.h" #include "iinteractive.h" +#include "extensions/iextensioninstaller.h" #include "extensions/iextensionsconfiguration.h" #include "extensions/iextensionsprovider.h" #include "shortcuts/ishortcutsregister.h" @@ -40,6 +41,7 @@ class ExtensionsListModel : public QAbstractListModel, public Injectable, public Inject interactive = { this }; Inject provider = { this }; + Inject installer = { this }; Inject configuration = { this }; Inject shortcutsRegister = { this }; @@ -58,6 +60,7 @@ class ExtensionsListModel : public QAbstractListModel, public Injectable, public Q_INVOKABLE void editShortcut(const QString& uri); Q_INVOKABLE void reloadPlugins(); + Q_INVOKABLE void removeExtension(const QString& uri); Q_INVOKABLE QVariantList categories() const; @@ -73,7 +76,8 @@ class ExtensionsListModel : public QAbstractListModel, public Injectable, public rEnabled, rCategory, rVersion, - rShortcuts + rShortcuts, + rIsRemovable }; struct ExecPoints {