Browse Source

Merge pull request #3203 from Ultimaker/plugin-browser

Plugin browser
Lipu Fei 7 years ago
parent
commit
91c9a82627

+ 142 - 51
plugins/PluginBrowser/PluginBrowser.py

@@ -1,25 +1,30 @@
 # Copyright (c) 2017 Ultimaker B.V.
 # PluginBrowser is released under the terms of the LGPLv3 or higher.
-from UM.Extension import Extension
-from UM.i18n import i18nCatalog
-from UM.Logger import Logger
+
+from PyQt5.QtCore import QUrl, QObject, Qt, pyqtProperty, pyqtSignal, pyqtSlot
+from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply
+
+from UM.Application import Application
 from UM.Qt.ListModel import ListModel
+from UM.Logger import Logger
 from UM.PluginRegistry import PluginRegistry
-from UM.Application import Application
+from UM.Qt.Bindings.PluginsModel import PluginsModel
+from UM.Extension import Extension
+from UM.i18n import i18nCatalog
+
 from UM.Version import Version
 from UM.Message import Message
 
-from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply
-from PyQt5.QtCore import QUrl, QObject, Qt, pyqtProperty, pyqtSignal, pyqtSlot
-
 import json
 import os
 import tempfile
 import platform
 import zipfile
+import shutil
 
-i18n_catalog = i18nCatalog("cura")
+from cura.CuraApplication import CuraApplication
 
+i18n_catalog = i18nCatalog("cura")
 
 class PluginBrowser(QObject, Extension):
     def __init__(self, parent=None):
@@ -34,11 +39,18 @@ class PluginBrowser(QObject, Extension):
         self._download_plugin_reply = None
 
         self._network_manager = None
+        self._plugin_registry = Application.getInstance().getPluginRegistry()
 
         self._plugins_metadata = []
         self._plugins_model = None
 
+        # Can be 'installed' or 'availble'
+        self._view = "available"
+
+        self._restart_required = False
+
         self._dialog = None
+        self._restartDialog = None
         self._download_progress = 0
 
         self._is_downloading = False
@@ -52,16 +64,29 @@ class PluginBrowser(QObject, Extension):
                                           )
                                ]
 
-        # Installed plugins are really installed after reboot. In order to prevent the user from downloading the
-        # same file over and over again, we keep track of the upgraded plugins.
+        # Installed plugins are really installed after reboot. In order to
+        # prevent the user from downloading the same file over and over again,
+        # we keep track of the upgraded plugins.
+
+        # NOTE: This will be depreciated in favor of the 'status' system.
         self._newly_installed_plugin_ids = []
+        self._newly_uninstalled_plugin_ids = []
+
+        self._plugin_statuses = {} # type: Dict[str, str]
 
         # variables for the license agreement dialog
         self._license_dialog_plugin_name = ""
         self._license_dialog_license_content = ""
         self._license_dialog_plugin_file_location = ""
+        self._restart_dialog_message = ""
 
     showLicenseDialog = pyqtSignal()
+    showRestartDialog = pyqtSignal()
+    pluginsMetadataChanged = pyqtSignal()
+    onDownloadProgressChanged = pyqtSignal()
+    onIsDownloadingChanged = pyqtSignal()
+    restartRequiredChanged = pyqtSignal()
+    viewChanged = pyqtSignal()
 
     @pyqtSlot(result = str)
     def getLicenseDialogPluginName(self):
@@ -75,15 +100,19 @@ class PluginBrowser(QObject, Extension):
     def getLicenseDialogLicenseContent(self):
         return self._license_dialog_license_content
 
+    @pyqtSlot(result = str)
+    def getRestartDialogMessage(self):
+        return self._restart_dialog_message
+
     def openLicenseDialog(self, plugin_name, license_content, plugin_file_location):
         self._license_dialog_plugin_name = plugin_name
         self._license_dialog_license_content = license_content
         self._license_dialog_plugin_file_location = plugin_file_location
         self.showLicenseDialog.emit()
 
-    pluginsMetadataChanged = pyqtSignal()
-    onDownloadProgressChanged = pyqtSignal()
-    onIsDownloadingChanged = pyqtSignal()
+    def openRestartDialog(self, message):
+        self._restart_dialog_message = message
+        self.showRestartDialog.emit()
 
     @pyqtProperty(bool, notify = onIsDownloadingChanged)
     def isDownloading(self):
@@ -179,17 +208,46 @@ class PluginBrowser(QObject, Extension):
 
     @pyqtSlot(str)
     def installPlugin(self, file_path):
+        # Ensure that it starts with a /, as otherwise it doesn't work on windows.
         if not file_path.startswith("/"):
-            location = "/" + file_path  # Ensure that it starts with a /, as otherwise it doesn't work on windows.
+            location = "/" + file_path
         else:
             location = file_path
+
         result = PluginRegistry.getInstance().installPlugin("file://" + location)
 
         self._newly_installed_plugin_ids.append(result["id"])
         self.pluginsMetadataChanged.emit()
 
+        self.openRestartDialog(result["message"])
+        self._restart_required = True
+        self.restartRequiredChanged.emit()
+        # Application.getInstance().messageBox(i18n_catalog.i18nc("@window:title", "Plugin browser"), result["message"])
+
+    @pyqtSlot(str)
+    def removePlugin(self, plugin_id):
+        result = PluginRegistry.getInstance().uninstallPlugin(plugin_id)
+
+        self._newly_uninstalled_plugin_ids.append(result["id"])
+        self.pluginsMetadataChanged.emit()
+
+        self._restart_required = True
+        self.restartRequiredChanged.emit()
+
         Application.getInstance().messageBox(i18n_catalog.i18nc("@window:title", "Plugin browser"), result["message"])
 
+    @pyqtSlot(str)
+    def enablePlugin(self, plugin_id):
+        self._plugin_registry.enablePlugin(plugin_id)
+        self.pluginsMetadataChanged.emit()
+        Logger.log("i", "%s was set as 'active'", id)
+
+    @pyqtSlot(str)
+    def disablePlugin(self, plugin_id):
+        self._plugin_registry.disablePlugin(plugin_id)
+        self.pluginsMetadataChanged.emit()
+        Logger.log("i", "%s was set as 'deactive'", id)
+
     @pyqtProperty(int, notify = onDownloadProgressChanged)
     def downloadProgress(self):
         return self._download_progress
@@ -221,55 +279,72 @@ class PluginBrowser(QObject, Extension):
         self.setDownloadProgress(0)
         self.setIsDownloading(False)
 
+    @pyqtSlot(str)
+    def setView(self, view):
+        self._view = view
+        self.viewChanged.emit()
+        self.pluginsMetadataChanged.emit()
+
     @pyqtProperty(QObject, notify=pluginsMetadataChanged)
     def pluginsModel(self):
-        if self._plugins_model is None:
-            self._plugins_model = ListModel()
-            self._plugins_model.addRoleName(Qt.UserRole + 1, "name")
-            self._plugins_model.addRoleName(Qt.UserRole + 2, "version")
-            self._plugins_model.addRoleName(Qt.UserRole + 3, "short_description")
-            self._plugins_model.addRoleName(Qt.UserRole + 4, "author")
-            self._plugins_model.addRoleName(Qt.UserRole + 5, "already_installed")
-            self._plugins_model.addRoleName(Qt.UserRole + 6, "file_location")
-            self._plugins_model.addRoleName(Qt.UserRole + 7, "can_upgrade")
-        else:
-            self._plugins_model.clear()
-        items = []
-        for metadata in self._plugins_metadata:
-            items.append({
-                "name": metadata["label"],
-                "version": metadata["version"],
-                "short_description": metadata["short_description"],
-                "author": metadata["author"],
-                "already_installed": self._checkAlreadyInstalled(metadata["id"]),
-                "file_location": metadata["file_location"],
-                "can_upgrade": self._checkCanUpgrade(metadata["id"], metadata["version"])
-            })
-        self._plugins_model.setItems(items)
+        print("Updating plugins model...", self._view)
+        self._plugins_model = PluginsModel(self._view)
+        # self._plugins_model.update()
+
+        # Check each plugin the registry for matching plugin from server
+        # metadata, and if found, compare the versions. Higher version sets
+        # 'can_upgrade' to 'True':
+        for plugin in self._plugins_model.items:
+            if self._checkCanUpgrade(plugin["id"], plugin["version"]):
+                plugin["can_upgrade"] = True
+                print(self._plugins_metadata)
+
+                for item in self._plugins_metadata:
+                    if item["id"] == plugin["id"]:
+                        plugin["update_url"] = item["file_location"]
+
         return self._plugins_model
 
+
+
     def _checkCanUpgrade(self, id, version):
-        plugin_registry = PluginRegistry.getInstance()
-        metadata = plugin_registry.getMetaData(id)
-        if metadata != {}:
-            if id in self._newly_installed_plugin_ids:
-                return False  # We already updated this plugin.
-            current_version = Version(metadata["plugin"]["version"])
-            new_version = Version(version)
-            if new_version > current_version:
-                return True
+
+        # TODO: This could maybe be done more efficiently using a dictionary...
+
+        # Scan plugin server data for plugin with the given id:
+        for plugin in self._plugins_metadata:
+            if id == plugin["id"]:
+                reg_version = Version(version)
+                new_version = Version(plugin["version"])
+                if new_version > reg_version:
+                    Logger.log("i", "%s has an update availible: %s", plugin["id"], plugin["version"])
+                    return True
         return False
 
     def _checkAlreadyInstalled(self, id):
-        plugin_registry = PluginRegistry.getInstance()
-        metadata = plugin_registry.getMetaData(id)
-        if metadata != {}:
+        metadata = self._plugin_registry.getMetaData(id)
+        # We already installed this plugin, but the registry just doesn't know it yet.
+        if id in self._newly_installed_plugin_ids:
+            return True
+        # We already uninstalled this plugin, but the registry just doesn't know it yet:
+        elif id in self._newly_uninstalled_plugin_ids:
+            return False
+        elif metadata != {}:
             return True
         else:
-            if id in self._newly_installed_plugin_ids:
-                return True  # We already installed this plugin, but the registry just doesn't know it yet.
             return False
 
+    def _checkInstallStatus(self, plugin_id):
+        if plugin_id in self._plugin_registry.getInstalledPlugins():
+            return "installed"
+        else:
+            return "uninstalled"
+
+    def _checkEnabled(self, id):
+        if id in self._plugin_registry.getActivePlugins():
+            return True
+        return False
+
     def _onRequestFinished(self, reply):
         reply_url = reply.url().toString()
         if reply.error() == QNetworkReply.TimeoutError:
@@ -290,7 +365,11 @@ class PluginBrowser(QObject, Extension):
             if reply_url == self._api_url + "plugins":
                 try:
                     json_data = json.loads(bytes(reply.readAll()).decode("utf-8"))
+
+                    # Add metadata to the manager:
                     self._plugins_metadata = json_data
+                    print(self._plugins_metadata)
+                    self._plugin_registry.addExternalPlugins(self._plugins_metadata)
                     self.pluginsMetadataChanged.emit()
                 except json.decoder.JSONDecodeError:
                     Logger.log("w", "Received an invalid print job state message: Not valid JSON.")
@@ -316,3 +395,15 @@ class PluginBrowser(QObject, Extension):
         self._network_manager = QNetworkAccessManager()
         self._network_manager.finished.connect(self._onRequestFinished)
         self._network_manager.networkAccessibleChanged.connect(self._onNetworkAccesibleChanged)
+
+    @pyqtProperty(bool, notify=restartRequiredChanged)
+    def restartRequired(self):
+        return self._restart_required
+
+    @pyqtProperty(str, notify=viewChanged)
+    def viewing(self):
+        return self._view
+
+    @pyqtSlot()
+    def restart(self):
+        CuraApplication.getInstance().quit()

+ 252 - 146
plugins/PluginBrowser/PluginBrowser.qml

@@ -1,191 +1,209 @@
-import UM 1.1 as UM
+// Copyright (c) 2017 Ultimaker B.V.
+// PluginBrowser is released under the terms of the LGPLv3 or higher.
+
 import QtQuick 2.2
 import QtQuick.Dialogs 1.1
 import QtQuick.Window 2.2
-import QtQuick.Controls 1.1
+import QtQuick.Controls 1.4
+import QtQuick.Controls.Styles 1.4
 
-UM.Dialog
-{
+// TODO: Switch to QtQuick.Controls 2.x and remove QtQuick.Controls.Styles
+
+import UM 1.1 as UM
+
+Window {
     id: base
 
-    title: catalog.i18nc("@title:window", "Find & Update plugins")
-    width: 600 * screenScaleFactor
-    height: 450 * screenScaleFactor
+    title: catalog.i18nc("@title:tab", "Plugins");
+    width: 800 * screenScaleFactor
+    height: 640 * screenScaleFactor
     minimumWidth: 350 * screenScaleFactor
     minimumHeight: 350 * screenScaleFactor
-    Item
-    {
-        anchors.fill: parent
-        Item
-        {
+    color: UM.Theme.getColor("sidebar")
+
+    Item {
+        id: view
+        anchors {
+            fill: parent
+            leftMargin: UM.Theme.getSize("default_margin").width
+            rightMargin: UM.Theme.getSize("default_margin").width
+            topMargin: UM.Theme.getSize("default_margin").height
+            bottomMargin: UM.Theme.getSize("default_margin").height
+        }
+
+        Rectangle {
             id: topBar
-            height: childrenRect.height;
             width: parent.width
-            Label
-            {
-                id: introText
-                text: catalog.i18nc("@label", "Here you can find a list of Third Party plugins.")
-                width: parent.width
-                height: 30
-            }
+            color: "transparent"
+            height: childrenRect.height
 
-            Button
-            {
-                id: refresh
-                text: catalog.i18nc("@action:button", "Refresh")
-                onClicked: manager.requestPluginList()
-                anchors.right: parent.right
-                enabled: !manager.isDownloading
+            Row {
+                spacing: 12
+                height: childrenRect.height
+                width: childrenRect.width
+                anchors.horizontalCenter: parent.horizontalCenter
+
+                Button {
+                    text: "Install"
+                    style: ButtonStyle {
+                        background: Rectangle {
+                            color: "transparent"
+                            implicitWidth: 96
+                            implicitHeight: 48
+                            Rectangle {
+                                visible: manager.viewing == "available" ? true : false
+                                color: UM.Theme.getColor("primary")
+                                anchors.bottom: parent.bottom
+                                width: parent.width
+                                height: 3
+                            }
+                        }
+                        label: Text {
+                            text: control.text
+                            color: UM.Theme.getColor("text")
+                            font {
+                                pixelSize: 15
+                            }
+                            verticalAlignment: Text.AlignVCenter
+                            horizontalAlignment: Text.AlignHCenter
+                        }
+                    }
+                    onClicked: manager.setView("available")
+                }
+
+                Button {
+                    text: "Manage"
+                    style: ButtonStyle {
+                        background: Rectangle {
+                            color: "transparent"
+                            implicitWidth: 96
+                            implicitHeight: 48
+                            Rectangle {
+                                visible: manager.viewing == "installed" ? true : false
+                                color: UM.Theme.getColor("primary")
+                                anchors.bottom: parent.bottom
+                                width: parent.width
+                                height: 3
+                            }
+                        }
+                        label: Text {
+                            text: control.text
+                            color: UM.Theme.getColor("text")
+                            font {
+                                pixelSize: 15
+                            }
+                            verticalAlignment: Text.AlignVCenter
+                            horizontalAlignment: Text.AlignHCenter
+                        }
+                    }
+                    onClicked: manager.setView("installed")
+                }
             }
         }
-        ScrollView
-        {
+
+        // Scroll view breaks in QtQuick.Controls 2.x
+        ScrollView {
+            id: installedPluginList
             width: parent.width
-            anchors.top: topBar.bottom
-            anchors.bottom: bottomBar.top
-            anchors.bottomMargin: UM.Theme.getSize("default_margin").height
+            height: 400
+
+            anchors {
+                top: topBar.bottom
+                topMargin: UM.Theme.getSize("default_margin").height
+                bottom: bottomBar.top
+                bottomMargin: UM.Theme.getSize("default_margin").height
+            }
+
             frameVisible: true
-            ListView
-            {
+
+            ListView {
                 id: pluginList
-                model: manager.pluginsModel
+                property var activePlugin
+                property var filter: "installed"
+
                 anchors.fill: parent
 
-                property var activePlugin
-                delegate: pluginDelegate
+                model: manager.pluginsModel
+                delegate: PluginEntry {}
             }
         }
-        Item
-        {
+
+        Rectangle {
             id: bottomBar
             width: parent.width
-            height: closeButton.height
+            height: childrenRect.height
+            color: "transparent"
             anchors.bottom: parent.bottom
-            anchors.left: parent.left
-            ProgressBar
-            {
-                id: progressbar
-                anchors.bottom: parent.bottom
-                minimumValue: 0;
-                maximumValue: 100
-                anchors.left:parent.left
+
+            Label {
+                visible: manager.restartRequired
+                text: "You will need to restart Cura before changes in plugins have effect."
+                height: 30
+                verticalAlignment: Text.AlignVCenter
+            }
+            Button {
+                id: restartChangedButton
+                text: "Quit Cura"
                 anchors.right: closeButton.left
                 anchors.rightMargin: UM.Theme.getSize("default_margin").width
-                value: manager.isDownloading ? manager.downloadProgress : 0
+                visible: manager.restartRequired
+                iconName: "dialog-restart"
+                onClicked: manager.restart()
+                style: ButtonStyle {
+                    background: Rectangle {
+                        implicitWidth: 96
+                        implicitHeight: 30
+                        color: UM.Theme.getColor("primary")
+                    }
+                    label: Text {
+                        verticalAlignment: Text.AlignVCenter
+                        color: UM.Theme.getColor("button_text")
+                        font {
+                            pixelSize: 13
+                            bold: true
+                        }
+                        text: control.text
+                        horizontalAlignment: Text.AlignHCenter
+                    }
+                }
             }
 
-            Button
-            {
+            Button {
                 id: closeButton
                 text: catalog.i18nc("@action:button", "Close")
                 iconName: "dialog-close"
-                onClicked:
-                {
-                    if (manager.isDownloading)
-                    {
+                onClicked: {
+                    if ( manager.isDownloading ) {
                         manager.cancelDownload()
                     }
                     base.close();
                 }
-                anchors.bottom: parent.bottom
                 anchors.right: parent.right
-            }
-        }
-
-        Item
-        {
-            SystemPalette { id: palette }
-            Component
-            {
-                id: pluginDelegate
-                Rectangle
-                {
-                    width: pluginList.width;
-                    height: texts.height;
-                    color: index % 2 ? palette.base : palette.alternateBase
-                    Column
-                    {
-                        id: texts
-                        width: parent.width
-                        height: childrenRect.height
-                        anchors.left: parent.left
-                        anchors.leftMargin: UM.Theme.getSize("default_margin").width
-                        anchors.right: downloadButton.left
-                        anchors.rightMargin: UM.Theme.getSize("default_margin").width
-                        Label
-                        {
-                            text: "<b>" + model.name + "</b>" + ((model.author !== "") ? (" - " + model.author) : "")
-                            width: contentWidth
-                            height: contentHeight +  UM.Theme.getSize("default_margin").height
-                            verticalAlignment: Text.AlignVCenter
-                        }
-
-                        Label
-                        {
-                            text: model.short_description
-                            width: parent.width
-                            height: contentHeight +  UM.Theme.getSize("default_margin").height
-                            wrapMode: Text.WordWrap
-                            verticalAlignment: Text.AlignVCenter
+                style: ButtonStyle {
+                    background: Rectangle {
+                        color: "transparent"
+                        implicitWidth: 96
+                        implicitHeight: 30
+                        border {
+                            width: 1
+                            color: UM.Theme.getColor("lining")
                         }
                     }
-                    Button
-                    {
-                        id: downloadButton
-                        text:
-                        {
-                            if (manager.isDownloading && pluginList.activePlugin == model)
-                            {
-                                return catalog.i18nc("@action:button", "Cancel");
-                            }
-                            else if (model.already_installed)
-                            {
-                                if (model.can_upgrade)
-                                {
-                                    return catalog.i18nc("@action:button", "Upgrade");
-                                }
-                                return catalog.i18nc("@action:button", "Installed");
-                            }
-                            return catalog.i18nc("@action:button", "Download");
-                        }
-                        onClicked:
-                        {
-                            if(!manager.isDownloading)
-                            {
-                                pluginList.activePlugin = model;
-                                manager.downloadAndInstallPlugin(model.file_location);
-                            }
-                            else
-                            {
-                                manager.cancelDownload();
-                            }
-                        }
-                        anchors.right: parent.right
-                        anchors.rightMargin: UM.Theme.getSize("default_margin").width
-                        anchors.verticalCenter: parent.verticalCenter
-                        enabled:
-                        {
-                            if (manager.isDownloading)
-                            {
-                                return (pluginList.activePlugin == model);
-                            }
-                            else
-                            {
-                                return (!model.already_installed || model.can_upgrade);
-                            }
-                        }
+                    label: Text {
+                        verticalAlignment: Text.AlignVCenter
+                        color: UM.Theme.getColor("text")
+                        text: control.text
+                        horizontalAlignment: Text.AlignHCenter
                     }
                 }
-
             }
         }
+
         UM.I18nCatalog { id: catalog; name: "cura" }
 
-        Connections
-        {
+        Connections {
             target: manager
-            onShowLicenseDialog:
-            {
+            onShowLicenseDialog: {
                 licenseDialog.pluginName = manager.getLicenseDialogPluginName();
                 licenseDialog.licenseContent = manager.getLicenseDialogLicenseContent();
                 licenseDialog.pluginFileLocation = manager.getLicenseDialogPluginFileLocation();
@@ -193,8 +211,7 @@ UM.Dialog
             }
         }
 
-        UM.Dialog
-        {
+        UM.Dialog {
             id: licenseDialog
             title: catalog.i18nc("@title:window", "Plugin License Agreement")
 
@@ -258,5 +275,94 @@ UM.Dialog
                 }
             ]
         }
+
+        Connections {
+            target: manager
+            onShowRestartDialog: {
+                restartDialog.message = manager.getRestartDialogMessage();
+                restartDialog.show();
+            }
+        }
+
+        Window {
+            id: restartDialog
+            // title: catalog.i18nc("@title:tab", "Plugins");
+            width: 360 * screenScaleFactor
+            height: 120 * screenScaleFactor
+            minimumWidth: 360 * screenScaleFactor
+            minimumHeight: 120 * screenScaleFactor
+            color: UM.Theme.getColor("sidebar")
+            property var message;
+
+            Text {
+                id: message
+                anchors {
+                    left: parent.left
+                    leftMargin: UM.Theme.getSize("default_margin").width
+                    top: parent.top
+                    topMargin: UM.Theme.getSize("default_margin").height
+                }
+                text: restartDialog.message != null ? restartDialog.message : ""
+            }
+            Button {
+                id: laterButton
+                text: "Later"
+                onClicked: restartDialog.close();
+                anchors {
+                    left: parent.left
+                    leftMargin: UM.Theme.getSize("default_margin").width
+                    bottom: parent.bottom
+                    bottomMargin: UM.Theme.getSize("default_margin").height
+                }
+                style: ButtonStyle {
+                    background: Rectangle {
+                        color: "transparent"
+                        implicitWidth: 96
+                        implicitHeight: 30
+                        border {
+                            width: 1
+                            color: UM.Theme.getColor("lining")
+                        }
+                    }
+                    label: Text {
+                        verticalAlignment: Text.AlignVCenter
+                        color: UM.Theme.getColor("text")
+                        text: control.text
+                        horizontalAlignment: Text.AlignHCenter
+                    }
+                }
+            }
+
+
+            Button {
+                id: restartButton
+                text: "Quit Cura"
+                anchors {
+                    right: parent.right
+                    rightMargin: UM.Theme.getSize("default_margin").width
+                    bottom: parent.bottom
+                    bottomMargin: UM.Theme.getSize("default_margin").height
+                }
+                onClicked: manager.restart()
+                style: ButtonStyle {
+                    background: Rectangle {
+                        implicitWidth: 96
+                        implicitHeight: 30
+                        color: UM.Theme.getColor("primary")
+                    }
+                    label: Text {
+                        verticalAlignment: Text.AlignVCenter
+                        color: UM.Theme.getColor("button_text")
+                        font {
+                            pixelSize: 13
+                            bold: true
+                        }
+                        text: control.text
+                        horizontalAlignment: Text.AlignHCenter
+                    }
+                }
+            }
+        }
+
     }
 }

+ 474 - 0
plugins/PluginBrowser/PluginEntry.qml

@@ -0,0 +1,474 @@
+// Copyright (c) 2017 Ultimaker B.V.
+// PluginBrowser is released under the terms of the LGPLv3 or higher.
+
+import QtQuick 2.2
+import QtQuick.Dialogs 1.1
+import QtQuick.Window 2.2
+import QtQuick.Controls 1.4
+import QtQuick.Controls.Styles 1.4
+
+// TODO: Switch to QtQuick.Controls 2.x and remove QtQuick.Controls.Styles
+
+import UM 1.1 as UM
+
+Component {
+    id: pluginDelegate
+
+    Rectangle {
+
+        // Don't show required plugins as they can't be managed anyway:
+        height: !model.required ? 84 : 0
+        visible: !model.required ? true : false
+        color: "transparent"
+        anchors {
+            left: parent.left
+            leftMargin: UM.Theme.getSize("default_margin").width
+            right: parent.right
+            rightMargin: UM.Theme.getSize("default_margin").width
+        }
+
+
+        // Bottom border:
+        Rectangle {
+            color: UM.Theme.getColor("lining")
+            width: parent.width
+            height: 1
+            anchors.bottom: parent.bottom
+        }
+
+        // Plugin info
+        Column {
+            id: pluginInfo
+
+            property var color: model.enabled ? UM.Theme.getColor("text") : UM.Theme.getColor("lining")
+
+            // Styling:
+            height: parent.height
+            anchors {
+                left: parent.left
+                top: parent.top
+                topMargin: UM.Theme.getSize("default_margin").height
+                right: authorInfo.left
+                rightMargin: UM.Theme.getSize("default_margin").width
+            }
+
+
+            Label {
+                text: model.name
+                width: parent.width
+                height: 24
+                wrapMode: Text.WordWrap
+                verticalAlignment: Text.AlignVCenter
+                font {
+                    pixelSize: 13
+                    bold: true
+                }
+                color: pluginInfo.color
+
+            }
+
+            Text {
+                text: model.description
+                width: parent.width
+                height: 36
+                clip: true
+                wrapMode: Text.WordWrap
+                color: pluginInfo.color
+                elide: Text.ElideRight
+            }
+        }
+
+        // Author info
+        Column {
+            id: authorInfo
+            width: 192
+            height: parent.height
+            anchors {
+                top: parent.top
+                topMargin: UM.Theme.getSize("default_margin").height
+                right: pluginActions.left
+                rightMargin: UM.Theme.getSize("default_margin").width
+            }
+
+            Label {
+                text: "<a href=\"mailto:"+model.author_email+"?Subject=Cura: "+model.name+"\">"+model.author+"</a>"
+                width: parent.width
+                height: 24
+                wrapMode: Text.WordWrap
+                verticalAlignment: Text.AlignVCenter
+                horizontalAlignment: Text.AlignLeft
+                onLinkActivated: Qt.openUrlExternally("mailto:"+model.author_email+"?Subject=Cura: "+model.name+" Plugin")
+                color: model.enabled ? UM.Theme.getColor("text") : UM.Theme.getColor("lining")
+            }
+        }
+
+        // Plugin actions
+        Row {
+            id: pluginActions
+
+            width: 96
+            height: parent.height
+            anchors {
+                top: parent.top
+                right: parent.right
+                topMargin: UM.Theme.getSize("default_margin").height
+            }
+            layoutDirection: Qt.RightToLeft
+            spacing: UM.Theme.getSize("default_margin").width
+
+            // For 3rd-Party Plugins:
+            Button {
+                id: installButton
+                text: {
+                    if ( manager.isDownloading && pluginList.activePlugin == model ) {
+                        return catalog.i18nc( "@action:button", "Cancel" );
+                    } else {
+                        if (model.can_upgrade) {
+                            return catalog.i18nc("@action:button", "Update");
+                        }
+                        return catalog.i18nc("@action:button", "Install");
+                    }
+                }
+                visible: model.external && ((model.status !== "installed") || model.can_upgrade)
+                style: ButtonStyle {
+                    background: Rectangle {
+                        implicitWidth: 96
+                        implicitHeight: 30
+                        color: "transparent"
+                        border {
+                            width: 1
+                            color: UM.Theme.getColor("lining")
+                        }
+                    }
+                    label: Label {
+                        text: control.text
+                        color: UM.Theme.getColor("text")
+                        verticalAlignment: Text.AlignVCenter
+                        horizontalAlignment: Text.AlignHCenter
+                    }
+                }
+                onClicked: {
+                    if ( manager.isDownloading && pluginList.activePlugin == model ) {
+                        manager.cancelDownload();
+                    } else {
+                        pluginList.activePlugin = model;
+                        if ( model.can_upgrade ) {
+                            manager.downloadAndInstallPlugin( model.update_url );
+                        } else {
+                            manager.downloadAndInstallPlugin( model.file_location );
+                        }
+
+                    }
+                }
+            }
+            Button {
+                id: removeButton
+                text: "Uninstall"
+                visible: model.external && model.status == "installed"
+                enabled: !manager.isDownloading
+                style: ButtonStyle {
+                    background: Rectangle {
+                        implicitWidth: 96
+                        implicitHeight: 30
+                        color: "transparent"
+                        border {
+                            width: 1
+                            color: UM.Theme.getColor("lining")
+                        }
+                    }
+                    label: Text {
+                        text: control.text
+                        color: UM.Theme.getColor("text")
+                        verticalAlignment: Text.AlignVCenter
+                        horizontalAlignment: Text.AlignHCenter
+                    }
+                }
+                onClicked: manager.removePlugin( model.id )
+            }
+
+            // For Ultimaker Plugins:
+            Button {
+                id: enableButton
+                text: "Enable"
+                visible: !model.external && model.enabled == false
+                style: ButtonStyle {
+                    background: Rectangle {
+                        implicitWidth: 96
+                        implicitHeight: 30
+                        color: "transparent"
+                        border {
+                            width: 1
+                            color: UM.Theme.getColor("lining")
+                        }
+                    }
+                    label: Text {
+                        text: control.text
+                        color: UM.Theme.getColor("text")
+                        verticalAlignment: Text.AlignVCenter
+                        horizontalAlignment: Text.AlignHCenter
+                    }
+                }
+                onClicked: {
+                    manager.enablePlugin(model.id);
+                }
+            }
+            Button {
+                id: disableButton
+                text: "Disable"
+                visible: !model.external && model.enabled == true
+                style: ButtonStyle {
+                    background: Rectangle {
+                        implicitWidth: 96
+                        implicitHeight: 30
+                        color: "transparent"
+                        border {
+                            width: 1
+                            color: UM.Theme.getColor("lining")
+                        }
+                    }
+                    label: Text {
+                        text: control.text
+                        color: UM.Theme.getColor("text")
+                        verticalAlignment: Text.AlignVCenter
+                        horizontalAlignment: Text.AlignHCenter
+                    }
+                }
+                onClicked: {
+                    manager.disablePlugin(model.id);
+                }
+            }
+            /*
+            Rectangle {
+                id: removeControls
+                visible: model.status == "installed" && model.enabled
+                width: 96
+                height: 30
+                color: "transparent"
+                Button {
+                    id: removeButton
+                    text: "Disable"
+                    enabled: {
+                        if ( manager.isDownloading && pluginList.activePlugin == model ) {
+                            return false;
+                        } else if ( model.required ) {
+                            return false;
+                        } else {
+                            return true;
+                        }
+                    }
+                    onClicked: {
+                        manager.disablePlugin(model.id);
+                    }
+                    style: ButtonStyle {
+                        background: Rectangle {
+                            color: "white"
+                            implicitWidth: 96
+                            implicitHeight: 30
+                            border {
+                                width: 1
+                                color: UM.Theme.getColor("lining")
+                            }
+                        }
+                        label: Text {
+                            verticalAlignment: Text.AlignVCenter
+                            color: "grey"
+                            text: control.text
+                            horizontalAlignment: Text.AlignLeft
+                        }
+                    }
+                }
+                Button {
+                    id: removeDropDown
+                    property bool open: false
+                    UM.RecolorImage {
+                        anchors.centerIn: parent
+                        height: 10
+                        width: 10
+                        source: UM.Theme.getIcon("arrow_bottom")
+                        color: "grey"
+                    }
+                    enabled: {
+                        if ( manager.isDownloading && pluginList.activePlugin == model ) {
+                            return false;
+                        } else if ( model.required ) {
+                            return false;
+                        } else {
+                            return true;
+                        }
+                    }
+                    anchors.right: parent.right
+                    style: ButtonStyle {
+                        background: Rectangle {
+                            color: "transparent"
+                            implicitWidth: 30
+                            implicitHeight: 30
+                        }
+                        label: Text {
+                            verticalAlignment: Text.AlignVCenter
+                            color: "grey"
+                            text: control.text
+                            horizontalAlignment: Text.AlignHCenter
+                        }
+                    }
+
+
+
+                    // For the disable option:
+                    // onClicked: pluginList.model.setEnabled(model.id, checked)
+
+                    onClicked: {
+                        if ( !removeDropDown.open ) {
+                            removeDropDown.open = true
+                        }
+                        else {
+                            removeDropDown.open = false
+                        }
+                    }
+                }
+
+                Rectangle {
+                    id: divider
+                    width: 1
+                    height: parent.height
+                    anchors.right: removeDropDown.left
+                    color: UM.Theme.getColor("lining")
+                }
+
+                Column {
+                    id: options
+                    anchors {
+                        top: removeButton.bottom
+                        left: parent.left
+                        right: parent.right
+                    }
+                    height: childrenRect.height
+                    visible: removeDropDown.open
+
+                    Button {
+                        id: disableButton
+                        text: "Remove"
+                        height: 30
+                        width: parent.width
+                        onClicked: {
+                            removeDropDown.open = false;
+                            manager.removePlugin( model.id );
+                        }
+                    }
+                }
+            }
+            */
+            /*
+            Button {
+                id: enableButton
+                visible: !model.enabled && model.status == "installed"
+                onClicked: manager.enablePlugin( model.id );
+
+                text: "Enable"
+                style: ButtonStyle {
+                    background: Rectangle {
+                        color: "transparent"
+                        implicitWidth: 96
+                        implicitHeight: 30
+                        border {
+                            width: 1
+                            color: UM.Theme.getColor("lining")
+                        }
+                    }
+                    label: Text {
+                        verticalAlignment: Text.AlignVCenter
+                        color: UM.Theme.getColor("text")
+                        text: control.text
+                        horizontalAlignment: Text.AlignHCenter
+                    }
+                }
+            }
+
+            Button {
+                id: updateButton
+                visible: model.status == "installed" && model.can_upgrade && model.enabled
+                // visible: model.already_installed
+                text: {
+                    // If currently downloading:
+                    if ( manager.isDownloading && pluginList.activePlugin == model ) {
+                        return catalog.i18nc( "@action:button", "Cancel" );
+                    } else {
+                        return catalog.i18nc("@action:button", "Update");
+                    }
+                }
+                style: ButtonStyle {
+                    background: Rectangle {
+                        color: UM.Theme.getColor("primary")
+                        implicitWidth: 96
+                        implicitHeight: 30
+                        // radius: 4
+                    }
+                    label: Text {
+                        verticalAlignment: Text.AlignVCenter
+                        color: "white"
+                        text: control.text
+                        horizontalAlignment: Text.AlignHCenter
+                    }
+                }
+            }
+            Button {
+                id: externalControls
+                visible: model.status == "available" ? true : false
+                text: {
+                    // If currently downloading:
+                    if ( manager.isDownloading && pluginList.activePlugin == model ) {
+                        return catalog.i18nc( "@action:button", "Cancel" );
+                    } else {
+                        return catalog.i18nc("@action:button", "Install");
+                    }
+                }
+                onClicked: {
+                    if ( manager.isDownloading && pluginList.activePlugin == model ) {
+                        manager.cancelDownload();
+                    } else {
+                        pluginList.activePlugin = model;
+                        manager.downloadAndInstallPlugin( model.file_location );
+                    }
+                }
+                style: ButtonStyle {
+                    background: Rectangle {
+                        color: "transparent"
+                        implicitWidth: 96
+                        implicitHeight: 30
+                        border {
+                            width: 1
+                            color: UM.Theme.getColor("lining")
+                        }
+                    }
+                    label: Text {
+                        verticalAlignment: Text.AlignVCenter
+                        color: "grey"
+                        text: control.text
+                        horizontalAlignment: Text.AlignHCenter
+                    }
+                }
+            }
+            */
+            ProgressBar {
+                id: progressbar
+                minimumValue: 0;
+                maximumValue: 100
+                anchors.left: installButton.left
+                anchors.right: installButton.right
+                anchors.top: installButton.bottom
+                anchors.topMargin: 4
+                value: manager.isDownloading ? manager.downloadProgress : 0
+                visible: manager.isDownloading && pluginList.activePlugin == model
+                style: ProgressBarStyle {
+                    background: Rectangle {
+                        color: "lightgray"
+                        implicitHeight: 6
+                    }
+                    progress: Rectangle {
+                        color: UM.Theme.getColor("primary")
+                    }
+                }
+            }
+
+        }
+    }
+}