From 5373e9a36d45bc6ede3eee27df8b246434cd2ec6 Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Mon, 29 Nov 2021 12:32:00 +0100 Subject: [PATCH 001/195] More generic way of showing the manage buttons Contributes to: CURA-8587 --- plugins/Marketplace/PackageModel.py | 40 +++++++++++++++++-- .../Marketplace/resources/qml/PackageCard.qml | 16 ++++---- 2 files changed, 45 insertions(+), 11 deletions(-) diff --git a/plugins/Marketplace/PackageModel.py b/plugins/Marketplace/PackageModel.py index 93d73bbc83..24f9671b34 100644 --- a/plugins/Marketplace/PackageModel.py +++ b/plugins/Marketplace/PackageModel.py @@ -1,11 +1,10 @@ # Copyright (c) 2021 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from PyQt5.QtCore import pyqtProperty, QObject +from PyQt5.QtCore import pyqtProperty, pyqtSignal, QObject from typing import Any, Dict, Optional -from UM.Util import parseBool - +from UM.Logger import Logger from UM.i18n import i18nCatalog # To translate placeholder names if data is not present. catalog = i18nCatalog("cura") @@ -28,6 +27,9 @@ class PackageModel(QObject): super().__init__(parent) self._package_id = package_data.get("package_id", "UnknownPackageId") self._package_type = package_data.get("package_type", "") + self._is_installed = package_data.get("is_installed", False) + self._is_active = package_data.get("is_active", False) + self._is_bundled = package_data.get("is_bundled", False) self._icon_url = package_data.get("icon_url", "") self._display_name = package_data.get("display_name", catalog.i18nc("@label:property", "Unknown Package")) tags = package_data.get("tags", []) @@ -96,3 +98,35 @@ class PackageModel(QObject): @pyqtProperty(str, constant = True) def sectionTitle(self) -> Optional[str]: return self._section_title + + enableManageButtonChanged = pyqtSignal() + + @pyqtProperty(str, notify = enableManageButtonChanged) + def enableManageButtonText(self): + if self._is_active: + return catalog.i18nc("@button", "Disable") + else: + return catalog.i18nc("@button", "Enable") + + @pyqtProperty(bool, notify = enableManageButtonChanged) + def enableManageButtonVisible(self): + return self._is_installed + + installManageButtonChanged = pyqtSignal() + + @pyqtProperty(str, notify = installManageButtonChanged) + def installManageButtonText(self): + if self._is_installed: + return catalog.i18nc("@button", "Uninstall") + else: + return catalog.i18nc("@button", "Install") + + @pyqtProperty(bool, notify = installManageButtonChanged) + def installManageButtonVisible(self): + return not self._is_bundled + + updateManageButtonChanged = pyqtSignal() + + @pyqtProperty(bool, notify = updateManageButtonChanged) + def updateManageButtonVisible(self): + return False # Todo: implement diff --git a/plugins/Marketplace/resources/qml/PackageCard.qml b/plugins/Marketplace/resources/qml/PackageCard.qml index 433b77a54d..d4f9d74246 100644 --- a/plugins/Marketplace/resources/qml/PackageCard.qml +++ b/plugins/Marketplace/resources/qml/PackageCard.qml @@ -302,26 +302,26 @@ Rectangle Cura.SecondaryButton { - id: disableButton + id: enableManageButton Layout.alignment: Qt.AlignTop - text: catalog.i18nc("@button", "Disable") - visible: false // not functional right now, also only when unfolding and required + text: packageData.enableManageButtonText + visible: packageData.enableManageButtonVisible } Cura.SecondaryButton { - id: uninstallButton + id: installManageButton Layout.alignment: Qt.AlignTop - text: catalog.i18nc("@button", "Uninstall") - visible: false // not functional right now, also only when unfolding and required + text: packageData.installManageButtonText + visible: packageData.installManageButtonVisible } Cura.PrimaryButton { - id: installButton + id: updateManageButton Layout.alignment: Qt.AlignTop text: catalog.i18nc("@button", "Update") // OR Download, if new! - visible: false // not functional right now, also only when unfolding and required + visible: packageData.updateManageButtonVisible } } From 3c225f1a73269373ac2b78311acc9a318475593a Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Mon, 29 Nov 2021 14:13:29 +0100 Subject: [PATCH 002/195] Fixed some layout issues with the Package Cards Contributes to: CURA-8587 --- plugins/Marketplace/resources/qml/PackageCard.qml | 8 +++++--- resources/themes/cura-light/theme.json | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/plugins/Marketplace/resources/qml/PackageCard.qml b/plugins/Marketplace/resources/qml/PackageCard.qml index d4f9d74246..040436375d 100644 --- a/plugins/Marketplace/resources/qml/PackageCard.qml +++ b/plugins/Marketplace/resources/qml/PackageCard.qml @@ -185,10 +185,11 @@ Rectangle } + // Description area Item { id: descriptionArea - height: descriptionLabel.height + height: childrenRect.height > descriptionLabel.height ? childrenRect.height : descriptionLabel.height anchors { top: titleBar.bottom @@ -269,6 +270,7 @@ Rectangle { bottom: parent.bottom left: packageItem.right + right: parent.right margins: UM.Theme.getSize("default_margin").height } spacing: UM.Theme.getSize("narrow_margin").width @@ -276,7 +278,7 @@ Rectangle Label { id: authorBy - Layout.alignment: Qt.AlignTop + Layout.alignment: Qt.AlignVCenter text: catalog.i18nc("@label", "By") font: UM.Theme.getFont("default") @@ -287,7 +289,7 @@ Rectangle { Layout.fillWidth: true Layout.preferredHeight: authorBy.height - Layout.alignment: Qt.AlignTop + Layout.alignment: Qt.AlignVCenter text: packageData.authorName textFont: UM.Theme.getFont("default_bold") diff --git a/resources/themes/cura-light/theme.json b/resources/themes/cura-light/theme.json index 81885faaf0..dede26db0b 100644 --- a/resources/themes/cura-light/theme.json +++ b/resources/themes/cura-light/theme.json @@ -554,7 +554,7 @@ "standard_list_lineheight": [1.5, 1.5], "standard_arrow": [1.0, 1.0], - "card": [25.0, 8.75], + "card": [25.0, 10], "card_icon": [6.0, 6.0], "card_tiny_icon": [1.5, 1.5], From a59307e10d8a82492354ec1356706fadc935c05d Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 26 Nov 2021 14:21:18 +0100 Subject: [PATCH 003/195] Also set correct size for package card title --- plugins/Marketplace/resources/qml/PackageCard.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/Marketplace/resources/qml/PackageCard.qml b/plugins/Marketplace/resources/qml/PackageCard.qml index 040436375d..e2d7b3c0b7 100644 --- a/plugins/Marketplace/resources/qml/PackageCard.qml +++ b/plugins/Marketplace/resources/qml/PackageCard.qml @@ -66,7 +66,7 @@ Rectangle left: packageItem.right right: parent.right top: parent.top - topMargin: UM.Theme.getSize("default_margin").height + topMargin: UM.Theme.getSize("narrow_margin").height leftMargin: UM.Theme.getSize("default_margin").width rightMargin:UM.Theme.getSize("thick_margin").width } @@ -74,7 +74,7 @@ Rectangle Label { text: packageData.displayName - font: UM.Theme.getFont("large_bold") + font: UM.Theme.getFont("medium_bold") color: UM.Theme.getColor("text") verticalAlignment: Text.AlignTop } From 34911380d2ed515c2bbc8bb8e5c4c123d37d7517 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 26 Nov 2021 14:14:43 +0100 Subject: [PATCH 004/195] Change fonts to default No idea why they were medium, but the design clearly shows that it should be the default font --- plugins/Marketplace/resources/qml/PackageCard.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/Marketplace/resources/qml/PackageCard.qml b/plugins/Marketplace/resources/qml/PackageCard.qml index e2d7b3c0b7..f57facf0c0 100644 --- a/plugins/Marketplace/resources/qml/PackageCard.qml +++ b/plugins/Marketplace/resources/qml/PackageCard.qml @@ -205,7 +205,7 @@ Rectangle property real lastLineWidth: 0; //Store the width of the last line, to properly position the elision. text: packageData.description - font: UM.Theme.getFont("medium") + font: UM.Theme.getFont("default") color: UM.Theme.getColor("text") maximumLineCount: 2 wrapMode: Text.Wrap @@ -330,6 +330,6 @@ Rectangle FontMetrics { id: fontMetrics - font: UM.Theme.getFont("medium") + font: UM.Theme.getFont("default") } } From 26a39f024013ff05ff0ba5d4b94634ca8e92e5c0 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Fri, 26 Nov 2021 16:04:23 +0100 Subject: [PATCH 005/195] Add a StackView around Marketplace to allow extra pages on top This allows a sort of full-screen pop-up to replace the entire Marketplace window contents, on top of the normal contents. The normal contents are kept as they are, but out of view. Contributes to issue CURA-8565. --- .../Marketplace/resources/qml/Marketplace.qml | 242 +++++++++--------- .../resources/qml/PackageDetails.qml | 10 + .../Marketplace/resources/qml/Packages.qml | 15 +- 3 files changed, 149 insertions(+), 118 deletions(-) create mode 100644 plugins/Marketplace/resources/qml/PackageDetails.qml diff --git a/plugins/Marketplace/resources/qml/Marketplace.qml b/plugins/Marketplace/resources/qml/Marketplace.qml index c04aa7eb6a..b293d21f92 100644 --- a/plugins/Marketplace/resources/qml/Marketplace.qml +++ b/plugins/Marketplace/resources/qml/Marketplace.qml @@ -42,143 +42,153 @@ Window anchors.fill: parent color: UM.Theme.getColor("main_background") - ColumnLayout + //The Marketplace can have a page in front of everything with package details. The stack view controls its visibility. + StackView { + id: contextStack anchors.fill: parent - spacing: UM.Theme.getSize("default_margin").height + initialItem: packageBrowse - // Page title. - Item + ColumnLayout { - Layout.preferredWidth: parent.width - Layout.preferredHeight: childrenRect.height + UM.Theme.getSize("default_margin").height + id: packageBrowse + anchors.fill: parent - Label + spacing: UM.Theme.getSize("default_margin").height + + // Page title. + Item { - id: pageTitle - anchors + Layout.preferredWidth: parent.width + Layout.preferredHeight: childrenRect.height + UM.Theme.getSize("default_margin").height + + Label { - left: parent.left - leftMargin: UM.Theme.getSize("default_margin").width - right: parent.right - rightMargin: UM.Theme.getSize("default_margin").width - bottom: parent.bottom - } - - font: UM.Theme.getFont("large") - color: UM.Theme.getColor("text") - text: content.item ? content.item.pageTitle: catalog.i18nc("@title", "Loading...") - } - } - - // Search & Top-Level Tabs - Item - { - Layout.preferredHeight: childrenRect.height - Layout.preferredWidth: parent.width - 2 * UM.Theme.getSize("thin_margin").width - RowLayout - { - width: parent.width - height: UM.Theme.getSize("button_icon").height + UM.Theme.getSize("default_margin").height - spacing: UM.Theme.getSize("thin_margin").width - - Rectangle - { - color: "transparent" - Layout.preferredHeight: parent.height - Layout.preferredWidth: searchBar.visible ? UM.Theme.getSize("thin_margin").width : 0 - Layout.fillWidth: ! searchBar.visible - } - - Cura.SearchBar - { - id: searchBar - Layout.preferredHeight: UM.Theme.getSize("button_icon").height - Layout.fillWidth: true - onTextEdited: searchStringChanged(text) - } - - // Page selection. - TabBar - { - id: pageSelectionTabBar - anchors.right: parent.right - height: UM.Theme.getSize("button_icon").height - spacing: 0 - background: Rectangle { color: "transparent" } - - PackageTypeTab + id: pageTitle + anchors { - id: pluginTabText - width: implicitWidth - text: catalog.i18nc("@button", "Plugins") - onClicked: + left: parent.left + leftMargin: UM.Theme.getSize("default_margin").width + right: parent.right + rightMargin: UM.Theme.getSize("default_margin").width + bottom: parent.bottom + } + + font: UM.Theme.getFont("large") + color: UM.Theme.getColor("text") + text: content.item ? content.item.pageTitle: catalog.i18nc("@title", "Loading...") + } + } + + // Search & Top-Level Tabs + Item + { + Layout.preferredHeight: childrenRect.height + Layout.preferredWidth: parent.width - 2 * UM.Theme.getSize("thin_margin").width + RowLayout + { + width: parent.width + height: UM.Theme.getSize("button_icon").height + UM.Theme.getSize("default_margin").height + spacing: UM.Theme.getSize("thin_margin").width + + Rectangle + { + color: "transparent" + Layout.preferredHeight: parent.height + Layout.preferredWidth: searchBar.visible ? UM.Theme.getSize("thin_margin").width : 0 + Layout.fillWidth: ! searchBar.visible + } + + Cura.SearchBar + { + id: searchBar + Layout.preferredHeight: UM.Theme.getSize("button_icon").height + Layout.fillWidth: true + onTextEdited: searchStringChanged(text) + } + + // Page selection. + TabBar + { + id: pageSelectionTabBar + anchors.right: parent.right + height: UM.Theme.getSize("button_icon").height + spacing: 0 + background: Rectangle { color: "transparent" } + + PackageTypeTab { - searchBar.text = "" - searchBar.visible = true - content.source = "Plugins.qml" + id: pluginTabText + width: implicitWidth + text: catalog.i18nc("@button", "Plugins") + onClicked: + { + searchBar.text = "" + searchBar.visible = true + content.source = "Plugins.qml" + } + } + PackageTypeTab + { + id: materialsTabText + width: implicitWidth + text: catalog.i18nc("@button", "Materials") + onClicked: + { + searchBar.text = "" + searchBar.visible = true + content.source = "Materials.qml" + } + } + ManagePackagesButton + { + onClicked: content.source = "ManagedPackages.qml" } } - PackageTypeTab - { - id: materialsTabText - width: implicitWidth - text: catalog.i18nc("@button", "Materials") - onClicked: - { - searchBar.text = "" - searchBar.visible = true - content.source = "Materials.qml" - } - } - ManagePackagesButton - { - onClicked: content.source = "ManagedPackages.qml" - } - } - TextMetrics - { - id: pluginTabTextMetrics - text: pluginTabText.text - font: pluginTabText.font + TextMetrics + { + id: pluginTabTextMetrics + text: pluginTabText.text + font: pluginTabText.font + } + TextMetrics + { + id: materialsTabTextMetrics + text: materialsTabText.text + font: materialsTabText.font + } } - TextMetrics - { - id: materialsTabTextMetrics - text: materialsTabText.text - font: materialsTabText.font - } } - } - - // Page contents. - Rectangle - { - Layout.preferredWidth: parent.width - Layout.fillHeight: true - color: UM.Theme.getColor("detail_background") // Page contents. - Loader + Rectangle { - id: content - anchors.fill: parent - anchors.margins: UM.Theme.getSize("default_margin").width - source: "Plugins.qml" + Layout.preferredWidth: parent.width + Layout.fillHeight: true + color: UM.Theme.getColor("detail_background") - Connections + // Page contents. + Loader { - target: content - function onLoaded() + id: content + anchors.fill: parent + anchors.margins: UM.Theme.getSize("default_margin").width + source: "Plugins.qml" + + Connections { - pageTitle.text = content.item.pageTitle - searchStringChanged.connect(handleSearchStringChanged) - } - function handleSearchStringChanged(new_search) - { - content.item.model.searchString = new_search + target: content + function onLoaded() + { + pageTitle.text = content.item.pageTitle + searchStringChanged.connect(handleSearchStringChanged) + } + function handleSearchStringChanged(new_search) + { + content.item.model.searchString = new_search + } } } } diff --git a/plugins/Marketplace/resources/qml/PackageDetails.qml b/plugins/Marketplace/resources/qml/PackageDetails.qml new file mode 100644 index 0000000000..4ad376e490 --- /dev/null +++ b/plugins/Marketplace/resources/qml/PackageDetails.qml @@ -0,0 +1,10 @@ +// Copyright (c) 2021 Ultimaker B.V. +// Cura is released under the terms of the LGPLv3 or higher. + +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +Label +{ + text: "Test!" +} \ No newline at end of file diff --git a/plugins/Marketplace/resources/qml/Packages.qml b/plugins/Marketplace/resources/qml/Packages.qml index 79b4bf23a5..def8d40acb 100644 --- a/plugins/Marketplace/resources/qml/Packages.qml +++ b/plugins/Marketplace/resources/qml/Packages.qml @@ -62,9 +62,20 @@ ListView } } - delegate: PackageCard + delegate: MouseArea { - packageData: model.package + width: parent.width + height: childrenRect.height + + onClicked: + { + contextStack.push(Qt.resolvedUrl("PackageDetails.qml")) + } + + PackageCard + { + packageData: model.package + } } //Wrapper item to add spacing between content and footer. From 27d9118d177c32ed4c0d422344d4b6484000ab07 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Fri, 26 Nov 2021 16:48:02 +0100 Subject: [PATCH 006/195] Basis of header line I think the icon on the button is too small, but that's not currently configurable. Will have to look into that. Contributes to issue CURA-8565. --- .../resources/qml/PackageDetails.qml | 41 ++++++++++++++++++- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/plugins/Marketplace/resources/qml/PackageDetails.qml b/plugins/Marketplace/resources/qml/PackageDetails.qml index 4ad376e490..0dd196c26b 100644 --- a/plugins/Marketplace/resources/qml/PackageDetails.qml +++ b/plugins/Marketplace/resources/qml/PackageDetails.qml @@ -3,8 +3,45 @@ import QtQuick 2.15 import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.3 -Label +import Cura 1.0 as Cura +import UM 1.0 as UM + +Item { - text: "Test!" + Column + { + anchors.fill: parent + anchors.margins: UM.Theme.getSize("default_margin").width + + RowLayout + { + spacing: UM.Theme.getSize("default_margin").width + + Cura.SecondaryButton + { + Layout.alignment: Qt.AlignVCenter + Layout.preferredHeight: UM.Theme.getSize("action_button").height + Layout.preferredWidth: height + + onClicked: contextStack.pop() //Remove this page, returning to the main package list or whichever thing is beneath it. + + tooltip: catalog.i18nc("@button:tooltip", "Back") + toolTipContentAlignment: Cura.ToolTip.ContentAlignment.AlignRight + iconSource: UM.Theme.getIcon("ArrowLeft") + leftPadding: UM.Theme.getSize("narrow_margin").width + rightPadding: leftPadding + } + + Label + { + Layout.alignment: Qt.AlignVCenter + + text: "Install Plug-ins" //TODO: Depend on package type, and translate. + font: UM.Theme.getFont("large") + color: UM.Theme.getColor("text") + } + } + } } \ No newline at end of file From 04501f788c0e6bb6db30f46e5155759b6caa2031 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Mon, 29 Nov 2021 11:57:12 +0100 Subject: [PATCH 007/195] Allow changing the icon size But use the same default as what was previously hard-coded. Now we can have buttons with non-standard icon sizes then, e.g. if the button size itself is also non-standard. Contributes to issue CURA-8565. --- resources/qml/ActionButton.qml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/resources/qml/ActionButton.qml b/resources/qml/ActionButton.qml index 62bea5df3b..f129e8c976 100644 --- a/resources/qml/ActionButton.qml +++ b/resources/qml/ActionButton.qml @@ -15,6 +15,7 @@ Button property bool isIconOnRightSide: false property alias iconSource: buttonIconLeft.source + property real iconSize: UM.Theme.getSize("action_button_icon").height property alias textFont: buttonText.font property alias cornerRadius: backgroundRect.radius property alias tooltip: tooltip.tooltipText @@ -158,7 +159,7 @@ Button { id: buttonIconRight source: buttonIconLeft.source - height: visible ? UM.Theme.getSize("action_button_icon").height : 0 + height: visible ? button.iconSize : 0 width: visible ? height : 0 sourceSize.width: width sourceSize.height: height From 0cd8798aff0133ee689d8105448f3d98f07518bf Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Mon, 29 Nov 2021 11:59:46 +0100 Subject: [PATCH 008/195] Remove superfluous anchors.fill This is already set by the StackView, which (logically) requires that the children fill the entire space taken by the StackView. Contributes to issue CURA-8565. --- plugins/Marketplace/resources/qml/Marketplace.qml | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/Marketplace/resources/qml/Marketplace.qml b/plugins/Marketplace/resources/qml/Marketplace.qml index b293d21f92..951de77f19 100644 --- a/plugins/Marketplace/resources/qml/Marketplace.qml +++ b/plugins/Marketplace/resources/qml/Marketplace.qml @@ -53,7 +53,6 @@ Window ColumnLayout { id: packageBrowse - anchors.fill: parent spacing: UM.Theme.getSize("default_margin").height From d4ebf3baa06c36cca54b92cf149ffa7c1425f68f Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Mon, 29 Nov 2021 12:03:03 +0100 Subject: [PATCH 009/195] Also use proper icon size for left icon Almost forgot! Contributes to issue CURA-8565. --- resources/qml/ActionButton.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/qml/ActionButton.qml b/resources/qml/ActionButton.qml index f129e8c976..942c0ee578 100644 --- a/resources/qml/ActionButton.qml +++ b/resources/qml/ActionButton.qml @@ -110,7 +110,7 @@ Button { id: buttonIconLeft source: "" - height: visible ? UM.Theme.getSize("action_button_icon").height : 0 + height: visible ? button.iconSize : 0 width: visible ? height : 0 sourceSize.width: width sourceSize.height: height From cb7b9b319367508771f6185450716979104d51aa Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Mon, 29 Nov 2021 12:03:29 +0100 Subject: [PATCH 010/195] Increase size of icon to fit button exactly Contributes to issue CURA-8565. --- plugins/Marketplace/resources/qml/PackageDetails.qml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/Marketplace/resources/qml/PackageDetails.qml b/plugins/Marketplace/resources/qml/PackageDetails.qml index 0dd196c26b..6cf29e9f45 100644 --- a/plugins/Marketplace/resources/qml/PackageDetails.qml +++ b/plugins/Marketplace/resources/qml/PackageDetails.qml @@ -29,9 +29,10 @@ Item tooltip: catalog.i18nc("@button:tooltip", "Back") toolTipContentAlignment: Cura.ToolTip.ContentAlignment.AlignRight - iconSource: UM.Theme.getIcon("ArrowLeft") leftPadding: UM.Theme.getSize("narrow_margin").width rightPadding: leftPadding + iconSource: UM.Theme.getIcon("ArrowLeft") + iconSize: height - leftPadding * 2 } Label From d31079b7aa54f5080d55830b462553351a6f02fb Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Mon, 29 Nov 2021 12:57:36 +0100 Subject: [PATCH 011/195] Split page in header and contents This requires a small refactor here. Contributes to issue CURA-8565. --- .../resources/qml/PackageDetails.qml | 71 ++++++++++++------- 1 file changed, 45 insertions(+), 26 deletions(-) diff --git a/plugins/Marketplace/resources/qml/PackageDetails.qml b/plugins/Marketplace/resources/qml/PackageDetails.qml index 6cf29e9f45..4b4b2123c7 100644 --- a/plugins/Marketplace/resources/qml/PackageDetails.qml +++ b/plugins/Marketplace/resources/qml/PackageDetails.qml @@ -10,39 +10,58 @@ import UM 1.0 as UM Item { - Column + RowLayout { - anchors.fill: parent - anchors.margins: UM.Theme.getSize("default_margin").width - - RowLayout + id: header + anchors { - spacing: UM.Theme.getSize("default_margin").width + top: parent.top + topMargin: UM.Theme.getSize("default_margin").height + left: parent.left + leftMargin: UM.Theme.getSize("default_margin").width + right: parent.right + rightMargin: anchors.leftMargin + } - Cura.SecondaryButton - { - Layout.alignment: Qt.AlignVCenter - Layout.preferredHeight: UM.Theme.getSize("action_button").height - Layout.preferredWidth: height + spacing: UM.Theme.getSize("default_margin").width - onClicked: contextStack.pop() //Remove this page, returning to the main package list or whichever thing is beneath it. + Cura.SecondaryButton + { + Layout.alignment: Qt.AlignVCenter + Layout.preferredHeight: UM.Theme.getSize("action_button").height + Layout.preferredWidth: height - tooltip: catalog.i18nc("@button:tooltip", "Back") - toolTipContentAlignment: Cura.ToolTip.ContentAlignment.AlignRight - leftPadding: UM.Theme.getSize("narrow_margin").width - rightPadding: leftPadding - iconSource: UM.Theme.getIcon("ArrowLeft") - iconSize: height - leftPadding * 2 - } + onClicked: contextStack.pop() //Remove this page, returning to the main package list or whichever thing is beneath it. - Label - { - Layout.alignment: Qt.AlignVCenter + tooltip: catalog.i18nc("@button:tooltip", "Back") + toolTipContentAlignment: Cura.ToolTip.ContentAlignment.AlignRight + leftPadding: UM.Theme.getSize("narrow_margin").width + rightPadding: leftPadding + iconSource: UM.Theme.getIcon("ArrowLeft") + iconSize: height - leftPadding * 2 + } - text: "Install Plug-ins" //TODO: Depend on package type, and translate. - font: UM.Theme.getFont("large") - color: UM.Theme.getColor("text") - } + Label + { + Layout.alignment: Qt.AlignVCenter + Layout.fillWidth: true + + text: "Install Plug-ins" //TODO: Depend on package type, and translate. + font: UM.Theme.getFont("large") + color: UM.Theme.getColor("text") } } + + Rectangle + { + anchors + { + top: header.bottom + topMargin: UM.Theme.getSize("default_margin").height + left: parent.left + right: parent.right + bottom: parent.bottom + } + color: UM.Theme.getColor("detail_background") + } } \ No newline at end of file From 5edd8302103621e1a821595a66951d051d3caf80 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Mon, 29 Nov 2021 14:50:48 +0100 Subject: [PATCH 012/195] Add package card to detail page The card has the wrong layout, but it's a start. The data is communicated in any case. Contributes to issue CURA-8565. --- .../Marketplace/resources/qml/PackageDetails.qml | 8 ++++++++ plugins/Marketplace/resources/qml/Packages.qml | 16 ++++++++++++++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/plugins/Marketplace/resources/qml/PackageDetails.qml b/plugins/Marketplace/resources/qml/PackageDetails.qml index 4b4b2123c7..49e4d97633 100644 --- a/plugins/Marketplace/resources/qml/PackageDetails.qml +++ b/plugins/Marketplace/resources/qml/PackageDetails.qml @@ -10,6 +10,9 @@ import UM 1.0 as UM Item { + id: detailPage + property var packageData: packages.selectedPackage + RowLayout { id: header @@ -63,5 +66,10 @@ Item bottom: parent.bottom } color: UM.Theme.getColor("detail_background") + + PackageCard + { + packageData: detailPage.packageData + } } } \ No newline at end of file diff --git a/plugins/Marketplace/resources/qml/Packages.qml b/plugins/Marketplace/resources/qml/Packages.qml index def8d40acb..89254f2526 100644 --- a/plugins/Marketplace/resources/qml/Packages.qml +++ b/plugins/Marketplace/resources/qml/Packages.qml @@ -9,9 +9,10 @@ import UM 1.4 as UM ListView { id: packages + width: parent.width property string pageTitle - width: parent.width + property var selectedPackage clip: true @@ -69,7 +70,8 @@ ListView onClicked: { - contextStack.push(Qt.resolvedUrl("PackageDetails.qml")) + packages.selectedPackage = model.package; + contextStack.push(packageDetailsComponent); } PackageCard @@ -78,6 +80,16 @@ ListView } } + Component + { + id: packageDetailsComponent + + PackageDetails + { + packageData: packages.selectedPackage + } + } + //Wrapper item to add spacing between content and footer. footer: Item { From 0069182c6b8ee09cbc2fc7cd2f7dde09e9357a47 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Mon, 29 Nov 2021 16:11:15 +0100 Subject: [PATCH 013/195] Set position and width of card in details page This means that the card itself shouldn't specify a width. It should get a width from how it's used. Contributes to issue CURA-8565. --- plugins/Marketplace/resources/qml/PackageCard.qml | 1 - plugins/Marketplace/resources/qml/PackageDetails.qml | 9 +++++++++ plugins/Marketplace/resources/qml/Packages.qml | 1 + 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/plugins/Marketplace/resources/qml/PackageCard.qml b/plugins/Marketplace/resources/qml/PackageCard.qml index f57facf0c0..448be8ff75 100644 --- a/plugins/Marketplace/resources/qml/PackageCard.qml +++ b/plugins/Marketplace/resources/qml/PackageCard.qml @@ -12,7 +12,6 @@ Rectangle { property var packageData - width: parent ? parent.width - UM.Theme.getSize("default_margin").width - UM.Theme.getSize("narrow_margin").width: 0 height: UM.Theme.getSize("card").height color: UM.Theme.getColor("main_background") radius: UM.Theme.getSize("default_radius").width diff --git a/plugins/Marketplace/resources/qml/PackageDetails.qml b/plugins/Marketplace/resources/qml/PackageDetails.qml index 49e4d97633..7db46300a8 100644 --- a/plugins/Marketplace/resources/qml/PackageDetails.qml +++ b/plugins/Marketplace/resources/qml/PackageDetails.qml @@ -70,6 +70,15 @@ Item PackageCard { packageData: detailPage.packageData + anchors + { + left: parent.left + leftMargin: UM.Theme.getSize("default_margin").width + right: parent.right + rightMargin: anchors.leftMargin + top: parent.top + topMargin: UM.Theme.getSize("default_margin").height + } } } } \ No newline at end of file diff --git a/plugins/Marketplace/resources/qml/Packages.qml b/plugins/Marketplace/resources/qml/Packages.qml index 89254f2526..47318d5a72 100644 --- a/plugins/Marketplace/resources/qml/Packages.qml +++ b/plugins/Marketplace/resources/qml/Packages.qml @@ -77,6 +77,7 @@ ListView PackageCard { packageData: model.package + width: parent.width - UM.Theme.getSize("default_margin").width - UM.Theme.getSize("narrow_margin").width } } From 76e43759c7cf51b0a9d01a1ddd9309223b041e4c Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Mon, 29 Nov 2021 16:32:55 +0100 Subject: [PATCH 014/195] Restore check for parent before getting its width Delegates that are outside of the viewport may have no parent any more. Don't attempt to find their parents' width. Contributes to issue CURA-8565. --- plugins/Marketplace/resources/qml/Packages.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/Marketplace/resources/qml/Packages.qml b/plugins/Marketplace/resources/qml/Packages.qml index 47318d5a72..49fca00fdb 100644 --- a/plugins/Marketplace/resources/qml/Packages.qml +++ b/plugins/Marketplace/resources/qml/Packages.qml @@ -65,7 +65,7 @@ ListView delegate: MouseArea { - width: parent.width + width: parent ? parent.width : 0 height: childrenRect.height onClicked: From 0b1b4ec01b08c47bcd0304fb9bbbcced14d0f7c3 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Mon, 29 Nov 2021 16:38:24 +0100 Subject: [PATCH 015/195] Communicate to PackageCard whether it is a detailed card or not If it is detailed, it currently hides the short description. That is not quite enough, but we'll expand that behaviour. Contributes to issue CURA-8565. --- plugins/Marketplace/resources/qml/PackageCard.qml | 7 ++++--- plugins/Marketplace/resources/qml/PackageDetails.qml | 4 +++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/plugins/Marketplace/resources/qml/PackageCard.qml b/plugins/Marketplace/resources/qml/PackageCard.qml index 448be8ff75..b8c053b5bb 100644 --- a/plugins/Marketplace/resources/qml/PackageCard.qml +++ b/plugins/Marketplace/resources/qml/PackageCard.qml @@ -11,6 +11,7 @@ import Cura 1.6 as Cura Rectangle { property var packageData + property bool expanded: false height: UM.Theme.getSize("card").height color: UM.Theme.getColor("main_background") @@ -21,7 +22,7 @@ Rectangle State { name: "Folded" - when: true // TODO + when: !expanded PropertyChanges { target: descriptionArea @@ -30,8 +31,8 @@ Rectangle }, State { - name: "Header" - when: false // TODO + name: "Expanded" + when: expanded PropertyChanges { target: descriptionArea diff --git a/plugins/Marketplace/resources/qml/PackageDetails.qml b/plugins/Marketplace/resources/qml/PackageDetails.qml index 7db46300a8..c1ef765a14 100644 --- a/plugins/Marketplace/resources/qml/PackageDetails.qml +++ b/plugins/Marketplace/resources/qml/PackageDetails.qml @@ -69,7 +69,6 @@ Item PackageCard { - packageData: detailPage.packageData anchors { left: parent.left @@ -79,6 +78,9 @@ Item top: parent.top topMargin: UM.Theme.getSize("default_margin").height } + + packageData: detailPage.packageData + expanded: true } } } \ No newline at end of file From 39488823b124d281a5b9ff09d8c7fcaaebd73b41 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Mon, 29 Nov 2021 18:37:54 +0100 Subject: [PATCH 016/195] Add forgotten ArrowLeft icon I've been using this for a while. It should've been included with the commit that added the button, but oh well. Contributes to issue CURA-8565. --- resources/themes/cura-light/icons/default/ArrowLeft.svg | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 resources/themes/cura-light/icons/default/ArrowLeft.svg diff --git a/resources/themes/cura-light/icons/default/ArrowLeft.svg b/resources/themes/cura-light/icons/default/ArrowLeft.svg new file mode 100644 index 0000000000..d722b8ae8d --- /dev/null +++ b/resources/themes/cura-light/icons/default/ArrowLeft.svg @@ -0,0 +1,3 @@ + + + From b1af7ad203f766ce663ea2450b96397b088bafb8 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Mon, 29 Nov 2021 18:46:56 +0100 Subject: [PATCH 017/195] Add button to visit plug-in website There are a lot of buttons leading to websites now: An arrow leading to the author website. An author name leading to the author website. A 'read more' label leading to the plug-in website and this new button leading to the plug-in website. Maybe we should raise this with the designer. Contributes to issue CURA-8565. --- resources/themes/cura-light/icons/default/Globe.svg | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 resources/themes/cura-light/icons/default/Globe.svg diff --git a/resources/themes/cura-light/icons/default/Globe.svg b/resources/themes/cura-light/icons/default/Globe.svg new file mode 100644 index 0000000000..4d955e9615 --- /dev/null +++ b/resources/themes/cura-light/icons/default/Globe.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file From 2b419a23796bfede7b701bfaf235d128eb74b9f3 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Mon, 29 Nov 2021 19:04:30 +0100 Subject: [PATCH 018/195] Make package detail page scroll if details are too long Some plug-ins could have very long descriptions now. We show all of it, but that could go off the screen in theory. This makes the content scrollable if it goes off the screen. Contributes to issue CURA-8565. --- .../resources/qml/PackageDetails.qml | 34 ++++++++++++------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/plugins/Marketplace/resources/qml/PackageDetails.qml b/plugins/Marketplace/resources/qml/PackageDetails.qml index c1ef765a14..1ccb3dc4fd 100644 --- a/plugins/Marketplace/resources/qml/PackageDetails.qml +++ b/plugins/Marketplace/resources/qml/PackageDetails.qml @@ -67,20 +67,30 @@ Item } color: UM.Theme.getColor("detail_background") - PackageCard + ScrollView { - anchors - { - left: parent.left - leftMargin: UM.Theme.getSize("default_margin").width - right: parent.right - rightMargin: anchors.leftMargin - top: parent.top - topMargin: UM.Theme.getSize("default_margin").height - } + anchors.fill: parent - packageData: detailPage.packageData - expanded: true + clip: true //Need to clip, not for the bottom (which is off the window) but for the top (which would overlap the header). + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + contentHeight: expandedPackageCard.height + UM.Theme.getSize("default_margin").height * 2 + + PackageCard + { + id: expandedPackageCard + anchors + { + left: parent.left + leftMargin: UM.Theme.getSize("default_margin").width + right: parent.right + rightMargin: anchors.leftMargin + top: parent.top + topMargin: UM.Theme.getSize("default_margin").height + } + + packageData: detailPage.packageData + expanded: true + } } } } \ No newline at end of file From 02d74b4226ba26cdd7c68ffebfee0e81d9a03fd8 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Mon, 29 Nov 2021 19:41:25 +0100 Subject: [PATCH 019/195] Make header of detail page depend on header of origin It's the same as the list of packages you came from, now. Contributes to issue CURA-8565. --- plugins/Marketplace/resources/qml/PackageDetails.qml | 3 ++- plugins/Marketplace/resources/qml/Packages.qml | 3 +-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/Marketplace/resources/qml/PackageDetails.qml b/plugins/Marketplace/resources/qml/PackageDetails.qml index 1ccb3dc4fd..fdf1c8f92c 100644 --- a/plugins/Marketplace/resources/qml/PackageDetails.qml +++ b/plugins/Marketplace/resources/qml/PackageDetails.qml @@ -12,6 +12,7 @@ Item { id: detailPage property var packageData: packages.selectedPackage + property string title: catalog.i18nc("@header", "Package details") RowLayout { @@ -49,7 +50,7 @@ Item Layout.alignment: Qt.AlignVCenter Layout.fillWidth: true - text: "Install Plug-ins" //TODO: Depend on package type, and translate. + text: detailPage.title font: UM.Theme.getFont("large") color: UM.Theme.getColor("text") } diff --git a/plugins/Marketplace/resources/qml/Packages.qml b/plugins/Marketplace/resources/qml/Packages.qml index 49fca00fdb..75207bc2e6 100644 --- a/plugins/Marketplace/resources/qml/Packages.qml +++ b/plugins/Marketplace/resources/qml/Packages.qml @@ -19,8 +19,6 @@ ListView Component.onCompleted: model.updatePackages() Component.onDestruction: model.abortUpdating() - //ScrollBar.vertical.policy: ScrollBar.AlwaysOff - spacing: UM.Theme.getSize("default_margin").height section.property: "package.sectionTitle" @@ -88,6 +86,7 @@ ListView PackageDetails { packageData: packages.selectedPackage + title: packages.pageTitle } } From eb156f114c7d596bf778c48dfee7ff9ddfb7bcff Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Tue, 30 Nov 2021 12:03:15 +0100 Subject: [PATCH 020/195] Introduce a ManageButton The state and styling of this coupled with the available options. Discussed with UX that primary state: Install, Enable, Update while secondary states are: Uninstall, Disable Each primary/secondary state also has a busy state, with the verb and spinner. Contributes to: CURA-8587 --- plugins/Marketplace/PackageModel.py | 55 ++++--- .../resources/qml/ManageButton.qml | 154 ++++++++++++++++++ 2 files changed, 187 insertions(+), 22 deletions(-) create mode 100644 plugins/Marketplace/resources/qml/ManageButton.qml diff --git a/plugins/Marketplace/PackageModel.py b/plugins/Marketplace/PackageModel.py index 24f9671b34..4a4973a2dc 100644 --- a/plugins/Marketplace/PackageModel.py +++ b/plugins/Marketplace/PackageModel.py @@ -99,34 +99,45 @@ class PackageModel(QObject): def sectionTitle(self) -> Optional[str]: return self._section_title - enableManageButtonChanged = pyqtSignal() + isInstalledChanged = pyqtSignal() - @pyqtProperty(str, notify = enableManageButtonChanged) - def enableManageButtonText(self): - if self._is_active: - return catalog.i18nc("@button", "Disable") - else: - return catalog.i18nc("@button", "Enable") - - @pyqtProperty(bool, notify = enableManageButtonChanged) - def enableManageButtonVisible(self): + @pyqtProperty(bool, notify = isInstalledChanged) + def isInstalled(self): return self._is_installed - installManageButtonChanged = pyqtSignal() + isEnabledChanged = pyqtSignal() - @pyqtProperty(str, notify = installManageButtonChanged) - def installManageButtonText(self): + @pyqtProperty(bool, notify = isEnabledChanged) + def isEnabled(self): + return self._is_active + + manageEnableStateChanged = pyqtSignal() + + @pyqtProperty(str, notify = manageEnableStateChanged) + def manageEnableState(self): + # TODO: Handle manual installed packages if self._is_installed: - return catalog.i18nc("@button", "Uninstall") + if self._is_active: + return "secondary" + else: + return "primary" else: - return catalog.i18nc("@button", "Install") + return "hidden" - @pyqtProperty(bool, notify = installManageButtonChanged) - def installManageButtonVisible(self): - return not self._is_bundled + manageInstallStateChanged = pyqtSignal() - updateManageButtonChanged = pyqtSignal() + @pyqtProperty(str, notify = manageInstallStateChanged) + def manageInstallState(self): + if self._is_installed: + if self._is_bundled: + return "hidden" + else: + return "secondary" + else: + return "primary" - @pyqtProperty(bool, notify = updateManageButtonChanged) - def updateManageButtonVisible(self): - return False # Todo: implement + manageUpdateStateChanged = pyqtSignal() + + @pyqtProperty(str, notify = manageUpdateStateChanged) + def manageUpdateState(self): + return "hidden" # TODO: implement diff --git a/plugins/Marketplace/resources/qml/ManageButton.qml b/plugins/Marketplace/resources/qml/ManageButton.qml new file mode 100644 index 0000000000..e58347124a --- /dev/null +++ b/plugins/Marketplace/resources/qml/ManageButton.qml @@ -0,0 +1,154 @@ +// Copyright (c) 2021 Ultimaker B.V. +// Cura is released under the terms of the LGPLv3 or higher. + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.1 + +import UM 1.6 as UM +import Cura 1.6 as Cura + +RowLayout +{ + id: manageButton + property alias primaryText: primaryButton.text + property alias secondaryText: secondaryButton.text + property string busyPrimaryText: busyMessageText.text + property string busySecondaryText: busyMessageText.text + property string mainState: "primary" + + state: mainState + + Cura.PrimaryButton + { + id: primaryButton + visible: false + + onClicked: + { + manageButton.state = "busy" + } + } + + Cura.SecondaryButton + { + id: secondaryButton + visible: false + + onClicked: + { + manageButton.state = "busy" + } + } + + Item + { + id: busyMessage + visible: false + height: UM.Theme.getSize("action_button").height + width: childrenRect.width + + BusyIndicator + { + id: busyIndicator + visible: parent.visible + width: height + anchors.left: parent.left + anchors.top: parent.top + anchors.bottom: parent.bottom + + palette.dark: UM.Theme.getColor("text") + + RotationAnimator + { + target: busyIndicator.contentItem + running: busyIndicator.visible && busyIndicator.running + from: 0 + to: 360 + loops: Animation.Infinite + duration: 2500 + } + } + Label + { + id: busyMessageText + visible: parent.visible + text: manageButton.mainState == "primary" ? manageButton.busyPrimaryText : manageButton.busySecondaryText + anchors.left: busyIndicator.right + anchors.verticalCenter: parent.verticalCenter + + font: UM.Theme.getFont("default") + color: UM.Theme.getColor("text") + } + } + + states: + [ + State + { + name: "primary" + PropertyChanges + { + target: primaryButton + visible: true + } + PropertyChanges + { + target: secondaryButton + visible: false + } + PropertyChanges + { + target: busyMessage + visible: false + } + }, + State + { + name: "secondary" + PropertyChanges + { + target: primaryButton + visible: false + } + PropertyChanges + { + target: secondaryButton + visible: true + } + PropertyChanges + { + target: busyMessage + visible: false + } + }, + State + { + name: "hidden" + PropertyChanges + { + target: manageButton + visible: false + } + }, + State + { + name: "busy" + PropertyChanges + { + target: primaryButton + visible: false + } + PropertyChanges + { + target: secondaryButton + visible: false + } + PropertyChanges + { + target: busyMessage + visible: true + } + } + ] +} From 6514fdf9f294b7822370e57ba446170710c7afc3 Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Tue, 30 Nov 2021 15:40:18 +0100 Subject: [PATCH 021/195] Check the server if a package can be updated The server is queried using the local packages_id and version to determine if they can be updated. The Manage button state is set accordingly. This is done in an async way to keep the UI responsive A.t.m I'm not sure if I might need to move this logic out, since we also need to make this query when check periodically for updates. when the list is not shown. But that would also entail creating the installed packages list before the Manage Packages tab is ever created in the Marketplace. Contributes to: CURA-8587 --- plugins/Marketplace/LocalPackageList.py | 33 +++++++++++++++++++++++- plugins/Marketplace/Marketplace.py | 1 + plugins/Marketplace/PackageList.py | 8 ++++++ plugins/Marketplace/RemotePackageList.py | 8 +----- 4 files changed, 42 insertions(+), 8 deletions(-) diff --git a/plugins/Marketplace/LocalPackageList.py b/plugins/Marketplace/LocalPackageList.py index 6acbaa8500..059dd9bbef 100644 --- a/plugins/Marketplace/LocalPackageList.py +++ b/plugins/Marketplace/LocalPackageList.py @@ -8,11 +8,14 @@ if TYPE_CHECKING: from PyQt5.QtCore import QObject from UM.i18n import i18nCatalog +from UM.TaskManagement.HttpRequestManager import HttpRequestManager +from UM.Logger import Logger from cura.CuraApplication import CuraApplication from .PackageList import PackageList -from .PackageModel import PackageModel # The contents of this list. +from .PackageModel import PackageModel +from . import Marketplace catalog = i18nCatalog("cura") @@ -55,6 +58,7 @@ class LocalPackageList(PackageList): for the sections are sorted alphabetically on the display name. These sorted sections are then added to the items """ package_info = list(self._allPackageInfo()) + self.checkForUpdates(package_info) sorted_sections: List[Dict[str, PackageModel]] = [] for section in self._getSections(): packages = filter(lambda p: p.sectionTitle == section, package_info) @@ -91,3 +95,30 @@ class LocalPackageList(PackageList): package_type = package_info["package_type"] section_title = self.PACKAGE_SECTION_HEADER[bundled_or_installed][package_type] return PackageModel(package_info, section_title = section_title, parent = self) + + def checkForUpdates(self, packages: List[PackageModel]): + installed_packages = "installed_packages=".join([f"{package.packageId}:{package.packageVersion}&" for package in packages]) + request_url = f"{Marketplace.PACKAGE_UPDATES_URL}?installed_packages={installed_packages[:-1]}" + + self._ongoing_request = HttpRequestManager.getInstance().get( + request_url, + scope = self._scope, + callback = self._parseResponse + ) + return [] + + def _parseResponse(self, reply: "QNetworkReply") -> None: + """ + Parse the response from the package list API request which can update. + + :param reply: A reply containing information about a number of packages. + """ + response_data = HttpRequestManager.readJSON(reply) + if "data" not in response_data: + Logger.error(f"Could not interpret the server's response. Missing 'data' or 'links' from response data. Keys in response: {response_data.keys()}") + self.setErrorMessage(catalog.i18nc("@info:error", "Could not interpret the server's response.")) + return + + for package_data in response_data["data"]: + index = self.find("package", package_data["package_id"]) + self.getItem(index)["package"].canUpdate = True diff --git a/plugins/Marketplace/Marketplace.py b/plugins/Marketplace/Marketplace.py index 18d80d6e68..1b98503969 100644 --- a/plugins/Marketplace/Marketplace.py +++ b/plugins/Marketplace/Marketplace.py @@ -21,6 +21,7 @@ if TYPE_CHECKING: ROOT_URL = f"{UltimakerCloudConstants.CuraCloudAPIRoot}/cura-packages/v{UltimakerCloudConstants.CuraCloudAPIVersion}/cura/v{CuraSDKVersion}" # Root of all Marketplace API requests. PACKAGES_URL = f"{ROOT_URL}/packages" # URL to use for requesting the list of packages. +PACKAGE_UPDATES_URL = f"{PACKAGES_URL}/package-updates" # URL to use for requesting the list of packages that can be updated. class Marketplace(Extension): diff --git a/plugins/Marketplace/PackageList.py b/plugins/Marketplace/PackageList.py index 8171d168f2..d3da8c826a 100644 --- a/plugins/Marketplace/PackageList.py +++ b/plugins/Marketplace/PackageList.py @@ -6,6 +6,11 @@ from typing import Optional, TYPE_CHECKING from UM.i18n import i18nCatalog from UM.Qt.ListModel import ListModel +from UM.TaskManagement.HttpRequestScope import JsonDecoratorScope # To request JSON responses from the API. +from UM.TaskManagement.HttpRequestManager import HttpRequestData # To request the package list from the API. + +from cura.CuraApplication import CuraApplication +from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope # To make requests to the Ultimaker API with correct authorization. if TYPE_CHECKING: from PyQt5.QtCore import QObject @@ -27,6 +32,9 @@ class PackageList(ListModel): self._has_more = False self._has_footer = True + self._ongoing_request: Optional[HttpRequestData] = None + self._scope = JsonDecoratorScope(UltimakerCloudScope(CuraApplication.getInstance())) + @pyqtSlot() def updatePackages(self) -> None: """ A Qt slot which will update the List from a source. Actual implementation should be done in the child class""" diff --git a/plugins/Marketplace/RemotePackageList.py b/plugins/Marketplace/RemotePackageList.py index e7df498fbf..4fedde9ff0 100644 --- a/plugins/Marketplace/RemotePackageList.py +++ b/plugins/Marketplace/RemotePackageList.py @@ -5,12 +5,9 @@ from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot from PyQt5.QtNetwork import QNetworkReply from typing import Optional, TYPE_CHECKING -from cura.CuraApplication import CuraApplication -from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope # To make requests to the Ultimaker API with correct authorization. from UM.i18n import i18nCatalog from UM.Logger import Logger -from UM.TaskManagement.HttpRequestManager import HttpRequestManager, HttpRequestData # To request the package list from the API. -from UM.TaskManagement.HttpRequestScope import JsonDecoratorScope # To request JSON responses from the API. +from UM.TaskManagement.HttpRequestManager import HttpRequestManager # To request the package list from the API. from . import Marketplace # To get the list of packages. Imported this way to prevent circular imports. from .PackageList import PackageList @@ -28,9 +25,6 @@ class RemotePackageList(PackageList): def __init__(self, parent: Optional["QObject"] = None) -> None: super().__init__(parent) - self._ongoing_request: Optional[HttpRequestData] = None - self._scope = JsonDecoratorScope(UltimakerCloudScope(CuraApplication.getInstance())) - self._package_type_filter = "" self._requested_search_string = "" self._current_search_string = "" From caa8da69b4037edd7390c18704f2abb7f3855627 Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Wed, 1 Dec 2021 16:46:37 +0100 Subject: [PATCH 022/195] Fix section mismatch after package order changed Contributes to: CURA-8587 --- plugins/Marketplace/resources/qml/Packages.qml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/plugins/Marketplace/resources/qml/Packages.qml b/plugins/Marketplace/resources/qml/Packages.qml index 75207bc2e6..f0d6f508cc 100644 --- a/plugins/Marketplace/resources/qml/Packages.qml +++ b/plugins/Marketplace/resources/qml/Packages.qml @@ -29,17 +29,16 @@ ListView color: UM.Theme.getColor("detail_background") - required property string section - Label { id: sectionHeaderText anchors.verticalCenter: parent.verticalCenter anchors.left: parent.left - text: parent.section + text: section font: UM.Theme.getFont("large") color: UM.Theme.getColor("text") + onTextChanged: print(text) } } @@ -221,4 +220,3 @@ ListView } } } - From 66e52294b5b0f1beb6c706698588ff074b33ff95 Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Wed, 1 Dec 2021 17:08:08 +0100 Subject: [PATCH 023/195] Get the locally installed packages Contributes to: CURA-8587 --- cura/CuraPackageManager.py | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/cura/CuraPackageManager.py b/cura/CuraPackageManager.py index 26d6591099..4199375608 100644 --- a/cura/CuraPackageManager.py +++ b/cura/CuraPackageManager.py @@ -1,13 +1,16 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from typing import List, Tuple, TYPE_CHECKING, Optional +from typing import Any, Dict, List, Tuple, TYPE_CHECKING, Optional +from collections import Generator from cura.CuraApplication import CuraApplication #To find some resource types. from cura.Settings.GlobalStack import GlobalStack from UM.PackageManager import PackageManager #The class we're extending. from UM.Resources import Resources #To find storage paths for some resource types. +from UM.i18n import i18nCatalog +catalog = i18nCatalog("cura") if TYPE_CHECKING: from UM.Qt.QtApplication import QtApplication @@ -17,6 +20,18 @@ if TYPE_CHECKING: class CuraPackageManager(PackageManager): def __init__(self, application: "QtApplication", parent: Optional["QObject"] = None) -> None: super().__init__(application, parent) + self._locally_installed_packages = None + + @property + def locally_installed_packages(self): + """locally installed packages, lazy execution""" + if self._locally_installed_packages is None: + self._locally_installed_packages = list(self.iterateAllLocalPackages()) + return self._locally_installed_packages + + @locally_installed_packages.setter + def locally_installed_packages(self, value): + self._locally_installed_packages = value def initialize(self) -> None: self._installation_dirs_dict["materials"] = Resources.getStoragePath(CuraApplication.ResourceTypes.MaterialInstanceContainer) @@ -47,3 +62,20 @@ class CuraPackageManager(PackageManager): machine_with_qualities.append((global_stack, str(extruder_nr), container_id)) return machine_with_materials, machine_with_qualities + + def iterateAllLocalPackages(self) -> Generator[Dict[str, Any]]: + """ A generator which returns an unordered list of all the PackageModels""" + + # Get all the installed packages, add a section_title depending on package_type and user installed + for packages in self.getAllInstalledPackagesInfo().values(): + for package_info in packages: + yield package_info + + # Get all to be removed package_info's. These packages are still used in the current session so the user might + # still want to interact with these. + for package_data in self.getPackagesToRemove().values(): + yield package_data["package_info"] + + # Get all to be installed package_info's. Since the user might want to interact with these + for package_data in self.getPackagesToInstall().values(): + yield package_data["package_info"] From 09bc28d840165649e7e1b5e0ae41d5df25cc42b5 Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Wed, 1 Dec 2021 17:09:52 +0100 Subject: [PATCH 024/195] Moved local package logic out of the LocalPackageList Contributes to: CURA-8587 --- plugins/Marketplace/LocalPackageList.py | 74 ++++++++----------------- 1 file changed, 23 insertions(+), 51 deletions(-) diff --git a/plugins/Marketplace/LocalPackageList.py b/plugins/Marketplace/LocalPackageList.py index 059dd9bbef..d93ed87ba0 100644 --- a/plugins/Marketplace/LocalPackageList.py +++ b/plugins/Marketplace/LocalPackageList.py @@ -1,7 +1,9 @@ # Copyright (c) 2021 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from typing import Any, Dict, Generator, List, Optional, TYPE_CHECKING +from typing import Any, Dict, List, Optional, TYPE_CHECKING +from operator import attrgetter + from PyQt5.QtCore import pyqtSlot, QObject if TYPE_CHECKING: @@ -21,16 +23,16 @@ catalog = i18nCatalog("cura") class LocalPackageList(PackageList): - PACKAGE_SECTION_HEADER = { + PACKAGE_CATEGORIES = { "installed": { - "plugin": catalog.i18nc("@label:property", "Installed Plugins"), - "material": catalog.i18nc("@label:property", "Installed Materials") + "plugin": catalog.i18nc("@label", "Installed Plugins"), + "material": catalog.i18nc("@label", "Installed Materials") }, "bundled": { - "plugin": catalog.i18nc("@label:property", "Bundled Plugins"), - "material": catalog.i18nc("@label:property", "Bundled Materials") + "plugin": catalog.i18nc("@label", "Bundled Plugins"), + "material": catalog.i18nc("@label", "Bundled Materials") } } # The section headers to be used for the different package categories @@ -47,57 +49,24 @@ class LocalPackageList(PackageList): """ self.setErrorMessage("") # Clear any previous errors. self.setIsLoading(True) - self._getLocalPackages() + + # Obtain and sort the local packages + self.setItems([{"package": p} for p in [self._makePackageModel(p) for p in self._manager.locally_installed_packages]]) + self.sort(attrgetter("sectionTitle", "canUpdate", "displayName"), key = "package", reverse = True) + self.checkForUpdates(self._manager.locally_installed_packages) + self.setIsLoading(False) self.setHasMore(False) # All packages should have been loaded at this time - def _getLocalPackages(self) -> None: - """ Obtain the local packages. - - The list is sorted per category as in the order of the PACKAGE_SECTION_HEADER dictionary, whereas the packages - for the sections are sorted alphabetically on the display name. These sorted sections are then added to the items - """ - package_info = list(self._allPackageInfo()) - self.checkForUpdates(package_info) - sorted_sections: List[Dict[str, PackageModel]] = [] - for section in self._getSections(): - packages = filter(lambda p: p.sectionTitle == section, package_info) - sorted_sections.extend([{"package": p} for p in sorted(packages, key = lambda p: p.displayName)]) - self.setItems(sorted_sections) - - def _getSections(self) -> Generator[str, None, None]: - """ Flatten and order the PACKAGE_SECTION_HEADER such that it can be used in obtaining the packages in the - correct order""" - for package_type in self.PACKAGE_SECTION_HEADER.values(): - for section in package_type.values(): - yield section - - def _allPackageInfo(self) -> Generator[PackageModel, None, None]: - """ A generator which returns a unordered list of all the PackageModels""" - - # Get all the installed packages, add a section_title depending on package_type and user installed - for packages in self._manager.getAllInstalledPackagesInfo().values(): - for package_info in packages: - yield self._makePackageModel(package_info) - - # Get all to be removed package_info's. These packages are still used in the current session so the user might - # still want to interact with these. - for package_data in self._manager.getPackagesToRemove().values(): - yield self._makePackageModel(package_data["package_info"]) - - # Get all to be installed package_info's. Since the user might want to interact with these - for package_data in self._manager.getPackagesToInstall().values(): - yield self._makePackageModel(package_data["package_info"]) - def _makePackageModel(self, package_info: Dict[str, Any]) -> PackageModel: """ Create a PackageModel from the package_info and determine its section_title""" bundled_or_installed = "installed" if self._manager.isUserInstalledPackage(package_info["package_id"]) else "bundled" package_type = package_info["package_type"] - section_title = self.PACKAGE_SECTION_HEADER[bundled_or_installed][package_type] + section_title = self.PACKAGE_CATEGORIES[bundled_or_installed][package_type] return PackageModel(package_info, section_title = section_title, parent = self) - def checkForUpdates(self, packages: List[PackageModel]): - installed_packages = "installed_packages=".join([f"{package.packageId}:{package.packageVersion}&" for package in packages]) + def checkForUpdates(self, packages: List[Dict[str, Any]]): + installed_packages = "installed_packages=".join([f"{package['package_id']}:{package['package_version']}&" for package in packages]) request_url = f"{Marketplace.PACKAGE_UPDATES_URL}?installed_packages={installed_packages[:-1]}" self._ongoing_request = HttpRequestManager.getInstance().get( @@ -105,7 +74,6 @@ class LocalPackageList(PackageList): scope = self._scope, callback = self._parseResponse ) - return [] def _parseResponse(self, reply: "QNetworkReply") -> None: """ @@ -115,10 +83,14 @@ class LocalPackageList(PackageList): """ response_data = HttpRequestManager.readJSON(reply) if "data" not in response_data: - Logger.error(f"Could not interpret the server's response. Missing 'data' or 'links' from response data. Keys in response: {response_data.keys()}") - self.setErrorMessage(catalog.i18nc("@info:error", "Could not interpret the server's response.")) + Logger.error( + f"Could not interpret the server's response. Missing 'data' from response data. Keys in response: {response_data.keys()}") + return + if len(response_data["data"]) == 0: return for package_data in response_data["data"]: index = self.find("package", package_data["package_id"]) self.getItem(index)["package"].canUpdate = True + + self.sort(attrgetter("sectionTitle", "canUpdate", "displayName"), key = "package", reverse = True) From 02e2e0a1c6bf71b29a3198a9beb78266ea241c64 Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Wed, 1 Dec 2021 17:35:56 +0100 Subject: [PATCH 025/195] Filter already installed packages from the install listviews Contributes to: CURA-8587 --- cura/CuraPackageManager.py | 5 ++--- plugins/Marketplace/LocalPackageList.py | 3 --- plugins/Marketplace/PackageList.py | 1 + plugins/Marketplace/RemotePackageList.py | 3 +++ 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cura/CuraPackageManager.py b/cura/CuraPackageManager.py index 4199375608..a8400bfae7 100644 --- a/cura/CuraPackageManager.py +++ b/cura/CuraPackageManager.py @@ -1,8 +1,7 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from typing import Any, Dict, List, Tuple, TYPE_CHECKING, Optional -from collections import Generator +from typing import Any, Dict, List, Tuple, TYPE_CHECKING, Optional, Generator from cura.CuraApplication import CuraApplication #To find some resource types. from cura.Settings.GlobalStack import GlobalStack @@ -63,7 +62,7 @@ class CuraPackageManager(PackageManager): return machine_with_materials, machine_with_qualities - def iterateAllLocalPackages(self) -> Generator[Dict[str, Any]]: + def iterateAllLocalPackages(self) -> Generator[Dict[str, Any], None, None]: """ A generator which returns an unordered list of all the PackageModels""" # Get all the installed packages, add a section_title depending on package_type and user installed diff --git a/plugins/Marketplace/LocalPackageList.py b/plugins/Marketplace/LocalPackageList.py index d93ed87ba0..bbe74a9056 100644 --- a/plugins/Marketplace/LocalPackageList.py +++ b/plugins/Marketplace/LocalPackageList.py @@ -13,8 +13,6 @@ from UM.i18n import i18nCatalog from UM.TaskManagement.HttpRequestManager import HttpRequestManager from UM.Logger import Logger -from cura.CuraApplication import CuraApplication - from .PackageList import PackageList from .PackageModel import PackageModel from . import Marketplace @@ -38,7 +36,6 @@ class LocalPackageList(PackageList): def __init__(self, parent: Optional["QObject"] = None) -> None: super().__init__(parent) - self._manager = CuraApplication.getInstance().getPackageManager() self._has_footer = False @pyqtSlot() diff --git a/plugins/Marketplace/PackageList.py b/plugins/Marketplace/PackageList.py index d3da8c826a..798a15324e 100644 --- a/plugins/Marketplace/PackageList.py +++ b/plugins/Marketplace/PackageList.py @@ -26,6 +26,7 @@ class PackageList(ListModel): def __init__(self, parent: Optional["QObject"] = None) -> None: super().__init__(parent) + self._manager = CuraApplication.getInstance().getPackageManager() self._error_message = "" self.addRoleName(self.PackageRole, "package") self._is_loading = False diff --git a/plugins/Marketplace/RemotePackageList.py b/plugins/Marketplace/RemotePackageList.py index 4fedde9ff0..d5c0763609 100644 --- a/plugins/Marketplace/RemotePackageList.py +++ b/plugins/Marketplace/RemotePackageList.py @@ -31,6 +31,7 @@ class RemotePackageList(PackageList): self._request_url = self._initialRequestUrl() self.isLoadingChanged.connect(self._onLoadingChanged) self.isLoadingChanged.emit() + self._locally_installed = { p["package_id"] for p in self._manager.locally_installed_packages } def __del__(self) -> None: """ @@ -128,6 +129,8 @@ class RemotePackageList(PackageList): return for package_data in response_data["data"]: + if package_data["package_id"] in self._locally_installed: + continue # We should only show packages which are not already installed try: package = PackageModel(package_data, parent = self) self.appendItem({"package": package}) # Add it to this list model. From ecc3760e8e2e5b964b22ee47b120e54a287b9ede Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Tue, 30 Nov 2021 13:24:12 +0100 Subject: [PATCH 026/195] Increase size of icons on action buttons We want those to be 1.5em now. This has an effect on all action buttons with icons in the interface! Contributes to issue CURA-8565. --- resources/themes/cura-light/theme.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/themes/cura-light/theme.json b/resources/themes/cura-light/theme.json index dede26db0b..4bc6adc99c 100644 --- a/resources/themes/cura-light/theme.json +++ b/resources/themes/cura-light/theme.json @@ -563,7 +563,7 @@ "button_lining": [0, 0], "action_button": [15.0, 2.5], - "action_button_icon": [1.0, 1.0], + "action_button_icon": [1.5, 1.5], "action_button_radius": [0.15, 0.15], "dialog_primary_button_padding": [3.0, 0], From 8162bdcfa82a9b39e4ede12dfa37179cb8710735 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Tue, 30 Nov 2021 14:42:12 +0100 Subject: [PATCH 027/195] Don't show download count for bundled plug-ins I considered rewriting the section title property to be QML-only and translate it from the QML, but this is a bit simpler in the end, even though there is data duplication now. Contributes to issue CURA-8565. --- plugins/Marketplace/PackageModel.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/plugins/Marketplace/PackageModel.py b/plugins/Marketplace/PackageModel.py index 4a4973a2dc..62558a51b8 100644 --- a/plugins/Marketplace/PackageModel.py +++ b/plugins/Marketplace/PackageModel.py @@ -17,10 +17,11 @@ class PackageModel(QObject): QML. The model can also be constructed directly from a response received by the API. """ - def __init__(self, package_data: Dict[str, Any], section_title: Optional[str] = None, parent: Optional[QObject] = None) -> None: + def __init__(self, package_data: Dict[str, Any], installation_status: str, section_title: Optional[str] = None, parent: Optional[QObject] = None) -> None: """ Constructs a new model for a single package. :param package_data: The data received from the Marketplace API about the package to create. + :param installation_status: Whether the package is `not_installed`, `installed` or `bundled`. :param section_title: If the packages are to be categorized per section provide the section_title :param parent: The parent QML object that controls the lifetime of this model (normally a PackageList). """ @@ -48,6 +49,7 @@ class PackageModel(QObject): if not self._icon_url or self._icon_url == "": self._icon_url = author_data.get("icon_url", "") + self._installation_status = installation_status self._section_title = section_title # Note that there's a lot more info in the package_data than just these specified here. @@ -95,6 +97,10 @@ class PackageModel(QObject): def authorInfoUrl(self): return self._author_info_url + @pyqtProperty(str, constant = True) + def installationStatus(self) -> str: + return self._installation_status + @pyqtProperty(str, constant = True) def sectionTitle(self) -> Optional[str]: return self._section_title From 3a284e3c2c4297fbcec325be8eb2d5972c3797bc Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Tue, 30 Nov 2021 14:50:45 +0100 Subject: [PATCH 028/195] Show hover colour when hovering a card This signals to the user they can select one. Contributes to issue CURA-8565. --- plugins/Marketplace/resources/qml/Packages.qml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plugins/Marketplace/resources/qml/Packages.qml b/plugins/Marketplace/resources/qml/Packages.qml index f0d6f508cc..3c627aef32 100644 --- a/plugins/Marketplace/resources/qml/Packages.qml +++ b/plugins/Marketplace/resources/qml/Packages.qml @@ -62,9 +62,11 @@ ListView delegate: MouseArea { + id: cardMouseArea width: parent ? parent.width : 0 height: childrenRect.height + hoverEnabled: true onClicked: { packages.selectedPackage = model.package; @@ -75,6 +77,7 @@ ListView { packageData: model.package width: parent.width - UM.Theme.getSize("default_margin").width - UM.Theme.getSize("narrow_margin").width + color: cardMouseArea.containsMouse ? UM.Theme.getColor("action_button_hovered") : UM.Theme.getColor("main_background") } } From 6f65521ce88c8cdbbad9eea37273a0d0e504a29c Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Tue, 30 Nov 2021 14:56:09 +0100 Subject: [PATCH 029/195] Make icons smaller for tertiary buttons These are typically visually smaller buttons, since they don't have an outline. It makes more sense to use the size of the text then, or something thereabouts. Contributes to issue CURA-8565. --- resources/qml/TertiaryButton.qml | 1 + resources/themes/cura-light/theme.json | 1 + 2 files changed, 2 insertions(+) diff --git a/resources/qml/TertiaryButton.qml b/resources/qml/TertiaryButton.qml index 76684b6ef2..8171188232 100644 --- a/resources/qml/TertiaryButton.qml +++ b/resources/qml/TertiaryButton.qml @@ -16,4 +16,5 @@ Cura.ActionButton textDisabledColor: UM.Theme.getColor("action_button_disabled_text") hoverColor: "transparent" underlineTextOnHover: true + iconSize: UM.Theme.getSize("action_button_icon_small").height } diff --git a/resources/themes/cura-light/theme.json b/resources/themes/cura-light/theme.json index 4bc6adc99c..6dec15aff8 100644 --- a/resources/themes/cura-light/theme.json +++ b/resources/themes/cura-light/theme.json @@ -564,6 +564,7 @@ "action_button": [15.0, 2.5], "action_button_icon": [1.5, 1.5], + "action_button_icon_small": [1.0, 1.0], "action_button_radius": [0.15, 0.15], "dialog_primary_button_padding": [3.0, 0], From 7fe327fb483ca58b1d2a47409d5e322c39841f51 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Tue, 30 Nov 2021 15:57:44 +0100 Subject: [PATCH 030/195] Fix resetting when Marketplace is closed and re-opened Previously, this would cause the Marketplace to freeze. We're still not entirely sure why. It seems to be a bug in Qt, but it's rather hard to deal with. This new solution is nicer in some ways but not as neat in others. - We're no longer clearing the content of the loader, so the QML and the package data remains in memory while the Marketplace is closed. We deem this to not be a problem, because the memory usage of this package data is only a couple of kB, nothing compared to the memory used by the slicer when it loads a model. - On the other hand, it's now possible to programmatically change the tab there, instead of manually having to click the buttons. - Fixes a bug where the highlighted tab of of the tab bar doesn't update when closing and re-opening the Marketplace. And a bug where there was a search bar for the manage page while it didn't work. Contributes to issue CURA-8565. --- .../Marketplace/resources/qml/Marketplace.qml | 36 +++++++++++-------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/plugins/Marketplace/resources/qml/Marketplace.qml b/plugins/Marketplace/resources/qml/Marketplace.qml index 951de77f19..a02c0e0d5f 100644 --- a/plugins/Marketplace/resources/qml/Marketplace.qml +++ b/plugins/Marketplace/resources/qml/Marketplace.qml @@ -21,8 +21,14 @@ Window width: minimumWidth height: minimumHeight - // Set and unset the content. No need to keep things in memory if it's not visible. - onVisibleChanged: content.source = visible ? "Plugins.qml" : "" + onVisibleChanged: + { + pageSelectionTabBar.currentIndex = 0; //Go back to the initial tab. + while(contextStack.depth > 1) + { + contextStack.pop(); //Do NOT use the StackView.Immediate transition here, since it causes the window to stay empty. Seemingly a Qt bug: https://bugreports.qt.io/browse/QTBUG-60670? + } + } Connections { @@ -116,33 +122,33 @@ Window spacing: 0 background: Rectangle { color: "transparent" } + onCurrentIndexChanged: + { + searchBar.text = ""; + searchBar.visible = currentItem.hasSearch; + content.source = currentItem.sourcePage; + } + PackageTypeTab { id: pluginTabText width: implicitWidth text: catalog.i18nc("@button", "Plugins") - onClicked: - { - searchBar.text = "" - searchBar.visible = true - content.source = "Plugins.qml" - } + property string sourcePage: "Plugins.qml" + property bool hasSearch: true } PackageTypeTab { id: materialsTabText width: implicitWidth text: catalog.i18nc("@button", "Materials") - onClicked: - { - searchBar.text = "" - searchBar.visible = true - content.source = "Materials.qml" - } + property string sourcePage: "Materials.qml" + property bool hasSearch: true } ManagePackagesButton { - onClicked: content.source = "ManagedPackages.qml" + property string sourcePage: "ManagedPackages.qml" + property bool hasSearch: false } } From 9a1fbd54b03e65279b599273c05626a873f8caa5 Mon Sep 17 00:00:00 2001 From: casper Date: Fri, 26 Nov 2021 10:06:50 +0100 Subject: [PATCH 031/195] Add basic onboarding banner to the market place --- .../resources/qml/OnboardBanner.qml | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 plugins/Marketplace/resources/qml/OnboardBanner.qml diff --git a/plugins/Marketplace/resources/qml/OnboardBanner.qml b/plugins/Marketplace/resources/qml/OnboardBanner.qml new file mode 100644 index 0000000000..b91a9a52f7 --- /dev/null +++ b/plugins/Marketplace/resources/qml/OnboardBanner.qml @@ -0,0 +1,79 @@ +// Copyright (c) 2021 Ultimaker B.V. +// Cura is released under the terms of the LGPLv3 or higher. + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.1 + +import UM 1.6 as UM +import Cura 1.6 as Cura + +// Onboarding banner. +Rectangle +{ + Layout.preferredHeight: childrenRect.height + 2 * UM.Theme.getSize("default_margin").height + anchors + { + margins: UM.Theme.getSize("default_margin").width + left: parent.left + right: parent.right + top: parent.top + } + + color: UM.Theme.getColor("action_panel_secondary") + + // Icon + Rectangle + { + id: onboardingIcon + anchors + { + top: parent.top + left: parent.left + margins: UM.Theme.getSize("default_margin").width + } + width: UM.Theme.getSize("button_icon").width + height: UM.Theme.getSize("button_icon").height + color: "transparent" + UM.RecolorImage + { + anchors.fill: parent + color: UM.Theme.getColor("primary_text") + source: UM.Theme.getIcon("Shop") + } + } + + // Close button + UM.SimpleButton + { + id: onboardingClose + anchors + { + top: parent.top + right: parent.right + margins: UM.Theme.getSize("default_margin").width + } + width: UM.Theme.getSize("message_close").width + height: UM.Theme.getSize("message_close").height + color: UM.Theme.getColor("primary_text") + hoverColor: UM.Theme.getColor("primary_text_hover") + iconSource: UM.Theme.getIcon("Cancel") + onClicked: confirmDeleteDialog.visible = true + } + + // Body + Text { + anchors + { + top: parent.top + left: onboardingIcon.right + right: onboardingClose.left + margins: UM.Theme.getSize("default_margin").width + } + + font: UM.Theme.getFont("medium") + color: UM.Theme.getColor("primary_text") + wrapMode: Text.WordWrap + text: catalog.i18nc("@text", "Streamline your workflow and customize your Ultimaker Cura experience with plugins contributed by our amazing community of users.") + } +} \ No newline at end of file From 5e4f67c7e1f3ee62e51afcd7738bf2edcb4d2f7e Mon Sep 17 00:00:00 2001 From: casper Date: Fri, 26 Nov 2021 10:10:09 +0100 Subject: [PATCH 032/195] Display different content on each of the marketplace onboarding banners --- .../resources/qml/ManagedPackages.qml | 1 + .../Marketplace/resources/qml/Materials.qml | 1 + .../resources/qml/OnboardBanner.qml | 20 +++++++++++++++++-- plugins/Marketplace/resources/qml/Plugins.qml | 1 + 4 files changed, 21 insertions(+), 2 deletions(-) diff --git a/plugins/Marketplace/resources/qml/ManagedPackages.qml b/plugins/Marketplace/resources/qml/ManagedPackages.qml index 243d5bf12e..c53384bf77 100644 --- a/plugins/Marketplace/resources/qml/ManagedPackages.qml +++ b/plugins/Marketplace/resources/qml/ManagedPackages.qml @@ -10,6 +10,7 @@ import UM 1.4 as UM Packages { pageTitle: catalog.i18nc("@header", "Manage packages") + bannerType: "__MANAGE_PACKAGES__" model: Marketplace.LocalPackageList { } diff --git a/plugins/Marketplace/resources/qml/Materials.qml b/plugins/Marketplace/resources/qml/Materials.qml index 1d1572976a..dff63305bf 100644 --- a/plugins/Marketplace/resources/qml/Materials.qml +++ b/plugins/Marketplace/resources/qml/Materials.qml @@ -6,6 +6,7 @@ import Marketplace 1.0 as Marketplace Packages { pageTitle: catalog.i18nc("@header", "Install Materials") + bannerType: "__MATERIALS__" model: Marketplace.RemotePackageList { packageTypeFilter: "material" diff --git a/plugins/Marketplace/resources/qml/OnboardBanner.qml b/plugins/Marketplace/resources/qml/OnboardBanner.qml index b91a9a52f7..8a1048018c 100644 --- a/plugins/Marketplace/resources/qml/OnboardBanner.qml +++ b/plugins/Marketplace/resources/qml/OnboardBanner.qml @@ -11,6 +11,8 @@ import Cura 1.6 as Cura // Onboarding banner. Rectangle { + property var bannerType + Layout.preferredHeight: childrenRect.height + 2 * UM.Theme.getSize("default_margin").height anchors { @@ -39,7 +41,14 @@ Rectangle { anchors.fill: parent color: UM.Theme.getColor("primary_text") - source: UM.Theme.getIcon("Shop") + source: { + switch (bannerType) { + case "__PLUGINS__" : return UM.Theme.getIcon("Shop"); + case "__MATERIALS__" : return UM.Theme.getIcon("Spool"); + case "__MANAGE_PACKAGES__" : return UM.Theme.getIcon("ArrowDoubleCircleRight"); + default: return ""; + } + } } } @@ -74,6 +83,13 @@ Rectangle font: UM.Theme.getFont("medium") color: UM.Theme.getColor("primary_text") wrapMode: Text.WordWrap - text: catalog.i18nc("@text", "Streamline your workflow and customize your Ultimaker Cura experience with plugins contributed by our amazing community of users.") + text: { + switch (bannerType) { + case "__PLUGINS__" : return catalog.i18nc("@text", "Streamline your workflow and customize your Ultimaker Cura experience with plugins contributed by our amazing community of users."); + case "__MATERIALS__" : return catalog.i18nc("@text", "Select and install material profiles optimised for your Ultimaker 3D printers."); + case "__MANAGE_PACKAGES__" : return catalog.i18nc("@text", "Manage your Ultimaker Cura plugins and material profiles here. Make sure to keep your plugins up to date and backup your setup regularly."); + default: return ""; + } + } } } \ No newline at end of file diff --git a/plugins/Marketplace/resources/qml/Plugins.qml b/plugins/Marketplace/resources/qml/Plugins.qml index ef5d92c2e8..24381e3027 100644 --- a/plugins/Marketplace/resources/qml/Plugins.qml +++ b/plugins/Marketplace/resources/qml/Plugins.qml @@ -6,6 +6,7 @@ import Marketplace 1.0 as Marketplace Packages { pageTitle: catalog.i18nc("@header", "Install Plugins") + bannerType: "__PLUGINS__" model: Marketplace.RemotePackageList { packageTypeFilter: "plugin" From 748101ce69932f5d8f82ef27bee6b33a887779e5 Mon Sep 17 00:00:00 2001 From: casper Date: Fri, 26 Nov 2021 14:49:00 +0100 Subject: [PATCH 033/195] Remove banners when clicking close button --- cura/CuraApplication.py | 31 +++++++++++++++++++ .../resources/qml/ManagedPackages.qml | 10 +++++- .../Marketplace/resources/qml/Materials.qml | 10 +++++- .../resources/qml/OnboardBanner.qml | 28 ++++++----------- plugins/Marketplace/resources/qml/Plugins.qml | 10 +++++- 5 files changed, 68 insertions(+), 21 deletions(-) diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 3d4ec1209f..6cf2593bbf 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -572,6 +572,10 @@ class CuraApplication(QtApplication): preferences.addPreference("general/accepted_user_agreement", False) + preferences.addPreference("cura/market_place_show_plugin_banner", True) + preferences.addPreference("cura/market_place_show_material_banner", True) + preferences.addPreference("cura/market_place_show_manage_packages_banner", True) + for key in [ "dialog_load_path", # dialog_save_path is in LocalFileOutputDevicePlugin "dialog_profile_path", @@ -2011,6 +2015,33 @@ class CuraApplication(QtApplication): show_whatsnew_only = has_active_machine and has_app_just_upgraded return show_whatsnew_only + @pyqtSlot(result = bool) + def shouldShowMarketPlacePluginBanner(self) -> bool: + return self._preferences.getValue("cura/market_place_show_plugin_banner") + + @pyqtSlot(result = bool) + def shouldShowMarketPlaceMaterialBanner(self) -> bool: + return self._preferences.getValue("cura/market_place_show_material_banner") + + @pyqtSlot(result = bool) + def shouldShowMarketPlaceManagePackagesBanner(self) -> bool: + return self._preferences.getValue("cura/market_place_show_manage_packages_banner") + + @pyqtSlot() + def closeMarketPlacePluginBanner(self) -> None: + Logger.log("i", "Close market place plugin banner") + self._preferences.setValue("cura/market_place_show_plugin_banner", False) + + @pyqtSlot() + def closeMarketPlaceMaterialBanner(self) -> None: + Logger.log("i", "Close market place material banner") + self._preferences.setValue("cura/market_place_show_material_banner", False) + + @pyqtSlot() + def closeMarketPlaceManagePackagesBanner(self) -> None: + Logger.log("i", "Close market place manage packages banner") + self._preferences.setValue("cura/market_place_show_manage_packages_banner", False) + @pyqtSlot(result = int) def appWidth(self) -> int: main_window = QtApplication.getInstance().getMainWindow() diff --git a/plugins/Marketplace/resources/qml/ManagedPackages.qml b/plugins/Marketplace/resources/qml/ManagedPackages.qml index c53384bf77..a329d992e5 100644 --- a/plugins/Marketplace/resources/qml/ManagedPackages.qml +++ b/plugins/Marketplace/resources/qml/ManagedPackages.qml @@ -10,7 +10,15 @@ import UM 1.4 as UM Packages { pageTitle: catalog.i18nc("@header", "Manage packages") - bannerType: "__MANAGE_PACKAGES__" + + bannerVisible: CuraApplication.shouldShowMarketPlaceManagePackagesBanner() + bannerIcon: "ArrowDoubleCircleRight" + bannerBody: catalog.i18nc("@text", "Manage your Ultimaker Cura plugins and material profiles here. Make sure to keep your plugins up to date and backup your setup regularly.") + onRemoveBanner: function() { + CuraApplication.closeMarketPlaceManagePackagesBanner(); + bannerVisible = false; + } + model: Marketplace.LocalPackageList { } diff --git a/plugins/Marketplace/resources/qml/Materials.qml b/plugins/Marketplace/resources/qml/Materials.qml index dff63305bf..32d2c2213a 100644 --- a/plugins/Marketplace/resources/qml/Materials.qml +++ b/plugins/Marketplace/resources/qml/Materials.qml @@ -6,7 +6,15 @@ import Marketplace 1.0 as Marketplace Packages { pageTitle: catalog.i18nc("@header", "Install Materials") - bannerType: "__MATERIALS__" + + bannerVisible: CuraApplication.shouldShowMarketPlaceMaterialBanner() + bannerIcon: "Spool" + bannerBody: catalog.i18nc("@text", "Streamline your workflow and customize your Ultimaker Cura experience with plugins contributed by our amazing community of users.") + onRemoveBanner: function() { + CuraApplication.closeMarketPlaceMaterialBanner(); + bannerVisible = false; + } + model: Marketplace.RemotePackageList { packageTypeFilter: "material" diff --git a/plugins/Marketplace/resources/qml/OnboardBanner.qml b/plugins/Marketplace/resources/qml/OnboardBanner.qml index 8a1048018c..8a29030514 100644 --- a/plugins/Marketplace/resources/qml/OnboardBanner.qml +++ b/plugins/Marketplace/resources/qml/OnboardBanner.qml @@ -11,7 +11,12 @@ import Cura 1.6 as Cura // Onboarding banner. Rectangle { - property var bannerType + property bool bannerVisible + property string bannerIcon + property string bannerBody + property var onRemoveBanner + + visible: bannerVisible Layout.preferredHeight: childrenRect.height + 2 * UM.Theme.getSize("default_margin").height anchors @@ -41,14 +46,7 @@ Rectangle { anchors.fill: parent color: UM.Theme.getColor("primary_text") - source: { - switch (bannerType) { - case "__PLUGINS__" : return UM.Theme.getIcon("Shop"); - case "__MATERIALS__" : return UM.Theme.getIcon("Spool"); - case "__MANAGE_PACKAGES__" : return UM.Theme.getIcon("ArrowDoubleCircleRight"); - default: return ""; - } - } + source: UM.Theme.getIcon(bannerIcon) } } @@ -67,7 +65,8 @@ Rectangle color: UM.Theme.getColor("primary_text") hoverColor: UM.Theme.getColor("primary_text_hover") iconSource: UM.Theme.getIcon("Cancel") - onClicked: confirmDeleteDialog.visible = true + + onClicked: onRemoveBanner() } // Body @@ -83,13 +82,6 @@ Rectangle font: UM.Theme.getFont("medium") color: UM.Theme.getColor("primary_text") wrapMode: Text.WordWrap - text: { - switch (bannerType) { - case "__PLUGINS__" : return catalog.i18nc("@text", "Streamline your workflow and customize your Ultimaker Cura experience with plugins contributed by our amazing community of users."); - case "__MATERIALS__" : return catalog.i18nc("@text", "Select and install material profiles optimised for your Ultimaker 3D printers."); - case "__MANAGE_PACKAGES__" : return catalog.i18nc("@text", "Manage your Ultimaker Cura plugins and material profiles here. Make sure to keep your plugins up to date and backup your setup regularly."); - default: return ""; - } - } + text: bannerBody } } \ No newline at end of file diff --git a/plugins/Marketplace/resources/qml/Plugins.qml b/plugins/Marketplace/resources/qml/Plugins.qml index 24381e3027..b9d38c6e2b 100644 --- a/plugins/Marketplace/resources/qml/Plugins.qml +++ b/plugins/Marketplace/resources/qml/Plugins.qml @@ -6,7 +6,15 @@ import Marketplace 1.0 as Marketplace Packages { pageTitle: catalog.i18nc("@header", "Install Plugins") - bannerType: "__PLUGINS__" + + bannerVisible: CuraApplication.shouldShowMarketPlacePluginBanner() + bannerIcon: "Shop" + bannerBody: catalog.i18nc("@text", "Select and install material profiles optimised for your Ultimaker 3D printers.") + onRemoveBanner: function() { + CuraApplication.closeMarketPlacePluginBanner(); + bannerVisible = false; + } + model: Marketplace.RemotePackageList { packageTypeFilter: "plugin" From b75ba44a94e78fda80ad3becfe8a6e8cacc17e72 Mon Sep 17 00:00:00 2001 From: casper Date: Sat, 27 Nov 2021 13:31:34 +0100 Subject: [PATCH 034/195] Add "readmore" button --- .../resources/qml/OnboardBanner.qml | 55 ++++++++++++++++++- 1 file changed, 52 insertions(+), 3 deletions(-) diff --git a/plugins/Marketplace/resources/qml/OnboardBanner.qml b/plugins/Marketplace/resources/qml/OnboardBanner.qml index 8a29030514..150377eaf3 100644 --- a/plugins/Marketplace/resources/qml/OnboardBanner.qml +++ b/plugins/Marketplace/resources/qml/OnboardBanner.qml @@ -70,7 +70,8 @@ Rectangle } // Body - Text { + Label { + id: infoText anchors { top: parent.top @@ -80,8 +81,56 @@ Rectangle } font: UM.Theme.getFont("medium") - color: UM.Theme.getColor("primary_text") - wrapMode: Text.WordWrap text: bannerBody + + renderType: Text.NativeRendering + color: "white" + wrapMode: Text.Wrap + elide: Text.ElideRight + + onLineLaidOut: + { + if(line.isLast) + { + // Check if read more button still fits after the body text + if (line.implicitWidth + readMoreButton.width + UM.Theme.getSize("default_margin").width > width) + { + // If it does place it after the body text + readMoreButton.anchors.left = infoText.left; + readMoreButton.anchors.bottom = infoText.bottom; + readMoreButton.anchors.bottomMargin = -(fontMetrics.height + UM.Theme.getSize("thin_margin").height); + readMoreButton.anchors.leftMargin = 0; + } + else + { + // Otherwise place it under the text + readMoreButton.anchors.left = infoText.left; + readMoreButton.anchors.bottom = infoText.bottom; + readMoreButton.anchors.leftMargin = line.implicitWidth + UM.Theme.getSize("default_margin").width; + readMoreButton.anchors.bottomMargin = 0; + } + } + } + } + + FontMetrics + { + id: fontMetrics + font: UM.Theme.getFont("default") + } + + Cura.TertiaryButton + { + id: readMoreButton + text: "Learn More" + textFont: UM.Theme.getFont("default") + textColor: infoText.color + leftPadding: 0 + rightPadding: 0 + iconSource: UM.Theme.getIcon("LinkExternal") + isIconOnRightSide: true + height: fontMetrics.height + + onClicked: print("TODO") } } \ No newline at end of file From a4d9beb9c2ceb8d1897f4771bd9b868b582becce Mon Sep 17 00:00:00 2001 From: casper Date: Sat, 27 Nov 2021 13:31:47 +0100 Subject: [PATCH 035/195] Change font size in banner --- plugins/Marketplace/resources/qml/OnboardBanner.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/Marketplace/resources/qml/OnboardBanner.qml b/plugins/Marketplace/resources/qml/OnboardBanner.qml index 150377eaf3..cbd151bed2 100644 --- a/plugins/Marketplace/resources/qml/OnboardBanner.qml +++ b/plugins/Marketplace/resources/qml/OnboardBanner.qml @@ -80,7 +80,7 @@ Rectangle margins: UM.Theme.getSize("default_margin").width } - font: UM.Theme.getFont("medium") + font: UM.Theme.getFont("default") text: bannerBody renderType: Text.NativeRendering From 815aadb0d76779735df0d0a3b5e5a2d5bb141d88 Mon Sep 17 00:00:00 2001 From: casper Date: Sat, 27 Nov 2021 13:48:38 +0100 Subject: [PATCH 036/195] Add links to read me support pages Note that since the support pages are not yet ready the "read more" buttons are hidden. Once the correct link is added they become visible again. --- plugins/Marketplace/resources/qml/ManagedPackages.qml | 1 + plugins/Marketplace/resources/qml/Materials.qml | 1 + plugins/Marketplace/resources/qml/OnboardBanner.qml | 4 +++- plugins/Marketplace/resources/qml/Plugins.qml | 1 + 4 files changed, 6 insertions(+), 1 deletion(-) diff --git a/plugins/Marketplace/resources/qml/ManagedPackages.qml b/plugins/Marketplace/resources/qml/ManagedPackages.qml index a329d992e5..e8e54d77b5 100644 --- a/plugins/Marketplace/resources/qml/ManagedPackages.qml +++ b/plugins/Marketplace/resources/qml/ManagedPackages.qml @@ -14,6 +14,7 @@ Packages bannerVisible: CuraApplication.shouldShowMarketPlaceManagePackagesBanner() bannerIcon: "ArrowDoubleCircleRight" bannerBody: catalog.i18nc("@text", "Manage your Ultimaker Cura plugins and material profiles here. Make sure to keep your plugins up to date and backup your setup regularly.") + readMoreUrl: "" // TODO add when support page is ready onRemoveBanner: function() { CuraApplication.closeMarketPlaceManagePackagesBanner(); bannerVisible = false; diff --git a/plugins/Marketplace/resources/qml/Materials.qml b/plugins/Marketplace/resources/qml/Materials.qml index 32d2c2213a..fc4869dac5 100644 --- a/plugins/Marketplace/resources/qml/Materials.qml +++ b/plugins/Marketplace/resources/qml/Materials.qml @@ -10,6 +10,7 @@ Packages bannerVisible: CuraApplication.shouldShowMarketPlaceMaterialBanner() bannerIcon: "Spool" bannerBody: catalog.i18nc("@text", "Streamline your workflow and customize your Ultimaker Cura experience with plugins contributed by our amazing community of users.") + readMoreUrl: "" // TODO add when support page is ready onRemoveBanner: function() { CuraApplication.closeMarketPlaceMaterialBanner(); bannerVisible = false; diff --git a/plugins/Marketplace/resources/qml/OnboardBanner.qml b/plugins/Marketplace/resources/qml/OnboardBanner.qml index cbd151bed2..4b5e494be5 100644 --- a/plugins/Marketplace/resources/qml/OnboardBanner.qml +++ b/plugins/Marketplace/resources/qml/OnboardBanner.qml @@ -15,6 +15,7 @@ Rectangle property string bannerIcon property string bannerBody property var onRemoveBanner + property string readMoreUrl visible: bannerVisible @@ -121,6 +122,7 @@ Rectangle Cura.TertiaryButton { + visible: readMoreUrl !== "" id: readMoreButton text: "Learn More" textFont: UM.Theme.getFont("default") @@ -131,6 +133,6 @@ Rectangle isIconOnRightSide: true height: fontMetrics.height - onClicked: print("TODO") + onClicked: Qt.openUrlExternally(readMoreUrl) } } \ No newline at end of file diff --git a/plugins/Marketplace/resources/qml/Plugins.qml b/plugins/Marketplace/resources/qml/Plugins.qml index b9d38c6e2b..f0c101153c 100644 --- a/plugins/Marketplace/resources/qml/Plugins.qml +++ b/plugins/Marketplace/resources/qml/Plugins.qml @@ -10,6 +10,7 @@ Packages bannerVisible: CuraApplication.shouldShowMarketPlacePluginBanner() bannerIcon: "Shop" bannerBody: catalog.i18nc("@text", "Select and install material profiles optimised for your Ultimaker 3D printers.") + readMoreUrl: "" // TODO add when support page is ready onRemoveBanner: function() { CuraApplication.closeMarketPlacePluginBanner(); bannerVisible = false; From 7e1247b171dc3cf99cf704734778218badf8fceb Mon Sep 17 00:00:00 2001 From: casper Date: Mon, 29 Nov 2021 11:50:52 +0100 Subject: [PATCH 037/195] Directly use bindings in banner pages CURA-8564 --- cura/CuraApplication.py | 27 ------------------- .../resources/qml/ManagedPackages.qml | 4 +-- .../Marketplace/resources/qml/Materials.qml | 5 ++-- plugins/Marketplace/resources/qml/Plugins.qml | 5 ++-- 4 files changed, 8 insertions(+), 33 deletions(-) diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 6cf2593bbf..21924a2680 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -2015,33 +2015,6 @@ class CuraApplication(QtApplication): show_whatsnew_only = has_active_machine and has_app_just_upgraded return show_whatsnew_only - @pyqtSlot(result = bool) - def shouldShowMarketPlacePluginBanner(self) -> bool: - return self._preferences.getValue("cura/market_place_show_plugin_banner") - - @pyqtSlot(result = bool) - def shouldShowMarketPlaceMaterialBanner(self) -> bool: - return self._preferences.getValue("cura/market_place_show_material_banner") - - @pyqtSlot(result = bool) - def shouldShowMarketPlaceManagePackagesBanner(self) -> bool: - return self._preferences.getValue("cura/market_place_show_manage_packages_banner") - - @pyqtSlot() - def closeMarketPlacePluginBanner(self) -> None: - Logger.log("i", "Close market place plugin banner") - self._preferences.setValue("cura/market_place_show_plugin_banner", False) - - @pyqtSlot() - def closeMarketPlaceMaterialBanner(self) -> None: - Logger.log("i", "Close market place material banner") - self._preferences.setValue("cura/market_place_show_material_banner", False) - - @pyqtSlot() - def closeMarketPlaceManagePackagesBanner(self) -> None: - Logger.log("i", "Close market place manage packages banner") - self._preferences.setValue("cura/market_place_show_manage_packages_banner", False) - @pyqtSlot(result = int) def appWidth(self) -> int: main_window = QtApplication.getInstance().getMainWindow() diff --git a/plugins/Marketplace/resources/qml/ManagedPackages.qml b/plugins/Marketplace/resources/qml/ManagedPackages.qml index e8e54d77b5..677a9ee574 100644 --- a/plugins/Marketplace/resources/qml/ManagedPackages.qml +++ b/plugins/Marketplace/resources/qml/ManagedPackages.qml @@ -11,12 +11,12 @@ Packages { pageTitle: catalog.i18nc("@header", "Manage packages") - bannerVisible: CuraApplication.shouldShowMarketPlaceManagePackagesBanner() + bannerVisible: UM.Preferences.getValue("cura/market_place_show_manage_packages_banner"); bannerIcon: "ArrowDoubleCircleRight" bannerBody: catalog.i18nc("@text", "Manage your Ultimaker Cura plugins and material profiles here. Make sure to keep your plugins up to date and backup your setup regularly.") readMoreUrl: "" // TODO add when support page is ready onRemoveBanner: function() { - CuraApplication.closeMarketPlaceManagePackagesBanner(); + UM.Preferences.setValue("cura/market_place_show_manage_packages_banner", false); bannerVisible = false; } diff --git a/plugins/Marketplace/resources/qml/Materials.qml b/plugins/Marketplace/resources/qml/Materials.qml index fc4869dac5..e4cd554334 100644 --- a/plugins/Marketplace/resources/qml/Materials.qml +++ b/plugins/Marketplace/resources/qml/Materials.qml @@ -2,17 +2,18 @@ // Cura is released under the terms of the LGPLv3 or higher. import Marketplace 1.0 as Marketplace +import UM 1.4 as UM Packages { pageTitle: catalog.i18nc("@header", "Install Materials") - bannerVisible: CuraApplication.shouldShowMarketPlaceMaterialBanner() + bannerVisible: UM.Preferences.getValue("cura/market_place_show_material_banner") bannerIcon: "Spool" bannerBody: catalog.i18nc("@text", "Streamline your workflow and customize your Ultimaker Cura experience with plugins contributed by our amazing community of users.") readMoreUrl: "" // TODO add when support page is ready onRemoveBanner: function() { - CuraApplication.closeMarketPlaceMaterialBanner(); + UM.Preferences.setValue("cura/market_place_show_material_banner", false); bannerVisible = false; } diff --git a/plugins/Marketplace/resources/qml/Plugins.qml b/plugins/Marketplace/resources/qml/Plugins.qml index f0c101153c..11aabedb85 100644 --- a/plugins/Marketplace/resources/qml/Plugins.qml +++ b/plugins/Marketplace/resources/qml/Plugins.qml @@ -2,17 +2,18 @@ // Cura is released under the terms of the LGPLv3 or higher. import Marketplace 1.0 as Marketplace +import UM 1.4 as UM Packages { pageTitle: catalog.i18nc("@header", "Install Plugins") - bannerVisible: CuraApplication.shouldShowMarketPlacePluginBanner() + bannerVisible: UM.Preferences.getValue("cura/market_place_show_plugin_banner") bannerIcon: "Shop" bannerBody: catalog.i18nc("@text", "Select and install material profiles optimised for your Ultimaker 3D printers.") readMoreUrl: "" // TODO add when support page is ready onRemoveBanner: function() { - CuraApplication.closeMarketPlacePluginBanner(); + UM.Preferences.setValue("cura/market_place_show_plugin_banner", false) bannerVisible = false; } From f7720d2a0d6870a512b1957c3bbe4d26b9156d40 Mon Sep 17 00:00:00 2001 From: casper Date: Mon, 29 Nov 2021 11:53:35 +0100 Subject: [PATCH 038/195] Rename property `bannerBody` to `bannerText` CURA-8564 --- plugins/Marketplace/resources/qml/ManagedPackages.qml | 2 +- plugins/Marketplace/resources/qml/Materials.qml | 2 +- plugins/Marketplace/resources/qml/OnboardBanner.qml | 4 ++-- plugins/Marketplace/resources/qml/Plugins.qml | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/Marketplace/resources/qml/ManagedPackages.qml b/plugins/Marketplace/resources/qml/ManagedPackages.qml index 677a9ee574..0d1472bb6a 100644 --- a/plugins/Marketplace/resources/qml/ManagedPackages.qml +++ b/plugins/Marketplace/resources/qml/ManagedPackages.qml @@ -13,7 +13,7 @@ Packages bannerVisible: UM.Preferences.getValue("cura/market_place_show_manage_packages_banner"); bannerIcon: "ArrowDoubleCircleRight" - bannerBody: catalog.i18nc("@text", "Manage your Ultimaker Cura plugins and material profiles here. Make sure to keep your plugins up to date and backup your setup regularly.") + bannerText: catalog.i18nc("@text", "Manage your Ultimaker Cura plugins and material profiles here. Make sure to keep your plugins up to date and backup your setup regularly.") readMoreUrl: "" // TODO add when support page is ready onRemoveBanner: function() { UM.Preferences.setValue("cura/market_place_show_manage_packages_banner", false); diff --git a/plugins/Marketplace/resources/qml/Materials.qml b/plugins/Marketplace/resources/qml/Materials.qml index e4cd554334..a1d6d91f8c 100644 --- a/plugins/Marketplace/resources/qml/Materials.qml +++ b/plugins/Marketplace/resources/qml/Materials.qml @@ -10,7 +10,7 @@ Packages bannerVisible: UM.Preferences.getValue("cura/market_place_show_material_banner") bannerIcon: "Spool" - bannerBody: catalog.i18nc("@text", "Streamline your workflow and customize your Ultimaker Cura experience with plugins contributed by our amazing community of users.") + bannerText: catalog.i18nc("@text", "Streamline your workflow and customize your Ultimaker Cura experience with plugins contributed by our amazing community of users.") readMoreUrl: "" // TODO add when support page is ready onRemoveBanner: function() { UM.Preferences.setValue("cura/market_place_show_material_banner", false); diff --git a/plugins/Marketplace/resources/qml/OnboardBanner.qml b/plugins/Marketplace/resources/qml/OnboardBanner.qml index 4b5e494be5..8d68512878 100644 --- a/plugins/Marketplace/resources/qml/OnboardBanner.qml +++ b/plugins/Marketplace/resources/qml/OnboardBanner.qml @@ -13,7 +13,7 @@ Rectangle { property bool bannerVisible property string bannerIcon - property string bannerBody + property string bannerText property var onRemoveBanner property string readMoreUrl @@ -82,7 +82,7 @@ Rectangle } font: UM.Theme.getFont("default") - text: bannerBody + text: bannerText renderType: Text.NativeRendering color: "white" diff --git a/plugins/Marketplace/resources/qml/Plugins.qml b/plugins/Marketplace/resources/qml/Plugins.qml index 11aabedb85..884529c3e0 100644 --- a/plugins/Marketplace/resources/qml/Plugins.qml +++ b/plugins/Marketplace/resources/qml/Plugins.qml @@ -10,7 +10,7 @@ Packages bannerVisible: UM.Preferences.getValue("cura/market_place_show_plugin_banner") bannerIcon: "Shop" - bannerBody: catalog.i18nc("@text", "Select and install material profiles optimised for your Ultimaker 3D printers.") + bannerText: catalog.i18nc("@text", "Select and install material profiles optimised for your Ultimaker 3D printers.") readMoreUrl: "" // TODO add when support page is ready onRemoveBanner: function() { UM.Preferences.setValue("cura/market_place_show_plugin_banner", false) From 5dc664e8214e041ebbd86c9f1f43a8c30a14e8ba Mon Sep 17 00:00:00 2001 From: casper Date: Mon, 29 Nov 2021 11:55:33 +0100 Subject: [PATCH 039/195] Use `visible` property of `Rectangle` rather than defining a separate property CURA-8564 --- plugins/Marketplace/resources/qml/OnboardBanner.qml | 3 --- 1 file changed, 3 deletions(-) diff --git a/plugins/Marketplace/resources/qml/OnboardBanner.qml b/plugins/Marketplace/resources/qml/OnboardBanner.qml index 8d68512878..4a65297816 100644 --- a/plugins/Marketplace/resources/qml/OnboardBanner.qml +++ b/plugins/Marketplace/resources/qml/OnboardBanner.qml @@ -11,14 +11,11 @@ import Cura 1.6 as Cura // Onboarding banner. Rectangle { - property bool bannerVisible property string bannerIcon property string bannerText property var onRemoveBanner property string readMoreUrl - visible: bannerVisible - Layout.preferredHeight: childrenRect.height + 2 * UM.Theme.getSize("default_margin").height anchors { From 7e486578723b50ba7866492ddf0a1b4ebd9f2191 Mon Sep 17 00:00:00 2001 From: casper Date: Mon, 29 Nov 2021 12:01:42 +0100 Subject: [PATCH 040/195] Use the `Item` element instead of `Rectangle` for banner icon To prevent un-necessary draw checks as the background is transparent CURA-8564 --- plugins/Marketplace/resources/qml/OnboardBanner.qml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/plugins/Marketplace/resources/qml/OnboardBanner.qml b/plugins/Marketplace/resources/qml/OnboardBanner.qml index 4a65297816..982bc0f501 100644 --- a/plugins/Marketplace/resources/qml/OnboardBanner.qml +++ b/plugins/Marketplace/resources/qml/OnboardBanner.qml @@ -28,7 +28,7 @@ Rectangle color: UM.Theme.getColor("action_panel_secondary") // Icon - Rectangle + Item { id: onboardingIcon anchors @@ -39,7 +39,6 @@ Rectangle } width: UM.Theme.getSize("button_icon").width height: UM.Theme.getSize("button_icon").height - color: "transparent" UM.RecolorImage { anchors.fill: parent From 67fc514b8fbc538e4c9b7f0b9888fe507c0828f6 Mon Sep 17 00:00:00 2001 From: casper Date: Mon, 29 Nov 2021 12:16:52 +0100 Subject: [PATCH 041/195] Use an `alias` property for the banner icon CURA-8564 --- plugins/Marketplace/resources/qml/ManagedPackages.qml | 2 +- plugins/Marketplace/resources/qml/Materials.qml | 2 +- plugins/Marketplace/resources/qml/OnboardBanner.qml | 10 ++-------- plugins/Marketplace/resources/qml/Plugins.qml | 2 +- 4 files changed, 5 insertions(+), 11 deletions(-) diff --git a/plugins/Marketplace/resources/qml/ManagedPackages.qml b/plugins/Marketplace/resources/qml/ManagedPackages.qml index 0d1472bb6a..9ce8408f8e 100644 --- a/plugins/Marketplace/resources/qml/ManagedPackages.qml +++ b/plugins/Marketplace/resources/qml/ManagedPackages.qml @@ -12,7 +12,7 @@ Packages pageTitle: catalog.i18nc("@header", "Manage packages") bannerVisible: UM.Preferences.getValue("cura/market_place_show_manage_packages_banner"); - bannerIcon: "ArrowDoubleCircleRight" + bannerIcon: UM.Theme.getIcon("ArrowDoubleCircleRight") bannerText: catalog.i18nc("@text", "Manage your Ultimaker Cura plugins and material profiles here. Make sure to keep your plugins up to date and backup your setup regularly.") readMoreUrl: "" // TODO add when support page is ready onRemoveBanner: function() { diff --git a/plugins/Marketplace/resources/qml/Materials.qml b/plugins/Marketplace/resources/qml/Materials.qml index a1d6d91f8c..de075af031 100644 --- a/plugins/Marketplace/resources/qml/Materials.qml +++ b/plugins/Marketplace/resources/qml/Materials.qml @@ -9,7 +9,7 @@ Packages pageTitle: catalog.i18nc("@header", "Install Materials") bannerVisible: UM.Preferences.getValue("cura/market_place_show_material_banner") - bannerIcon: "Spool" + bannerIcon: UM.Theme.getIcon("Spool") bannerText: catalog.i18nc("@text", "Streamline your workflow and customize your Ultimaker Cura experience with plugins contributed by our amazing community of users.") readMoreUrl: "" // TODO add when support page is ready onRemoveBanner: function() { diff --git a/plugins/Marketplace/resources/qml/OnboardBanner.qml b/plugins/Marketplace/resources/qml/OnboardBanner.qml index 982bc0f501..b44295c90a 100644 --- a/plugins/Marketplace/resources/qml/OnboardBanner.qml +++ b/plugins/Marketplace/resources/qml/OnboardBanner.qml @@ -11,7 +11,7 @@ import Cura 1.6 as Cura // Onboarding banner. Rectangle { - property string bannerIcon + property alias bannerIcon: onboardingIcon.source; property string bannerText property var onRemoveBanner property string readMoreUrl @@ -28,7 +28,7 @@ Rectangle color: UM.Theme.getColor("action_panel_secondary") // Icon - Item + UM.RecolorImage { id: onboardingIcon anchors @@ -39,12 +39,6 @@ Rectangle } width: UM.Theme.getSize("button_icon").width height: UM.Theme.getSize("button_icon").height - UM.RecolorImage - { - anchors.fill: parent - color: UM.Theme.getColor("primary_text") - source: UM.Theme.getIcon(bannerIcon) - } } // Close button diff --git a/plugins/Marketplace/resources/qml/Plugins.qml b/plugins/Marketplace/resources/qml/Plugins.qml index 884529c3e0..2922a39c9b 100644 --- a/plugins/Marketplace/resources/qml/Plugins.qml +++ b/plugins/Marketplace/resources/qml/Plugins.qml @@ -9,7 +9,7 @@ Packages pageTitle: catalog.i18nc("@header", "Install Plugins") bannerVisible: UM.Preferences.getValue("cura/market_place_show_plugin_banner") - bannerIcon: "Shop" + bannerIcon: UM.Theme.getIcon("Shop") bannerText: catalog.i18nc("@text", "Select and install material profiles optimised for your Ultimaker 3D printers.") readMoreUrl: "" // TODO add when support page is ready onRemoveBanner: function() { From ad1798ee6e6996aa75a2ed22130afdb7f5354c24 Mon Sep 17 00:00:00 2001 From: casper Date: Mon, 29 Nov 2021 12:26:48 +0100 Subject: [PATCH 042/195] Use color from theme for the market place banner banner text CURA-8564 --- plugins/Marketplace/resources/qml/OnboardBanner.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/Marketplace/resources/qml/OnboardBanner.qml b/plugins/Marketplace/resources/qml/OnboardBanner.qml index b44295c90a..148a279cd2 100644 --- a/plugins/Marketplace/resources/qml/OnboardBanner.qml +++ b/plugins/Marketplace/resources/qml/OnboardBanner.qml @@ -75,7 +75,7 @@ Rectangle text: bannerText renderType: Text.NativeRendering - color: "white" + color: UM.Theme.getColor("primary_text") wrapMode: Text.Wrap elide: Text.ElideRight From 8d8de1c6f486506d1257c2337195e678c8a0956e Mon Sep 17 00:00:00 2001 From: casper Date: Mon, 29 Nov 2021 12:34:25 +0100 Subject: [PATCH 043/195] Move static anchor properties inside the `TertiaryButton` element CURA-8564 --- plugins/Marketplace/resources/qml/OnboardBanner.qml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/plugins/Marketplace/resources/qml/OnboardBanner.qml b/plugins/Marketplace/resources/qml/OnboardBanner.qml index 148a279cd2..62f6b96356 100644 --- a/plugins/Marketplace/resources/qml/OnboardBanner.qml +++ b/plugins/Marketplace/resources/qml/OnboardBanner.qml @@ -87,16 +87,12 @@ Rectangle if (line.implicitWidth + readMoreButton.width + UM.Theme.getSize("default_margin").width > width) { // If it does place it after the body text - readMoreButton.anchors.left = infoText.left; - readMoreButton.anchors.bottom = infoText.bottom; readMoreButton.anchors.bottomMargin = -(fontMetrics.height + UM.Theme.getSize("thin_margin").height); readMoreButton.anchors.leftMargin = 0; } else { // Otherwise place it under the text - readMoreButton.anchors.left = infoText.left; - readMoreButton.anchors.bottom = infoText.bottom; readMoreButton.anchors.leftMargin = line.implicitWidth + UM.Theme.getSize("default_margin").width; readMoreButton.anchors.bottomMargin = 0; } @@ -114,6 +110,8 @@ Rectangle { visible: readMoreUrl !== "" id: readMoreButton + anchors.left: infoText.left + anchors.bottom: infoText.bottom text: "Learn More" textFont: UM.Theme.getFont("default") textColor: infoText.color From f10eea4fc4329677d5587478989ac209b7af4001 Mon Sep 17 00:00:00 2001 From: casper Date: Mon, 29 Nov 2021 14:34:15 +0100 Subject: [PATCH 044/195] Remove top anchor from an element used in ColumnLayout This element did need additional margins at the top. Added margins to the ColumnLayout element instead. CURA-8564 --- plugins/Marketplace/resources/qml/OnboardBanner.qml | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/Marketplace/resources/qml/OnboardBanner.qml b/plugins/Marketplace/resources/qml/OnboardBanner.qml index 62f6b96356..0107d6d816 100644 --- a/plugins/Marketplace/resources/qml/OnboardBanner.qml +++ b/plugins/Marketplace/resources/qml/OnboardBanner.qml @@ -22,7 +22,6 @@ Rectangle margins: UM.Theme.getSize("default_margin").width left: parent.left right: parent.right - top: parent.top } color: UM.Theme.getColor("action_panel_secondary") From fe19587b7f657ba88efcb11ff0a748edca5a6608 Mon Sep 17 00:00:00 2001 From: casper Date: Mon, 29 Nov 2021 15:09:33 +0100 Subject: [PATCH 045/195] Rename properties in marketplace onboarding banner CURA-8564 --- plugins/Marketplace/resources/qml/ManagedPackages.qml | 2 +- plugins/Marketplace/resources/qml/Materials.qml | 2 +- plugins/Marketplace/resources/qml/OnboardBanner.qml | 9 ++++----- plugins/Marketplace/resources/qml/Plugins.qml | 2 +- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/plugins/Marketplace/resources/qml/ManagedPackages.qml b/plugins/Marketplace/resources/qml/ManagedPackages.qml index 9ce8408f8e..2610f7cd9d 100644 --- a/plugins/Marketplace/resources/qml/ManagedPackages.qml +++ b/plugins/Marketplace/resources/qml/ManagedPackages.qml @@ -14,7 +14,7 @@ Packages bannerVisible: UM.Preferences.getValue("cura/market_place_show_manage_packages_banner"); bannerIcon: UM.Theme.getIcon("ArrowDoubleCircleRight") bannerText: catalog.i18nc("@text", "Manage your Ultimaker Cura plugins and material profiles here. Make sure to keep your plugins up to date and backup your setup regularly.") - readMoreUrl: "" // TODO add when support page is ready + bannerReadMoreUrl: "" // TODO add when support page is ready onRemoveBanner: function() { UM.Preferences.setValue("cura/market_place_show_manage_packages_banner", false); bannerVisible = false; diff --git a/plugins/Marketplace/resources/qml/Materials.qml b/plugins/Marketplace/resources/qml/Materials.qml index de075af031..2634f7b328 100644 --- a/plugins/Marketplace/resources/qml/Materials.qml +++ b/plugins/Marketplace/resources/qml/Materials.qml @@ -11,7 +11,7 @@ Packages bannerVisible: UM.Preferences.getValue("cura/market_place_show_material_banner") bannerIcon: UM.Theme.getIcon("Spool") bannerText: catalog.i18nc("@text", "Streamline your workflow and customize your Ultimaker Cura experience with plugins contributed by our amazing community of users.") - readMoreUrl: "" // TODO add when support page is ready + bannerReadMoreUrl: "" // TODO add when support page is ready onRemoveBanner: function() { UM.Preferences.setValue("cura/market_place_show_material_banner", false); bannerVisible = false; diff --git a/plugins/Marketplace/resources/qml/OnboardBanner.qml b/plugins/Marketplace/resources/qml/OnboardBanner.qml index 0107d6d816..0dbe2cb897 100644 --- a/plugins/Marketplace/resources/qml/OnboardBanner.qml +++ b/plugins/Marketplace/resources/qml/OnboardBanner.qml @@ -11,9 +11,9 @@ import Cura 1.6 as Cura // Onboarding banner. Rectangle { - property alias bannerIcon: onboardingIcon.source; - property string bannerText - property var onRemoveBanner + property alias icon: onboardingIcon.source + property alias text: infoText.text + property var onRemove property string readMoreUrl Layout.preferredHeight: childrenRect.height + 2 * UM.Theme.getSize("default_margin").height @@ -56,7 +56,7 @@ Rectangle hoverColor: UM.Theme.getColor("primary_text_hover") iconSource: UM.Theme.getIcon("Cancel") - onClicked: onRemoveBanner() + onClicked: onRemove() } // Body @@ -71,7 +71,6 @@ Rectangle } font: UM.Theme.getFont("default") - text: bannerText renderType: Text.NativeRendering color: UM.Theme.getColor("primary_text") diff --git a/plugins/Marketplace/resources/qml/Plugins.qml b/plugins/Marketplace/resources/qml/Plugins.qml index 2922a39c9b..29b264c702 100644 --- a/plugins/Marketplace/resources/qml/Plugins.qml +++ b/plugins/Marketplace/resources/qml/Plugins.qml @@ -11,7 +11,7 @@ Packages bannerVisible: UM.Preferences.getValue("cura/market_place_show_plugin_banner") bannerIcon: UM.Theme.getIcon("Shop") bannerText: catalog.i18nc("@text", "Select and install material profiles optimised for your Ultimaker 3D printers.") - readMoreUrl: "" // TODO add when support page is ready + bannerReadMoreUrl: "" // TODO add when support page is ready onRemoveBanner: function() { UM.Preferences.setValue("cura/market_place_show_plugin_banner", false) bannerVisible = false; From 61275ed9bc7b4247219b6e54fad2db2a0e8c58bb Mon Sep 17 00:00:00 2001 From: casper Date: Tue, 30 Nov 2021 10:51:43 +0100 Subject: [PATCH 046/195] Easy navigation to Cloud marketplace CURA-8563 --- plugins/Marketplace/resources/qml/ManagedPackages.qml | 1 + plugins/Marketplace/resources/qml/Materials.qml | 1 + plugins/Marketplace/resources/qml/Plugins.qml | 1 + 3 files changed, 3 insertions(+) diff --git a/plugins/Marketplace/resources/qml/ManagedPackages.qml b/plugins/Marketplace/resources/qml/ManagedPackages.qml index 2610f7cd9d..b90bffd723 100644 --- a/plugins/Marketplace/resources/qml/ManagedPackages.qml +++ b/plugins/Marketplace/resources/qml/ManagedPackages.qml @@ -19,6 +19,7 @@ Packages UM.Preferences.setValue("cura/market_place_show_manage_packages_banner", false); bannerVisible = false; } + searchInBrowserUrl: "https://marketplace.ultimaker.com/app/cura/plugins" model: Marketplace.LocalPackageList { diff --git a/plugins/Marketplace/resources/qml/Materials.qml b/plugins/Marketplace/resources/qml/Materials.qml index 2634f7b328..3afe7b412a 100644 --- a/plugins/Marketplace/resources/qml/Materials.qml +++ b/plugins/Marketplace/resources/qml/Materials.qml @@ -16,6 +16,7 @@ Packages UM.Preferences.setValue("cura/market_place_show_material_banner", false); bannerVisible = false; } + searchInBrowserUrl: "https://marketplace.ultimaker.com/app/cura/materials" model: Marketplace.RemotePackageList { diff --git a/plugins/Marketplace/resources/qml/Plugins.qml b/plugins/Marketplace/resources/qml/Plugins.qml index 29b264c702..c473a3a48e 100644 --- a/plugins/Marketplace/resources/qml/Plugins.qml +++ b/plugins/Marketplace/resources/qml/Plugins.qml @@ -16,6 +16,7 @@ Packages UM.Preferences.setValue("cura/market_place_show_plugin_banner", false) bannerVisible = false; } + searchInBrowserUrl: "https://marketplace.ultimaker.com/app/cura/plugins" model: Marketplace.RemotePackageList { From 628be10e984887cb66c2207a34865020333e5a97 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 30 Nov 2021 14:17:40 +0100 Subject: [PATCH 047/195] Add campaign links CURA-8563 --- plugins/Marketplace/resources/qml/ManagedPackages.qml | 2 +- plugins/Marketplace/resources/qml/Materials.qml | 2 +- plugins/Marketplace/resources/qml/Plugins.qml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/Marketplace/resources/qml/ManagedPackages.qml b/plugins/Marketplace/resources/qml/ManagedPackages.qml index b90bffd723..f44fbd0a9b 100644 --- a/plugins/Marketplace/resources/qml/ManagedPackages.qml +++ b/plugins/Marketplace/resources/qml/ManagedPackages.qml @@ -19,7 +19,7 @@ Packages UM.Preferences.setValue("cura/market_place_show_manage_packages_banner", false); bannerVisible = false; } - searchInBrowserUrl: "https://marketplace.ultimaker.com/app/cura/plugins" + searchInBrowserUrl: "https://marketplace.ultimaker.com/app/cura/plugins?utm_source=cura&utm_medium=software&utm_campaign=marketplace-search-plugins-browser" model: Marketplace.LocalPackageList { diff --git a/plugins/Marketplace/resources/qml/Materials.qml b/plugins/Marketplace/resources/qml/Materials.qml index 3afe7b412a..489915aa10 100644 --- a/plugins/Marketplace/resources/qml/Materials.qml +++ b/plugins/Marketplace/resources/qml/Materials.qml @@ -16,7 +16,7 @@ Packages UM.Preferences.setValue("cura/market_place_show_material_banner", false); bannerVisible = false; } - searchInBrowserUrl: "https://marketplace.ultimaker.com/app/cura/materials" + searchInBrowserUrl: "https://marketplace.ultimaker.com/app/cura/materials?utm_source=cura&utm_medium=software&utm_campaign=marketplace-search-materials-browser" model: Marketplace.RemotePackageList { diff --git a/plugins/Marketplace/resources/qml/Plugins.qml b/plugins/Marketplace/resources/qml/Plugins.qml index c473a3a48e..3b0b5d7c23 100644 --- a/plugins/Marketplace/resources/qml/Plugins.qml +++ b/plugins/Marketplace/resources/qml/Plugins.qml @@ -16,7 +16,7 @@ Packages UM.Preferences.setValue("cura/market_place_show_plugin_banner", false) bannerVisible = false; } - searchInBrowserUrl: "https://marketplace.ultimaker.com/app/cura/plugins" + searchInBrowserUrl: "https://marketplace.ultimaker.com/app/cura/plugins?utm_source=cura&utm_medium=software&utm_campaign=marketplace-search-plugins-browser" model: Marketplace.RemotePackageList { From e83f8e4b8a4ea91533c7990ccbc4856b8f0450e8 Mon Sep 17 00:00:00 2001 From: casper Date: Tue, 30 Nov 2021 12:08:09 +0100 Subject: [PATCH 048/195] Add correct text to material and plugins onboarding banners CURA-8564 --- plugins/Marketplace/resources/qml/Materials.qml | 2 +- plugins/Marketplace/resources/qml/Plugins.qml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/Marketplace/resources/qml/Materials.qml b/plugins/Marketplace/resources/qml/Materials.qml index 489915aa10..39d283b0a5 100644 --- a/plugins/Marketplace/resources/qml/Materials.qml +++ b/plugins/Marketplace/resources/qml/Materials.qml @@ -10,7 +10,7 @@ Packages bannerVisible: UM.Preferences.getValue("cura/market_place_show_material_banner") bannerIcon: UM.Theme.getIcon("Spool") - bannerText: catalog.i18nc("@text", "Streamline your workflow and customize your Ultimaker Cura experience with plugins contributed by our amazing community of users.") + bannerText: catalog.i18nc("@text", "Select and install material profiles optimised for your Ultimaker 3D printers.") bannerReadMoreUrl: "" // TODO add when support page is ready onRemoveBanner: function() { UM.Preferences.setValue("cura/market_place_show_material_banner", false); diff --git a/plugins/Marketplace/resources/qml/Plugins.qml b/plugins/Marketplace/resources/qml/Plugins.qml index 3b0b5d7c23..538afc827a 100644 --- a/plugins/Marketplace/resources/qml/Plugins.qml +++ b/plugins/Marketplace/resources/qml/Plugins.qml @@ -10,7 +10,7 @@ Packages bannerVisible: UM.Preferences.getValue("cura/market_place_show_plugin_banner") bannerIcon: UM.Theme.getIcon("Shop") - bannerText: catalog.i18nc("@text", "Select and install material profiles optimised for your Ultimaker 3D printers.") + bannerText: catalog.i18nc("@text", "Streamline your workflow and customize your Ultimaker Cura experience with plugins contributed by our amazing community of users.") bannerReadMoreUrl: "" // TODO add when support page is ready onRemoveBanner: function() { UM.Preferences.setValue("cura/market_place_show_plugin_banner", false) From 2618ad4a0fe021f3796ebe8c194f70eb1117d46a Mon Sep 17 00:00:00 2001 From: casper Date: Tue, 30 Nov 2021 12:10:55 +0100 Subject: [PATCH 049/195] Always show read more button in on boarding banner Even if there is no link CURA-8564 --- plugins/Marketplace/resources/qml/OnboardBanner.qml | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/Marketplace/resources/qml/OnboardBanner.qml b/plugins/Marketplace/resources/qml/OnboardBanner.qml index 0dbe2cb897..90af5f9b4f 100644 --- a/plugins/Marketplace/resources/qml/OnboardBanner.qml +++ b/plugins/Marketplace/resources/qml/OnboardBanner.qml @@ -106,7 +106,6 @@ Rectangle Cura.TertiaryButton { - visible: readMoreUrl !== "" id: readMoreButton anchors.left: infoText.left anchors.bottom: infoText.bottom From b07b5d5bc4826bd4c7a1571e6c64bf30ed5f2885 Mon Sep 17 00:00:00 2001 From: casper Date: Tue, 30 Nov 2021 12:11:32 +0100 Subject: [PATCH 050/195] Change margins of read more button in marketplace onboarding banner To comply with UX design CURA-8564 --- plugins/Marketplace/resources/qml/OnboardBanner.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/Marketplace/resources/qml/OnboardBanner.qml b/plugins/Marketplace/resources/qml/OnboardBanner.qml index 90af5f9b4f..a2c1613bcb 100644 --- a/plugins/Marketplace/resources/qml/OnboardBanner.qml +++ b/plugins/Marketplace/resources/qml/OnboardBanner.qml @@ -85,8 +85,8 @@ Rectangle if (line.implicitWidth + readMoreButton.width + UM.Theme.getSize("default_margin").width > width) { // If it does place it after the body text - readMoreButton.anchors.bottomMargin = -(fontMetrics.height + UM.Theme.getSize("thin_margin").height); - readMoreButton.anchors.leftMargin = 0; + readMoreButton.anchors.bottomMargin = -(fontMetrics.height); + readMoreButton.anchors.leftMargin = UM.Theme.getSize("thin_margin").width; } else { From 52c8e6b21700aa7da819461af2d4d6d2a9ff5a38 Mon Sep 17 00:00:00 2001 From: casper Date: Tue, 30 Nov 2021 12:19:32 +0100 Subject: [PATCH 051/195] Decrease size of the icons in the marketplace onboarding banners CURA-8564 --- plugins/Marketplace/resources/qml/OnboardBanner.qml | 4 ++-- resources/themes/cura-light/theme.json | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/plugins/Marketplace/resources/qml/OnboardBanner.qml b/plugins/Marketplace/resources/qml/OnboardBanner.qml index a2c1613bcb..f77f8bee97 100644 --- a/plugins/Marketplace/resources/qml/OnboardBanner.qml +++ b/plugins/Marketplace/resources/qml/OnboardBanner.qml @@ -36,8 +36,8 @@ Rectangle left: parent.left margins: UM.Theme.getSize("default_margin").width } - width: UM.Theme.getSize("button_icon").width - height: UM.Theme.getSize("button_icon").height + width: UM.Theme.getSize("banner_icon_size").width + height: UM.Theme.getSize("banner_icon_size").height } // Close button diff --git a/resources/themes/cura-light/theme.json b/resources/themes/cura-light/theme.json index 6dec15aff8..8ca9f72ca8 100644 --- a/resources/themes/cura-light/theme.json +++ b/resources/themes/cura-light/theme.json @@ -684,6 +684,8 @@ "table_row": [2.0, 2.0], "welcome_wizard_content_image_big": [18, 15], - "welcome_wizard_cloud_content_image": [4, 4] + "welcome_wizard_cloud_content_image": [4, 4], + + "banner_icon_size": [2.0, 2.0] } } From df66bcb541f9edc1aff284239c912f3fc3d043d3 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Tue, 30 Nov 2021 19:00:08 +0100 Subject: [PATCH 052/195] Add additional buttons at the bottom for materials with links to data sheets And where to buy it. Contributes to issue CURA-8585. --- resources/themes/cura-light/icons/default/DocumentFilled.svg | 3 +++ resources/themes/cura-light/icons/default/ShoppingCart.svg | 3 +++ 2 files changed, 6 insertions(+) create mode 100644 resources/themes/cura-light/icons/default/DocumentFilled.svg create mode 100644 resources/themes/cura-light/icons/default/ShoppingCart.svg diff --git a/resources/themes/cura-light/icons/default/DocumentFilled.svg b/resources/themes/cura-light/icons/default/DocumentFilled.svg new file mode 100644 index 0000000000..bb654fea33 --- /dev/null +++ b/resources/themes/cura-light/icons/default/DocumentFilled.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/themes/cura-light/icons/default/ShoppingCart.svg b/resources/themes/cura-light/icons/default/ShoppingCart.svg new file mode 100644 index 0000000000..b3fece3fab --- /dev/null +++ b/resources/themes/cura-light/icons/default/ShoppingCart.svg @@ -0,0 +1,3 @@ + + + From c1cffa09fe51b61de3024d2b0511f97084b0f89e Mon Sep 17 00:00:00 2001 From: Remco Burema Date: Wed, 1 Dec 2021 13:22:50 +0100 Subject: [PATCH 053/195] Solve layout warnings. --- plugins/Marketplace/resources/qml/Marketplace.qml | 2 +- plugins/Marketplace/resources/qml/OnboardBanner.qml | 8 ++------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/plugins/Marketplace/resources/qml/Marketplace.qml b/plugins/Marketplace/resources/qml/Marketplace.qml index a02c0e0d5f..bee825d955 100644 --- a/plugins/Marketplace/resources/qml/Marketplace.qml +++ b/plugins/Marketplace/resources/qml/Marketplace.qml @@ -117,7 +117,7 @@ Window TabBar { id: pageSelectionTabBar - anchors.right: parent.right + Layout.alignment: Qt.AlignRight height: UM.Theme.getSize("button_icon").height spacing: 0 background: Rectangle { color: "transparent" } diff --git a/plugins/Marketplace/resources/qml/OnboardBanner.qml b/plugins/Marketplace/resources/qml/OnboardBanner.qml index f77f8bee97..25e4b53241 100644 --- a/plugins/Marketplace/resources/qml/OnboardBanner.qml +++ b/plugins/Marketplace/resources/qml/OnboardBanner.qml @@ -17,12 +17,8 @@ Rectangle property string readMoreUrl Layout.preferredHeight: childrenRect.height + 2 * UM.Theme.getSize("default_margin").height - anchors - { - margins: UM.Theme.getSize("default_margin").width - left: parent.left - right: parent.right - } + Layout.fillWidth: true + Layout.margins: UM.Theme.getSize("default_margin").width color: UM.Theme.getColor("action_panel_secondary") From 327b434788e246a7783704bb73cca27ddca9da51 Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Wed, 1 Dec 2021 18:32:14 +0100 Subject: [PATCH 054/195] fix merge conflict Contributes to: CURA-8587 --- plugins/Marketplace/PackageList.py | 3 ++- plugins/Marketplace/resources/qml/ManageButton.qml | 4 ++++ plugins/Marketplace/resources/qml/Packages.qml | 1 - 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/plugins/Marketplace/PackageList.py b/plugins/Marketplace/PackageList.py index 798a15324e..9c21c03745 100644 --- a/plugins/Marketplace/PackageList.py +++ b/plugins/Marketplace/PackageList.py @@ -7,7 +7,8 @@ from typing import Optional, TYPE_CHECKING from UM.i18n import i18nCatalog from UM.Qt.ListModel import ListModel from UM.TaskManagement.HttpRequestScope import JsonDecoratorScope # To request JSON responses from the API. -from UM.TaskManagement.HttpRequestManager import HttpRequestData # To request the package list from the API. +from UM.TaskManagement.HttpRequestManager import HttpRequestData # To request the package list from the API +from UM.Logger import Logger from cura.CuraApplication import CuraApplication from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope # To make requests to the Ultimaker API with correct authorization. diff --git a/plugins/Marketplace/resources/qml/ManageButton.qml b/plugins/Marketplace/resources/qml/ManageButton.qml index e58347124a..889a8ce7f3 100644 --- a/plugins/Marketplace/resources/qml/ManageButton.qml +++ b/plugins/Marketplace/resources/qml/ManageButton.qml @@ -17,6 +17,8 @@ RowLayout property string busySecondaryText: busyMessageText.text property string mainState: "primary" + signal clicked + state: mainState Cura.PrimaryButton @@ -26,6 +28,7 @@ RowLayout onClicked: { + manageButton.clicked() manageButton.state = "busy" } } @@ -37,6 +40,7 @@ RowLayout onClicked: { + manageButton.clicked() manageButton.state = "busy" } } diff --git a/plugins/Marketplace/resources/qml/Packages.qml b/plugins/Marketplace/resources/qml/Packages.qml index 3c627aef32..275792ed42 100644 --- a/plugins/Marketplace/resources/qml/Packages.qml +++ b/plugins/Marketplace/resources/qml/Packages.qml @@ -38,7 +38,6 @@ ListView text: section font: UM.Theme.getFont("large") color: UM.Theme.getColor("text") - onTextChanged: print(text) } } From ff5a4a4f5afbd15a187a50916015dfdcff845d25 Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Thu, 2 Dec 2021 08:21:57 +0100 Subject: [PATCH 055/195] Adding functionality to the manageButtons Contributes to: CURA-8587 --- plugins/Marketplace/LocalPackageList.py | 4 ++- plugins/Marketplace/PackageList.py | 27 +++++++++++++++++++ plugins/Marketplace/RemotePackageList.py | 1 + .../resources/qml/ManageButton.qml | 6 ++--- 4 files changed, 34 insertions(+), 4 deletions(-) diff --git a/plugins/Marketplace/LocalPackageList.py b/plugins/Marketplace/LocalPackageList.py index bbe74a9056..be805fb002 100644 --- a/plugins/Marketplace/LocalPackageList.py +++ b/plugins/Marketplace/LocalPackageList.py @@ -60,7 +60,9 @@ class LocalPackageList(PackageList): bundled_or_installed = "installed" if self._manager.isUserInstalledPackage(package_info["package_id"]) else "bundled" package_type = package_info["package_type"] section_title = self.PACKAGE_CATEGORIES[bundled_or_installed][package_type] - return PackageModel(package_info, section_title = section_title, parent = self) + package = PackageModel(package_info, section_title = section_title, parent = self) + self._connectManageButtonSignals(package) + return package def checkForUpdates(self, packages: List[Dict[str, Any]]): installed_packages = "installed_packages=".join([f"{package['package_id']}:{package['package_version']}&" for package in packages]) diff --git a/plugins/Marketplace/PackageList.py b/plugins/Marketplace/PackageList.py index 9c21c03745..1ce8d3fe1d 100644 --- a/plugins/Marketplace/PackageList.py +++ b/plugins/Marketplace/PackageList.py @@ -101,3 +101,30 @@ class PackageList(ListModel): """ Indicating if the PackageList should have a Footer visible. For paginated PackageLists :return: ``True`` if a Footer should be displayed in the ListView, e.q.: paginated lists, ``False`` Otherwise""" return self._has_footer + + def _connectManageButtonSignals(self, package): + package.installPackageTriggered.connect(self.installPackage) + package.uninstallPackageTriggered.connect(self.uninstallPackage) + package.updatePackageTriggered.connect(self.updatePackage) + package.enablePackageTriggered.connect(self.enablePackage) + package.disablePackageTriggered.connect(self.disablePackage) + + @pyqtSlot(str) + def installPackage(self, package_id): + Logger.debug(f"Installing {package_id}") + + @pyqtSlot(str) + def uninstallPackage(self, package_id): + Logger.debug(f"Uninstalling {package_id}") + + @pyqtSlot(str) + def updatePackage(self, package_id): + Logger.debug(f"Updating {package_id}") + + @pyqtSlot(str) + def enablePackage(self, package_id): + Logger.debug(f"Enabling {package_id}") + + @pyqtSlot(str) + def disablePackage(self, package_id): + Logger.debug(f"Disabling {package_id}") diff --git a/plugins/Marketplace/RemotePackageList.py b/plugins/Marketplace/RemotePackageList.py index d5c0763609..63370042e7 100644 --- a/plugins/Marketplace/RemotePackageList.py +++ b/plugins/Marketplace/RemotePackageList.py @@ -133,6 +133,7 @@ class RemotePackageList(PackageList): continue # We should only show packages which are not already installed try: package = PackageModel(package_data, parent = self) + self._connectManageButtonSignals(package) self.appendItem({"package": package}) # Add it to this list model. except RuntimeError: # Setting the ownership of this object to not qml can still result in a RuntimeError. Which can occur when quickly toggling diff --git a/plugins/Marketplace/resources/qml/ManageButton.qml b/plugins/Marketplace/resources/qml/ManageButton.qml index 889a8ce7f3..b13a31c5cf 100644 --- a/plugins/Marketplace/resources/qml/ManageButton.qml +++ b/plugins/Marketplace/resources/qml/ManageButton.qml @@ -17,7 +17,7 @@ RowLayout property string busySecondaryText: busyMessageText.text property string mainState: "primary" - signal clicked + signal clicked(bool primary_action) state: mainState @@ -28,7 +28,7 @@ RowLayout onClicked: { - manageButton.clicked() + manageButton.clicked(true) manageButton.state = "busy" } } @@ -40,7 +40,7 @@ RowLayout onClicked: { - manageButton.clicked() + manageButton.clicked(false) manageButton.state = "busy" } } From 08067432c6115a669eafbca7088ba2eaf6fa4cae Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Thu, 2 Dec 2021 08:54:40 +0100 Subject: [PATCH 056/195] disable other manageButtons when actions is performed Still need to make this transfer to the detaile card Contributes to: CURA-8587 --- plugins/Marketplace/resources/qml/ManageButton.qml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plugins/Marketplace/resources/qml/ManageButton.qml b/plugins/Marketplace/resources/qml/ManageButton.qml index b13a31c5cf..035c369fd8 100644 --- a/plugins/Marketplace/resources/qml/ManageButton.qml +++ b/plugins/Marketplace/resources/qml/ManageButton.qml @@ -16,6 +16,8 @@ RowLayout property string busyPrimaryText: busyMessageText.text property string busySecondaryText: busyMessageText.text property string mainState: "primary" + property bool enabled: true + readonly property bool busy: state == "busy" signal clicked(bool primary_action) @@ -25,6 +27,7 @@ RowLayout { id: primaryButton visible: false + enabled: manageButton.enabled onClicked: { @@ -37,6 +40,7 @@ RowLayout { id: secondaryButton visible: false + enabled: manageButton.enabled onClicked: { From 3b3d9860581860939b4ef257e44045ef15c2ad27 Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Thu, 2 Dec 2021 18:02:49 +0100 Subject: [PATCH 057/195] Groundwork for installing/updating packages Contributes to: CURA-8587 --- cura/CuraPackageManager.py | 6 +- plugins/Marketplace/LocalPackageList.py | 5 +- plugins/Marketplace/PackageList.py | 68 +++++++++++++++++-- .../resources/qml/ManageButton.qml | 8 +-- 4 files changed, 73 insertions(+), 14 deletions(-) diff --git a/cura/CuraPackageManager.py b/cura/CuraPackageManager.py index a8400bfae7..34d8c5c61f 100644 --- a/cura/CuraPackageManager.py +++ b/cura/CuraPackageManager.py @@ -20,12 +20,16 @@ class CuraPackageManager(PackageManager): def __init__(self, application: "QtApplication", parent: Optional["QObject"] = None) -> None: super().__init__(application, parent) self._locally_installed_packages = None + self.installedPackagesChanged.connect(self._updateLocallyInstalledPackages) + + def _updateLocallyInstalledPackages(self): + self._locally_installed_packages = list(self.iterateAllLocalPackages()) @property def locally_installed_packages(self): """locally installed packages, lazy execution""" if self._locally_installed_packages is None: - self._locally_installed_packages = list(self.iterateAllLocalPackages()) + self._updateLocallyInstalledPackages() return self._locally_installed_packages @locally_installed_packages.setter diff --git a/plugins/Marketplace/LocalPackageList.py b/plugins/Marketplace/LocalPackageList.py index be805fb002..4721224e16 100644 --- a/plugins/Marketplace/LocalPackageList.py +++ b/plugins/Marketplace/LocalPackageList.py @@ -89,7 +89,8 @@ class LocalPackageList(PackageList): return for package_data in response_data["data"]: - index = self.find("package", package_data["package_id"]) - self.getItem(index)["package"].canUpdate = True + package = self._getPackageModel(package_data["package_id"]) + package.download_url = package_data.get("download_url", "") + package.canUpdate = True self.sort(attrgetter("sectionTitle", "canUpdate", "displayName"), key = "package", reverse = True) diff --git a/plugins/Marketplace/PackageList.py b/plugins/Marketplace/PackageList.py index 1ce8d3fe1d..32eb302e97 100644 --- a/plugins/Marketplace/PackageList.py +++ b/plugins/Marketplace/PackageList.py @@ -1,18 +1,21 @@ # Copyright (c) 2021 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. +import tempfile from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, Qt -from typing import Optional, TYPE_CHECKING +from typing import Dict, Optional, TYPE_CHECKING from UM.i18n import i18nCatalog from UM.Qt.ListModel import ListModel -from UM.TaskManagement.HttpRequestScope import JsonDecoratorScope # To request JSON responses from the API. -from UM.TaskManagement.HttpRequestManager import HttpRequestData # To request the package list from the API +from UM.TaskManagement.HttpRequestScope import JsonDecoratorScope +from UM.TaskManagement.HttpRequestManager import HttpRequestData , HttpRequestManager from UM.Logger import Logger from cura.CuraApplication import CuraApplication from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope # To make requests to the Ultimaker API with correct authorization. +from .PackageModel import PackageModel + if TYPE_CHECKING: from PyQt5.QtCore import QObject @@ -24,6 +27,7 @@ class PackageList(ListModel): such as Packages obtained from Remote or Local source """ PackageRole = Qt.UserRole + 1 + DISK_WRITE_BUFFER_SIZE = 256 * 1024 # 256 KB def __init__(self, parent: Optional["QObject"] = None) -> None: super().__init__(parent) @@ -33,6 +37,8 @@ class PackageList(ListModel): self._is_loading = False self._has_more = False self._has_footer = True + self._to_install: Dict[str, str] = {} + self.canInstallChanged.connect(self._install) self._ongoing_request: Optional[HttpRequestData] = None self._scope = JsonDecoratorScope(UltimakerCloudScope(CuraApplication.getInstance())) @@ -105,13 +111,60 @@ class PackageList(ListModel): def _connectManageButtonSignals(self, package): package.installPackageTriggered.connect(self.installPackage) package.uninstallPackageTriggered.connect(self.uninstallPackage) - package.updatePackageTriggered.connect(self.updatePackage) + package.updatePackageTriggered.connect(self.installPackage) package.enablePackageTriggered.connect(self.enablePackage) package.disablePackageTriggered.connect(self.disablePackage) + def _getPackageModel(self, package_id: str) -> PackageModel: + index = self.find("package", package_id) + return self.getItem(index)["package"] + + canInstallChanged = pyqtSignal(str, bool) + + def download(self, package_id, url, update: bool = False): + + def downloadFinished(reply: "QNetworkReply") -> None: + self._downloadFinished(package_id, reply, update) + + HttpRequestManager.getInstance().get( + url, + scope = self._scope, + callback = downloadFinished + ) + + def _downloadFinished(self, package_id: str, reply: "QNetworkReply", update: bool = False) -> None: + try: + with tempfile.NamedTemporaryFile(mode = "wb+", suffix = ".curapackage", delete = False) as temp_file: + bytes_read = reply.read(self.DISK_WRITE_BUFFER_SIZE) + while bytes_read: + temp_file.write(bytes_read) + bytes_read = reply.read(self.DISK_WRITE_BUFFER_SIZE) + Logger.debug(f"Finished downloading {package_id} and stored it as {temp_file.name}") + self._to_install[package_id] = temp_file.name + self.canInstallChanged.emit(package_id, update) + except IOError as e: + Logger.logException("e", "Failed to write downloaded package to temp file", e) + temp_file.close() + @pyqtSlot(str) - def installPackage(self, package_id): + def installPackage(self, package_id: str) -> None: + package = self._getPackageModel(package_id) + url = package.download_url + Logger.debug(f"Trying to download and install {package_id} from {url}") + self.download(package_id, url) + + def _install(self, package_id: str, update: bool = False) -> None: + package_path = self._to_install.pop(package_id) Logger.debug(f"Installing {package_id}") + to_be_installed = self._manager.installPackage(package_path) != None + package = self._getPackageModel(package_id) + if package.canUpdate and to_be_installed: + package.canUpdate = False + package.setManageInstallState(to_be_installed) + if update: + package.setIsUpdating(False) + else: + package.setIsInstalling(False) @pyqtSlot(str) def uninstallPackage(self, package_id): @@ -119,7 +172,10 @@ class PackageList(ListModel): @pyqtSlot(str) def updatePackage(self, package_id): - Logger.debug(f"Updating {package_id}") + package = self._getPackageModel(package_id) + url = package.download_url + Logger.debug(f"Trying to download and update {package_id} from {url}") + self.download(package_id, url, True) @pyqtSlot(str) def enablePackage(self, package_id): diff --git a/plugins/Marketplace/resources/qml/ManageButton.qml b/plugins/Marketplace/resources/qml/ManageButton.qml index 035c369fd8..797e83830f 100644 --- a/plugins/Marketplace/resources/qml/ManageButton.qml +++ b/plugins/Marketplace/resources/qml/ManageButton.qml @@ -17,11 +17,11 @@ RowLayout property string busySecondaryText: busyMessageText.text property string mainState: "primary" property bool enabled: true - readonly property bool busy: state == "busy" + property bool busy: false signal clicked(bool primary_action) - state: mainState + state: busy ? "busy" : mainState Cura.PrimaryButton { @@ -32,7 +32,6 @@ RowLayout onClicked: { manageButton.clicked(true) - manageButton.state = "busy" } } @@ -45,7 +44,6 @@ RowLayout onClicked: { manageButton.clicked(false) - manageButton.state = "busy" } } @@ -155,7 +153,7 @@ RowLayout PropertyChanges { target: busyMessage - visible: true + visible: manageButton.visible } } ] From 00acfe9d72dcd77af586a8abe3c16c83f572c5e6 Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Fri, 3 Dec 2021 11:15:04 +0100 Subject: [PATCH 058/195] Added uninstall functionality Get it in a sharable state Contributes to: CURA-8587 --- cura/CuraPackageManager.py | 1 - plugins/Marketplace/PackageList.py | 28 ++++++++++++++++++++++++++-- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/cura/CuraPackageManager.py b/cura/CuraPackageManager.py index 34d8c5c61f..e6123c1947 100644 --- a/cura/CuraPackageManager.py +++ b/cura/CuraPackageManager.py @@ -69,7 +69,6 @@ class CuraPackageManager(PackageManager): def iterateAllLocalPackages(self) -> Generator[Dict[str, Any], None, None]: """ A generator which returns an unordered list of all the PackageModels""" - # Get all the installed packages, add a section_title depending on package_type and user installed for packages in self.getAllInstalledPackagesInfo().values(): for package_info in packages: yield package_info diff --git a/plugins/Marketplace/PackageList.py b/plugins/Marketplace/PackageList.py index 32eb302e97..d988ce94bf 100644 --- a/plugins/Marketplace/PackageList.py +++ b/plugins/Marketplace/PackageList.py @@ -12,6 +12,8 @@ from UM.TaskManagement.HttpRequestManager import HttpRequestData , HttpRequestMa from UM.Logger import Logger from cura.CuraApplication import CuraApplication +from cura.API.Account import Account +from cura import CuraPackageManager from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope # To make requests to the Ultimaker API with correct authorization. from .PackageModel import PackageModel @@ -31,7 +33,8 @@ class PackageList(ListModel): def __init__(self, parent: Optional["QObject"] = None) -> None: super().__init__(parent) - self._manager = CuraApplication.getInstance().getPackageManager() + self._manager: CuraPackageManager = CuraApplication.getInstance().getPackageManager() + self._account: Account = CuraApplication.getInstance().getCuraAPI().account self._error_message = "" self.addRoleName(self.PackageRole, "package") self._is_loading = False @@ -126,7 +129,7 @@ class PackageList(ListModel): def downloadFinished(reply: "QNetworkReply") -> None: self._downloadFinished(package_id, reply, update) - HttpRequestManager.getInstance().get( + self._ongoing_request = HttpRequestManager.getInstance().get( url, scope = self._scope, callback = downloadFinished @@ -165,13 +168,34 @@ class PackageList(ListModel): package.setIsUpdating(False) else: package.setIsInstalling(False) + #self._subscribe(package_id) + + def _subscribe(self, package_id: str) -> None: + if self._account.isLoggedIn: + Logger.debug(f"Subscribing the user for package: {package_id}") + self._ongoing_request = HttpRequestManager.getInstance().put( + url = "", + data = {}, + scope = self._scope + ) @pyqtSlot(str) def uninstallPackage(self, package_id): Logger.debug(f"Uninstalling {package_id}") + package = self._getPackageModel(package_id) + self._manager.removePackage(package_id) + package.setIsInstalling(False) + package.setManageInstallState(False) + #self._unsunscribe(package_id) + + def _unsunscribe(self, package_id: str) -> None: + if self._account.isLoggedIn: + Logger.debug(f"Unsubscribing the user for package: {package_id}") + self._ongoing_request = HttpRequestManager.getInstance().delete(url = "", scope = self._scope) @pyqtSlot(str) def updatePackage(self, package_id): + self._manager.removePackage(package_id, force_add = True) package = self._getPackageModel(package_id) url = package.download_url Logger.debug(f"Trying to download and update {package_id} from {url}") From e37f08790ff77674753c4631f6d4807c5a846d6f Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Mon, 29 Nov 2021 12:32:00 +0100 Subject: [PATCH 059/195] Fixed a merge conflict And not in a neat way. I'm a shamed to say copy-paste was used extensively. Contributes to: CURA-8587 --- plugins/Marketplace/PackageModel.py | 77 +++++++++++++++--- .../Marketplace/resources/qml/PackageCard.qml | 78 +++++++++++++++---- 2 files changed, 130 insertions(+), 25 deletions(-) diff --git a/plugins/Marketplace/PackageModel.py b/plugins/Marketplace/PackageModel.py index d123550c28..c9164bbe26 100644 --- a/plugins/Marketplace/PackageModel.py +++ b/plugins/Marketplace/PackageModel.py @@ -3,7 +3,7 @@ from PyQt5.QtCore import pyqtProperty, QObject, pyqtSignal import re -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Optional, Union from cura.Settings.CuraContainerRegistry import CuraContainerRegistry # To get names of materials we're compatible with. from UM.Logger import Logger @@ -20,11 +20,10 @@ class PackageModel(QObject): QML. The model can also be constructed directly from a response received by the API. """ - def __init__(self, package_data: Dict[str, Any], installation_status: str, section_title: Optional[str] = None, parent: Optional[QObject] = None) -> None: + def __init__(self, package_data: Dict[str, Any], section_title: Optional[str] = None, parent: Optional[QObject] = None) -> None: """ Constructs a new model for a single package. :param package_data: The data received from the Marketplace API about the package to create. - :param installation_status: Whether the package is `not_installed`, `installed` or `bundled`. :param section_title: If the packages are to be categorized per section provide the section_title :param parent: The parent QML object that controls the lifetime of this model (normally a PackageList). """ @@ -44,7 +43,7 @@ class PackageModel(QObject): self._description = package_data.get("description", "") self._formatted_description = self._format(self._description) - self._download_url = package_data.get("download_url", "") + self.download_url = package_data.get("download_url", "") self._release_notes = package_data.get("release_notes", "") # Not used yet, propose to add to description? subdata = package_data.get("data", {}) @@ -62,10 +61,21 @@ class PackageModel(QObject): if not self._icon_url or self._icon_url == "": self._icon_url = author_data.get("icon_url", "") - self._installation_status = installation_status + self._can_update = False + self._is_installing = False + self._is_updating = False self._section_title = section_title # Note that there's a lot more info in the package_data than just these specified here. + def __eq__(self, other: Union[str, "PackageModel"]): + if isinstance(other, PackageModel): + return other == self + else: + return other == self._package_id + + def __repr__(self): + return f"<{self._package_id} : {self._package_version} : {self._section_title}>" + def _findLink(self, subdata: Dict[str, Any], link_type: str) -> str: """ Searches the package data for a link of a certain type. @@ -219,10 +229,6 @@ class PackageModel(QObject): def authorInfoUrl(self): return self._author_info_url - @pyqtProperty(str, constant = True) - def installationStatus(self) -> str: - return self._installation_status - @pyqtProperty(str, constant = True) def sectionTitle(self) -> Optional[str]: return self._section_title @@ -255,6 +261,28 @@ class PackageModel(QObject): def isCompatibleAirManager(self) -> bool: return self._is_compatible_air_manager + isInstallingChanged = pyqtSignal() + + def setIsInstalling(self, value: bool) -> None: + if value != self._is_installing: + self._is_installing = value + self.isInstallingChanged.emit() + + @pyqtProperty(bool, fset = setIsInstalling, notify = isInstallingChanged) + def isInstalling(self) -> bool: + return self._is_installing + + isUpdatingChanged = pyqtSignal() + + def setIsUpdating(self, value: bool) -> None: + if value != self._is_updating: + self._is_updating = value + self.isUpdatingChanged.emit() + + @pyqtProperty(bool, fset = setIsUpdating, notify = isUpdatingChanged) + def isUpdating(self) -> bool: + return self._is_updating + isInstalledChanged = pyqtSignal() @pyqtProperty(bool, notify = isInstalledChanged) @@ -282,8 +310,13 @@ class PackageModel(QObject): manageInstallStateChanged = pyqtSignal() + def setManageInstallState(self, value: bool) -> None: + if value != self._is_installed: + self._is_installed = value + self.manageInstallStateChanged.emit() + @pyqtProperty(str, notify = manageInstallStateChanged) - def manageInstallState(self): + def manageInstallState(self) -> str: if self._is_installed: if self._is_bundled: return "hidden" @@ -296,4 +329,26 @@ class PackageModel(QObject): @pyqtProperty(str, notify = manageUpdateStateChanged) def manageUpdateState(self): - return "hidden" # TODO: implement + if self._can_update: + return "primary" + return "hidden" + + @property + def canUpdate(self): + return self._can_update + + @canUpdate.setter + def canUpdate(self, value): + if value != self._can_update: + self._can_update = value + self.manageUpdateStateChanged.emit() + + installPackageTriggered = pyqtSignal(str) + + uninstallPackageTriggered = pyqtSignal(str) + + updatePackageTriggered = pyqtSignal(str) + + enablePackageTriggered = pyqtSignal(str) + + disablePackageTriggered = pyqtSignal(str) diff --git a/plugins/Marketplace/resources/qml/PackageCard.qml b/plugins/Marketplace/resources/qml/PackageCard.qml index b8f815bedf..ad49c650e8 100644 --- a/plugins/Marketplace/resources/qml/PackageCard.qml +++ b/plugins/Marketplace/resources/qml/PackageCard.qml @@ -302,7 +302,6 @@ Rectangle width: UM.Theme.getSize("card_tiny_icon").width height: UM.Theme.getSize("card_tiny_icon").height - visible: packageData.installationStatus !== "bundled" //Don't show download count for packages that are bundled. It'll usually be 0. source: UM.Theme.getIcon("Download") color: UM.Theme.getColor("text") } @@ -311,7 +310,6 @@ Rectangle { anchors.verticalCenter: downloadsIcon.verticalCenter - visible: packageData.installationStatus !== "bundled" //Don't show download count for packages that are bundled. It'll usually be 0. color: UM.Theme.getColor("text") font: UM.Theme.getFont("default") text: packageData.downloadCount @@ -354,28 +352,80 @@ Rectangle onClicked: Qt.openUrlExternally(packageData.authorInfoUrl) } - Cura.SecondaryButton + ManageButton { - id: disableButton + id: enableManageButton Layout.alignment: Qt.AlignTop - text: catalog.i18nc("@button", "Disable") - visible: false // not functional right now, also only when unfolding and required + primaryText: catalog.i18nc("@button", "Enable") + busyPrimaryText: catalog.i18nc("@button", "enabling...") + secondaryText: catalog.i18nc("@button", "Disable") + busySecondaryText: catalog.i18nc("@button", "disabling...") + mainState: packageData.manageEnableState + enabled: !(installManageButton.busy || updateManageButton.busy) + } + Connections + { + target: enableManageButton + function onClicked(primary_action) + { + if (primary_action) + { + packageData.enablePackageTriggered(packageData.packageId) + } + else + { + packageData.disablePackageTriggered(packageData.packageId) + } + } } - Cura.SecondaryButton + ManageButton { - id: uninstallButton + id: installManageButton Layout.alignment: Qt.AlignTop - text: catalog.i18nc("@button", "Uninstall") - visible: false // not functional right now, also only when unfolding and required + primaryText: catalog.i18nc("@button", "Install") + busyPrimaryText: catalog.i18nc("@button", "installing...") + secondaryText: catalog.i18nc("@button", "Uninstall") + busySecondaryText: catalog.i18nc("@button", "uninstalling...") + mainState: packageData.manageInstallState + busy: packageData.isInstalling + enabled: !(enableManageButton.busy || updateManageButton.busy) + } + Connections + { + target: installManageButton + function onClicked(primary_action) + { + packageData.isInstalling = true + if (primary_action) + { + packageData.installPackageTriggered(packageData.packageId) + } + else + { + packageData.uninstallPackageTriggered(packageData.packageId) + } + } } - Cura.PrimaryButton + ManageButton { - id: installButton + id: updateManageButton Layout.alignment: Qt.AlignTop - text: catalog.i18nc("@button", "Update") // OR Download, if new! - visible: false // not functional right now, also only when unfolding and required + primaryText: catalog.i18nc("@button", "Update") + busyPrimaryText: catalog.i18nc("@button", "updating...") + mainState: packageData.manageUpdateState + busy: packageData.isUpdating + enabled: !(installManageButton.busy || enableManageButton.busy) + } + Connections + { + target: updateManageButton + function onClicked(primary_action) + { + packageData.isUpdating = true + packageData.updatePackageTriggered(packageData.packageId) + } } } } From a83a598e96467d3a0258de71f136ccbc481cc2a0 Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Fri, 3 Dec 2021 15:50:51 +0100 Subject: [PATCH 060/195] Added error handling when downloading packages failed A simple callback function which ensures that the proper signals are emitted when we fail to retrieve the package. Contributes to: CURA-8587 --- plugins/Marketplace/PackageList.py | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/plugins/Marketplace/PackageList.py b/plugins/Marketplace/PackageList.py index d988ce94bf..2a1283a5ba 100644 --- a/plugins/Marketplace/PackageList.py +++ b/plugins/Marketplace/PackageList.py @@ -129,10 +129,14 @@ class PackageList(ListModel): def downloadFinished(reply: "QNetworkReply") -> None: self._downloadFinished(package_id, reply, update) - self._ongoing_request = HttpRequestManager.getInstance().get( + def downloadError(reply: "QNetworkReply", error: "QNetworkReply.NetworkError") -> None: + self._downloadError(package_id, update, reply, error) + + HttpRequestManager.getInstance().get( url, scope = self._scope, - callback = downloadFinished + callback = downloadFinished, + error_callback = downloadError ) def _downloadFinished(self, package_id: str, reply: "QNetworkReply", update: bool = False) -> None: @@ -146,8 +150,19 @@ class PackageList(ListModel): self._to_install[package_id] = temp_file.name self.canInstallChanged.emit(package_id, update) except IOError as e: - Logger.logException("e", "Failed to write downloaded package to temp file", e) + Logger.error(f"Failed to write downloaded package to temp file {e}") temp_file.close() + self._downloadError(package_id, update) + + def _downloadError(self, package_id: str, update: bool = False, reply: Optional["QNetworkReply"] = None, error: Optional["QNetworkReply.NetworkError"] = None) -> None: + if reply: + reply_string = bytes(reply.readAll()).decode() + Logger.error(f"Failed to download package: {package_id} due to {reply_string}") + package = self._getPackageModel(package_id) + if update: + package.setIsUpdating(False) + else: + package.setIsInstalling(False) @pyqtSlot(str) def installPackage(self, package_id: str) -> None: @@ -168,7 +183,7 @@ class PackageList(ListModel): package.setIsUpdating(False) else: package.setIsInstalling(False) - #self._subscribe(package_id) + # self._subscribe(package_id) def _subscribe(self, package_id: str) -> None: if self._account.isLoggedIn: From 743ac67cdb27559d779703510d97839552d11a79 Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Fri, 3 Dec 2021 17:08:28 +0100 Subject: [PATCH 061/195] un-/subscribe the user to installed packages Contributes to: CURA-8587 --- plugins/Marketplace/Constants.py | 11 +++++++++++ plugins/Marketplace/LocalPackageList.py | 4 ++-- plugins/Marketplace/Marketplace.py | 6 ------ plugins/Marketplace/PackageList.py | 16 +++++++++------- plugins/Marketplace/PackageModel.py | 1 + plugins/Marketplace/RemotePackageList.py | 4 ++-- 6 files changed, 25 insertions(+), 17 deletions(-) create mode 100644 plugins/Marketplace/Constants.py diff --git a/plugins/Marketplace/Constants.py b/plugins/Marketplace/Constants.py new file mode 100644 index 0000000000..bc6d1f05fa --- /dev/null +++ b/plugins/Marketplace/Constants.py @@ -0,0 +1,11 @@ +# Copyright (c) 2021 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. +from cura.UltimakerCloud import UltimakerCloudConstants +from cura.ApplicationMetadata import CuraSDKVersion + +ROOT_URL = f"{UltimakerCloudConstants.CuraCloudAPIRoot}/cura-packages/v{UltimakerCloudConstants.CuraCloudAPIVersion}" +ROOT_CURA_URL = f"{ROOT_URL}/cura/v{CuraSDKVersion}" # Root of all Marketplace API requests. +ROOT_USER_URL = f"{ROOT_URL}/user" +PACKAGES_URL = f"{ROOT_CURA_URL}/packages" # URL to use for requesting the list of packages. +PACKAGE_UPDATES_URL = f"{PACKAGES_URL}/package-updates" # URL to use for requesting the list of packages that can be updated. +USER_PACKAGES_URL = f"{ROOT_USER_URL}/packages" diff --git a/plugins/Marketplace/LocalPackageList.py b/plugins/Marketplace/LocalPackageList.py index 4721224e16..284f51c806 100644 --- a/plugins/Marketplace/LocalPackageList.py +++ b/plugins/Marketplace/LocalPackageList.py @@ -15,7 +15,7 @@ from UM.Logger import Logger from .PackageList import PackageList from .PackageModel import PackageModel -from . import Marketplace +from .Constants import PACKAGE_UPDATES_URL catalog = i18nCatalog("cura") @@ -66,7 +66,7 @@ class LocalPackageList(PackageList): def checkForUpdates(self, packages: List[Dict[str, Any]]): installed_packages = "installed_packages=".join([f"{package['package_id']}:{package['package_version']}&" for package in packages]) - request_url = f"{Marketplace.PACKAGE_UPDATES_URL}?installed_packages={installed_packages[:-1]}" + request_url = f"{PACKAGE_UPDATES_URL}?installed_packages={installed_packages[:-1]}" self._ongoing_request = HttpRequestManager.getInstance().get( request_url, diff --git a/plugins/Marketplace/Marketplace.py b/plugins/Marketplace/Marketplace.py index 1b98503969..89ad986920 100644 --- a/plugins/Marketplace/Marketplace.py +++ b/plugins/Marketplace/Marketplace.py @@ -6,9 +6,7 @@ from PyQt5.QtCore import pyqtSlot from PyQt5.QtQml import qmlRegisterType from typing import Optional, TYPE_CHECKING -from cura.ApplicationMetadata import CuraSDKVersion from cura.CuraApplication import CuraApplication # Creating QML objects and managing packages. -from cura.UltimakerCloud import UltimakerCloudConstants from UM.Extension import Extension # We are implementing the main object of an extension here. from UM.PluginRegistry import PluginRegistry # To find out where we are stored (the proper way). @@ -19,10 +17,6 @@ from .LocalPackageList import LocalPackageList # To register this type with QML if TYPE_CHECKING: from PyQt5.QtCore import QObject -ROOT_URL = f"{UltimakerCloudConstants.CuraCloudAPIRoot}/cura-packages/v{UltimakerCloudConstants.CuraCloudAPIVersion}/cura/v{CuraSDKVersion}" # Root of all Marketplace API requests. -PACKAGES_URL = f"{ROOT_URL}/packages" # URL to use for requesting the list of packages. -PACKAGE_UPDATES_URL = f"{PACKAGES_URL}/package-updates" # URL to use for requesting the list of packages that can be updated. - class Marketplace(Extension): """ diff --git a/plugins/Marketplace/PackageList.py b/plugins/Marketplace/PackageList.py index 2a1283a5ba..3c86040a00 100644 --- a/plugins/Marketplace/PackageList.py +++ b/plugins/Marketplace/PackageList.py @@ -1,6 +1,7 @@ # Copyright (c) 2021 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. import tempfile +import json from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, Qt from typing import Dict, Optional, TYPE_CHECKING @@ -17,6 +18,7 @@ from cura import CuraPackageManager from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope # To make requests to the Ultimaker API with correct authorization. from .PackageModel import PackageModel +from .Constants import USER_PACKAGES_URL if TYPE_CHECKING: from PyQt5.QtCore import QObject @@ -183,14 +185,14 @@ class PackageList(ListModel): package.setIsUpdating(False) else: package.setIsInstalling(False) - # self._subscribe(package_id) + self._subscribe(package_id, str(package.sdk_version)) - def _subscribe(self, package_id: str) -> None: + def _subscribe(self, package_id: str, sdk_version: str) -> None: if self._account.isLoggedIn: Logger.debug(f"Subscribing the user for package: {package_id}") - self._ongoing_request = HttpRequestManager.getInstance().put( - url = "", - data = {}, + HttpRequestManager.getInstance().put( + url = USER_PACKAGES_URL, + data = json.dumps({"data": {"package_id": package_id, "sdk_version": sdk_version}}).encode(), scope = self._scope ) @@ -201,12 +203,12 @@ class PackageList(ListModel): self._manager.removePackage(package_id) package.setIsInstalling(False) package.setManageInstallState(False) - #self._unsunscribe(package_id) + self._unsunscribe(package_id) def _unsunscribe(self, package_id: str) -> None: if self._account.isLoggedIn: Logger.debug(f"Unsubscribing the user for package: {package_id}") - self._ongoing_request = HttpRequestManager.getInstance().delete(url = "", scope = self._scope) + HttpRequestManager.getInstance().delete(url = f"{USER_PACKAGES_URL}/{package_id}", scope = self._scope) @pyqtSlot(str) def updatePackage(self, package_id): diff --git a/plugins/Marketplace/PackageModel.py b/plugins/Marketplace/PackageModel.py index c9164bbe26..e6615283d9 100644 --- a/plugins/Marketplace/PackageModel.py +++ b/plugins/Marketplace/PackageModel.py @@ -65,6 +65,7 @@ class PackageModel(QObject): self._is_installing = False self._is_updating = False self._section_title = section_title + self.sdk_version = package_data.get("sdk_version_semver", "") # Note that there's a lot more info in the package_data than just these specified here. def __eq__(self, other: Union[str, "PackageModel"]): diff --git a/plugins/Marketplace/RemotePackageList.py b/plugins/Marketplace/RemotePackageList.py index 63370042e7..a8fb20e88f 100644 --- a/plugins/Marketplace/RemotePackageList.py +++ b/plugins/Marketplace/RemotePackageList.py @@ -9,7 +9,7 @@ from UM.i18n import i18nCatalog from UM.Logger import Logger from UM.TaskManagement.HttpRequestManager import HttpRequestManager # To request the package list from the API. -from . import Marketplace # To get the list of packages. Imported this way to prevent circular imports. +from .Constants import PACKAGES_URL # To get the list of packages. Imported this way to prevent circular imports. from .PackageList import PackageList from .PackageModel import PackageModel # The contents of this list. @@ -108,7 +108,7 @@ class RemotePackageList(PackageList): Get the URL to request the first paginated page with. :return: A URL to request. """ - request_url = f"{Marketplace.PACKAGES_URL}?limit={self.ITEMS_PER_PAGE}" + request_url = f"{PACKAGES_URL}?limit={self.ITEMS_PER_PAGE}" if self._package_type_filter != "": request_url += f"&package_type={self._package_type_filter}" if self._current_search_string != "": From 4c570c87e9388d9d7b01c6bf70da448aa73a6d2c Mon Sep 17 00:00:00 2001 From: casper Date: Fri, 3 Dec 2021 17:19:18 +0100 Subject: [PATCH 062/195] Make sure recently installed packages only appear once in package list Some packages might be added to both `getPackagesToInstall` and `getAllInstalledPackagesInfo` cura 8587 --- cura/CuraPackageManager.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/cura/CuraPackageManager.py b/cura/CuraPackageManager.py index e6123c1947..6c62442758 100644 --- a/cura/CuraPackageManager.py +++ b/cura/CuraPackageManager.py @@ -68,16 +68,24 @@ class CuraPackageManager(PackageManager): def iterateAllLocalPackages(self) -> Generator[Dict[str, Any], None, None]: """ A generator which returns an unordered list of all the PackageModels""" + handled_packages = set() for packages in self.getAllInstalledPackagesInfo().values(): for package_info in packages: - yield package_info + if not handled_packages.__contains__(package_info["package_id"]): + handled_packages.add(package_info["package_id"]) + yield package_info # Get all to be removed package_info's. These packages are still used in the current session so the user might # still want to interact with these. for package_data in self.getPackagesToRemove().values(): - yield package_data["package_info"] + for package_data in self.getPackagesToRemove().values(): + if not handled_packages.__contains__(package_data["package_info"]["package_id"]): + handled_packages.add(package_data["package_info"]["package_id"]) + yield package_data["package_info"] # Get all to be installed package_info's. Since the user might want to interact with these for package_data in self.getPackagesToInstall().values(): - yield package_data["package_info"] + if not handled_packages.__contains__(package_data["package_info"]["package_id"]): + handled_packages.add(package_data["package_info"]["package_id"]) + yield package_data["package_info"] \ No newline at end of file From c937337324427c3c3b2d897ceea9caf838271c0e Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Fri, 3 Dec 2021 17:21:38 +0100 Subject: [PATCH 063/195] Change the state of the enable button after an un-/install Contributes to: CURA-8587 --- plugins/Marketplace/PackageModel.py | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/Marketplace/PackageModel.py b/plugins/Marketplace/PackageModel.py index e6615283d9..3f602d1384 100644 --- a/plugins/Marketplace/PackageModel.py +++ b/plugins/Marketplace/PackageModel.py @@ -315,6 +315,7 @@ class PackageModel(QObject): if value != self._is_installed: self._is_installed = value self.manageInstallStateChanged.emit() + self.manageEnableStateChanged.emit() @pyqtProperty(str, notify = manageInstallStateChanged) def manageInstallState(self) -> str: From 8400459e212214637247445260cf5eeda99ed131 Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Fri, 3 Dec 2021 17:41:29 +0100 Subject: [PATCH 064/195] Use Python syntax to check if item is in collection Als removed the check on the first loop, because we know for certain these will already be unique values. Contributes to: CURA-8587 --- cura/CuraPackageManager.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/cura/CuraPackageManager.py b/cura/CuraPackageManager.py index 6c62442758..98e0e592aa 100644 --- a/cura/CuraPackageManager.py +++ b/cura/CuraPackageManager.py @@ -68,24 +68,22 @@ class CuraPackageManager(PackageManager): def iterateAllLocalPackages(self) -> Generator[Dict[str, Any], None, None]: """ A generator which returns an unordered list of all the PackageModels""" - handled_packages = set() + handled_packages = {} for packages in self.getAllInstalledPackagesInfo().values(): for package_info in packages: - if not handled_packages.__contains__(package_info["package_id"]): - handled_packages.add(package_info["package_id"]) - yield package_info + handled_packages.add(package_info["package_id"]) + yield package_info # Get all to be removed package_info's. These packages are still used in the current session so the user might # still want to interact with these. for package_data in self.getPackagesToRemove().values(): - for package_data in self.getPackagesToRemove().values(): - if not handled_packages.__contains__(package_data["package_info"]["package_id"]): + if not package_data["package_info"]["package_id"] in handled_packages: handled_packages.add(package_data["package_info"]["package_id"]) yield package_data["package_info"] # Get all to be installed package_info's. Since the user might want to interact with these for package_data in self.getPackagesToInstall().values(): - if not handled_packages.__contains__(package_data["package_info"]["package_id"]): + if not package_data["package_info"]["package_id"] in handled_packages: handled_packages.add(package_data["package_info"]["package_id"]) - yield package_data["package_info"] \ No newline at end of file + yield package_data["package_info"] From 58ee0d19fdd71fc8a085701003df3d45e6d9102e Mon Sep 17 00:00:00 2001 From: casper Date: Sat, 4 Dec 2021 10:28:25 +0100 Subject: [PATCH 065/195] Vertically align author components with manage buttons To comply with UX design cura 8587 --- plugins/Marketplace/resources/qml/PackageCard.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/Marketplace/resources/qml/PackageCard.qml b/plugins/Marketplace/resources/qml/PackageCard.qml index ad49c650e8..6331c9f58c 100644 --- a/plugins/Marketplace/resources/qml/PackageCard.qml +++ b/plugins/Marketplace/resources/qml/PackageCard.qml @@ -328,7 +328,7 @@ Rectangle Label { id: authorBy - Layout.alignment: Qt.AlignTop + Layout.alignment: Qt.AlignCenter text: catalog.i18nc("@label", "By") font: UM.Theme.getFont("default") @@ -339,7 +339,7 @@ Rectangle { Layout.fillWidth: true Layout.preferredHeight: authorBy.height - Layout.alignment: Qt.AlignTop + Layout.alignment: Qt.AlignCenter text: packageData.authorName textFont: UM.Theme.getFont("default_bold") From 09709ede8b27f02ae0dc2f8131a15f9ca94183c6 Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Sun, 5 Dec 2021 15:11:35 +0100 Subject: [PATCH 066/195] Fix duplicate packages in get AlllocalPackages The helper class is needed because dict's aren't hashable which complicates the `in` check. Contributes to: CURA-8587 --- cura/CuraPackageManager.py | 43 +++++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/cura/CuraPackageManager.py b/cura/CuraPackageManager.py index 98e0e592aa..21cd0c69dc 100644 --- a/cura/CuraPackageManager.py +++ b/cura/CuraPackageManager.py @@ -23,7 +23,7 @@ class CuraPackageManager(PackageManager): self.installedPackagesChanged.connect(self._updateLocallyInstalledPackages) def _updateLocallyInstalledPackages(self): - self._locally_installed_packages = list(self.iterateAllLocalPackages()) + self._locally_installed_packages = self.getAllLocalPackages() @property def locally_installed_packages(self): @@ -66,24 +66,29 @@ class CuraPackageManager(PackageManager): return machine_with_materials, machine_with_qualities - def iterateAllLocalPackages(self) -> Generator[Dict[str, Any], None, None]: - """ A generator which returns an unordered list of all the PackageModels""" - handled_packages = {} + def getAllLocalPackages(self) -> List[Dict[str, Any]]: + """ returns an unordered list of all the package_info installed, to be installed or to be returned""" - for packages in self.getAllInstalledPackagesInfo().values(): - for package_info in packages: - handled_packages.add(package_info["package_id"]) - yield package_info + class PkgInfo: + # Needed helper class because a dict isn't hashable + def __init__(self, package_info): + self._info = package_info - # Get all to be removed package_info's. These packages are still used in the current session so the user might - # still want to interact with these. - for package_data in self.getPackagesToRemove().values(): - if not package_data["package_info"]["package_id"] in handled_packages: - handled_packages.add(package_data["package_info"]["package_id"]) - yield package_data["package_info"] + def __contains__(self, item): + return item == self._info["package_id"] - # Get all to be installed package_info's. Since the user might want to interact with these - for package_data in self.getPackagesToInstall().values(): - if not package_data["package_info"]["package_id"] in handled_packages: - handled_packages.add(package_data["package_info"]["package_id"]) - yield package_data["package_info"] + def __repr__(self): + return repr(self._info) + + def __iter__(self): + for k, v in self._info.items(): + yield k, v + + def asdict(self): + return self._info + + packages = [PkgInfo(package_info) for package in self.getAllInstalledPackagesInfo().values() for package_info in package] + packages.extend([PkgInfo(package["package_info"]) for package in self.getPackagesToRemove().values() if package["package_info"]["package_id"] not in packages]) + packages.extend([PkgInfo(package["package_info"]) for package in self.getPackagesToInstall().values() if package["package_info"]["package_id"] not in packages]) + + return [dict(package) for package in packages] From bd2f2708034bddaf11a81e6775a1d21942b09f5f Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Sun, 5 Dec 2021 15:18:51 +0100 Subject: [PATCH 067/195] Added typing Contributes to: CURA-8587 --- plugins/Marketplace/PackageList.py | 19 +++++++++---------- plugins/Marketplace/PackageModel.py | 8 ++++---- plugins/Marketplace/RemotePackageList.py | 4 ++-- 3 files changed, 15 insertions(+), 16 deletions(-) diff --git a/plugins/Marketplace/PackageList.py b/plugins/Marketplace/PackageList.py index 3c86040a00..8c9bcdb963 100644 --- a/plugins/Marketplace/PackageList.py +++ b/plugins/Marketplace/PackageList.py @@ -9,11 +9,10 @@ from typing import Dict, Optional, TYPE_CHECKING from UM.i18n import i18nCatalog from UM.Qt.ListModel import ListModel from UM.TaskManagement.HttpRequestScope import JsonDecoratorScope -from UM.TaskManagement.HttpRequestManager import HttpRequestData , HttpRequestManager +from UM.TaskManagement.HttpRequestManager import HttpRequestData, HttpRequestManager from UM.Logger import Logger from cura.CuraApplication import CuraApplication -from cura.API.Account import Account from cura import CuraPackageManager from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope # To make requests to the Ultimaker API with correct authorization. @@ -36,7 +35,7 @@ class PackageList(ListModel): def __init__(self, parent: Optional["QObject"] = None) -> None: super().__init__(parent) self._manager: CuraPackageManager = CuraApplication.getInstance().getPackageManager() - self._account: Account = CuraApplication.getInstance().getCuraAPI().account + self._account = CuraApplication.getInstance().getCuraAPI().account self._error_message = "" self.addRoleName(self.PackageRole, "package") self._is_loading = False @@ -113,7 +112,7 @@ class PackageList(ListModel): :return: ``True`` if a Footer should be displayed in the ListView, e.q.: paginated lists, ``False`` Otherwise""" return self._has_footer - def _connectManageButtonSignals(self, package): + def _connectManageButtonSignals(self, package: PackageModel) -> None: package.installPackageTriggered.connect(self.installPackage) package.uninstallPackageTriggered.connect(self.uninstallPackage) package.updatePackageTriggered.connect(self.installPackage) @@ -126,7 +125,7 @@ class PackageList(ListModel): canInstallChanged = pyqtSignal(str, bool) - def download(self, package_id, url, update: bool = False): + def download(self, package_id: str, url: str, update: bool = False) -> None: def downloadFinished(reply: "QNetworkReply") -> None: self._downloadFinished(package_id, reply, update) @@ -176,7 +175,7 @@ class PackageList(ListModel): def _install(self, package_id: str, update: bool = False) -> None: package_path = self._to_install.pop(package_id) Logger.debug(f"Installing {package_id}") - to_be_installed = self._manager.installPackage(package_path) != None + to_be_installed = self._manager.installPackage(package_path) is not None package = self._getPackageModel(package_id) if package.canUpdate and to_be_installed: package.canUpdate = False @@ -197,7 +196,7 @@ class PackageList(ListModel): ) @pyqtSlot(str) - def uninstallPackage(self, package_id): + def uninstallPackage(self, package_id: str) -> None: Logger.debug(f"Uninstalling {package_id}") package = self._getPackageModel(package_id) self._manager.removePackage(package_id) @@ -211,7 +210,7 @@ class PackageList(ListModel): HttpRequestManager.getInstance().delete(url = f"{USER_PACKAGES_URL}/{package_id}", scope = self._scope) @pyqtSlot(str) - def updatePackage(self, package_id): + def updatePackage(self, package_id: str) -> None: self._manager.removePackage(package_id, force_add = True) package = self._getPackageModel(package_id) url = package.download_url @@ -219,9 +218,9 @@ class PackageList(ListModel): self.download(package_id, url, True) @pyqtSlot(str) - def enablePackage(self, package_id): + def enablePackage(self, package_id: str) -> None: Logger.debug(f"Enabling {package_id}") @pyqtSlot(str) - def disablePackage(self, package_id): + def disablePackage(self, package_id: str) -> None: Logger.debug(f"Disabling {package_id}") diff --git a/plugins/Marketplace/PackageModel.py b/plugins/Marketplace/PackageModel.py index 3f602d1384..4a8254de7d 100644 --- a/plugins/Marketplace/PackageModel.py +++ b/plugins/Marketplace/PackageModel.py @@ -293,13 +293,13 @@ class PackageModel(QObject): isEnabledChanged = pyqtSignal() @pyqtProperty(bool, notify = isEnabledChanged) - def isEnabled(self): + def isEnabled(self) -> bool: return self._is_active manageEnableStateChanged = pyqtSignal() @pyqtProperty(str, notify = manageEnableStateChanged) - def manageEnableState(self): + def manageEnableState(self) -> str: # TODO: Handle manual installed packages if self._is_installed: if self._is_active: @@ -330,13 +330,13 @@ class PackageModel(QObject): manageUpdateStateChanged = pyqtSignal() @pyqtProperty(str, notify = manageUpdateStateChanged) - def manageUpdateState(self): + def manageUpdateState(self) -> str: if self._can_update: return "primary" return "hidden" @property - def canUpdate(self): + def canUpdate(self) -> bool: return self._can_update @canUpdate.setter diff --git a/plugins/Marketplace/RemotePackageList.py b/plugins/Marketplace/RemotePackageList.py index a8fb20e88f..fa05ca9526 100644 --- a/plugins/Marketplace/RemotePackageList.py +++ b/plugins/Marketplace/RemotePackageList.py @@ -3,7 +3,7 @@ from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot from PyQt5.QtNetwork import QNetworkReply -from typing import Optional, TYPE_CHECKING +from typing import Optional, Set, TYPE_CHECKING from UM.i18n import i18nCatalog from UM.Logger import Logger @@ -31,7 +31,7 @@ class RemotePackageList(PackageList): self._request_url = self._initialRequestUrl() self.isLoadingChanged.connect(self._onLoadingChanged) self.isLoadingChanged.emit() - self._locally_installed = { p["package_id"] for p in self._manager.locally_installed_packages } + self._locally_installed: Set[str] = { p["package_id"] for p in self._manager.locally_installed_packages } def __del__(self) -> None: """ From 305fb4ab09b6cfec6f930ead99240bf060a4d8c9 Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Sun, 5 Dec 2021 16:23:23 +0100 Subject: [PATCH 068/195] renamed locally_installed property Contributes to: CURA-8587 --- cura/CuraPackageManager.py | 20 ++++++++------------ plugins/Marketplace/LocalPackageList.py | 5 +++-- plugins/Marketplace/RemotePackageList.py | 4 ++-- 3 files changed, 13 insertions(+), 16 deletions(-) diff --git a/cura/CuraPackageManager.py b/cura/CuraPackageManager.py index 21cd0c69dc..4250af9072 100644 --- a/cura/CuraPackageManager.py +++ b/cura/CuraPackageManager.py @@ -19,22 +19,18 @@ if TYPE_CHECKING: class CuraPackageManager(PackageManager): def __init__(self, application: "QtApplication", parent: Optional["QObject"] = None) -> None: super().__init__(application, parent) - self._locally_installed_packages = None - self.installedPackagesChanged.connect(self._updateLocallyInstalledPackages) + self._local_packages: Optional[List[Dict[str, Any]]] = None + self.installedPackagesChanged.connect(self._updateLocalPackages) - def _updateLocallyInstalledPackages(self): - self._locally_installed_packages = self.getAllLocalPackages() + def _updateLocalPackages(self) -> None: + self._local_packages = self.getAllLocalPackages() @property - def locally_installed_packages(self): + def local_packages(self) -> List[Dict[str, Any]]: """locally installed packages, lazy execution""" - if self._locally_installed_packages is None: - self._updateLocallyInstalledPackages() - return self._locally_installed_packages - - @locally_installed_packages.setter - def locally_installed_packages(self, value): - self._locally_installed_packages = value + if self._local_packages is None: + self._updateLocalPackages() + return self._local_packages def initialize(self) -> None: self._installation_dirs_dict["materials"] = Resources.getStoragePath(CuraApplication.ResourceTypes.MaterialInstanceContainer) diff --git a/plugins/Marketplace/LocalPackageList.py b/plugins/Marketplace/LocalPackageList.py index 284f51c806..6bd22ee918 100644 --- a/plugins/Marketplace/LocalPackageList.py +++ b/plugins/Marketplace/LocalPackageList.py @@ -48,9 +48,10 @@ class LocalPackageList(PackageList): self.setIsLoading(True) # Obtain and sort the local packages - self.setItems([{"package": p} for p in [self._makePackageModel(p) for p in self._manager.locally_installed_packages]]) + Logger.debug(f"Number of local packages: {len(self._manager.local_packages)} -> {[p['package_id'] for p in self._manager.local_packages]}") + self.setItems([{"package": p} for p in [self._makePackageModel(p) for p in self._manager.local_packages]]) self.sort(attrgetter("sectionTitle", "canUpdate", "displayName"), key = "package", reverse = True) - self.checkForUpdates(self._manager.locally_installed_packages) + self.checkForUpdates(self._manager.local_packages) self.setIsLoading(False) self.setHasMore(False) # All packages should have been loaded at this time diff --git a/plugins/Marketplace/RemotePackageList.py b/plugins/Marketplace/RemotePackageList.py index fa05ca9526..a994d3d2d2 100644 --- a/plugins/Marketplace/RemotePackageList.py +++ b/plugins/Marketplace/RemotePackageList.py @@ -31,7 +31,7 @@ class RemotePackageList(PackageList): self._request_url = self._initialRequestUrl() self.isLoadingChanged.connect(self._onLoadingChanged) self.isLoadingChanged.emit() - self._locally_installed: Set[str] = { p["package_id"] for p in self._manager.locally_installed_packages } + self._local_packages: Set[str] = { p["package_id"] for p in self._manager.local_packages } def __del__(self) -> None: """ @@ -129,7 +129,7 @@ class RemotePackageList(PackageList): return for package_data in response_data["data"]: - if package_data["package_id"] in self._locally_installed: + if package_data["package_id"] in self._local_packages: continue # We should only show packages which are not already installed try: package = PackageModel(package_data, parent = self) From a51b29cdf60a2c2677908056b1e850202ae52718 Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Sun, 5 Dec 2021 16:28:21 +0100 Subject: [PATCH 069/195] Fixed double items in getAllLocalPackages Contributes to: CURA-8587 --- cura/CuraPackageManager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cura/CuraPackageManager.py b/cura/CuraPackageManager.py index 4250af9072..3ef24368fd 100644 --- a/cura/CuraPackageManager.py +++ b/cura/CuraPackageManager.py @@ -70,7 +70,7 @@ class CuraPackageManager(PackageManager): def __init__(self, package_info): self._info = package_info - def __contains__(self, item): + def __eq__(self, item): return item == self._info["package_id"] def __repr__(self): From 863e92d0d25c0835ef99872711e1b20d307ba36a Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Mon, 6 Dec 2021 09:14:40 +0100 Subject: [PATCH 070/195] Fixed state of manage buttons Contributes to: CURA-8587 --- plugins/Marketplace/LocalPackageList.py | 7 +- plugins/Marketplace/PackageList.py | 100 +++---- plugins/Marketplace/PackageModel.py | 246 ++++++++++++------ .../resources/qml/ManageButton.qml | 2 +- .../Marketplace/resources/qml/PackageCard.qml | 10 +- 5 files changed, 227 insertions(+), 138 deletions(-) diff --git a/plugins/Marketplace/LocalPackageList.py b/plugins/Marketplace/LocalPackageList.py index 6bd22ee918..ea259dbdc7 100644 --- a/plugins/Marketplace/LocalPackageList.py +++ b/plugins/Marketplace/LocalPackageList.py @@ -48,9 +48,8 @@ class LocalPackageList(PackageList): self.setIsLoading(True) # Obtain and sort the local packages - Logger.debug(f"Number of local packages: {len(self._manager.local_packages)} -> {[p['package_id'] for p in self._manager.local_packages]}") self.setItems([{"package": p} for p in [self._makePackageModel(p) for p in self._manager.local_packages]]) - self.sort(attrgetter("sectionTitle", "canUpdate", "displayName"), key = "package", reverse = True) + self.sort(attrgetter("sectionTitle", "can_update", "displayName"), key = "package", reverse = True) self.checkForUpdates(self._manager.local_packages) self.setIsLoading(False) @@ -90,8 +89,8 @@ class LocalPackageList(PackageList): return for package_data in response_data["data"]: - package = self._getPackageModel(package_data["package_id"]) + package = self.getPackageModel(package_data["package_id"]) package.download_url = package_data.get("download_url", "") package.canUpdate = True - self.sort(attrgetter("sectionTitle", "canUpdate", "displayName"), key = "package", reverse = True) + self.sort(attrgetter("sectionTitle", "can_update", "displayName"), key = "package", reverse = True) diff --git a/plugins/Marketplace/PackageList.py b/plugins/Marketplace/PackageList.py index 8c9bcdb963..5ad2c50ddb 100644 --- a/plugins/Marketplace/PackageList.py +++ b/plugins/Marketplace/PackageList.py @@ -112,19 +112,26 @@ class PackageList(ListModel): :return: ``True`` if a Footer should be displayed in the ListView, e.q.: paginated lists, ``False`` Otherwise""" return self._has_footer - def _connectManageButtonSignals(self, package: PackageModel) -> None: - package.installPackageTriggered.connect(self.installPackage) - package.uninstallPackageTriggered.connect(self.uninstallPackage) - package.updatePackageTriggered.connect(self.installPackage) - package.enablePackageTriggered.connect(self.enablePackage) - package.disablePackageTriggered.connect(self.disablePackage) - - def _getPackageModel(self, package_id: str) -> PackageModel: + def getPackageModel(self, package_id: str) -> PackageModel: index = self.find("package", package_id) return self.getItem(index)["package"] canInstallChanged = pyqtSignal(str, bool) + def _install(self, package_id: str, update: bool = False) -> None: + package_path = self._to_install.pop(package_id) + Logger.debug(f"Installing {package_id}") + to_be_installed = self._manager.installPackage(package_path) is not None + package = self.getPackageModel(package_id) + if package.can_update and to_be_installed: + package.can_update = False + if update: + package.is_updating = False + else: + package.is_recently_installed = True + package.is_installing = False + self.subscribeUserToPackage(package_id, str(package.sdk_version)) + def download(self, package_id: str, url: str, update: bool = False) -> None: def downloadFinished(reply: "QNetworkReply") -> None: @@ -159,34 +166,13 @@ class PackageList(ListModel): if reply: reply_string = bytes(reply.readAll()).decode() Logger.error(f"Failed to download package: {package_id} due to {reply_string}") - package = self._getPackageModel(package_id) + package = self.getPackageModel(package_id) if update: - package.setIsUpdating(False) + package.is_updating = False else: - package.setIsInstalling(False) + package.is_installing = False - @pyqtSlot(str) - def installPackage(self, package_id: str) -> None: - package = self._getPackageModel(package_id) - url = package.download_url - Logger.debug(f"Trying to download and install {package_id} from {url}") - self.download(package_id, url) - - def _install(self, package_id: str, update: bool = False) -> None: - package_path = self._to_install.pop(package_id) - Logger.debug(f"Installing {package_id}") - to_be_installed = self._manager.installPackage(package_path) is not None - package = self._getPackageModel(package_id) - if package.canUpdate and to_be_installed: - package.canUpdate = False - package.setManageInstallState(to_be_installed) - if update: - package.setIsUpdating(False) - else: - package.setIsInstalling(False) - self._subscribe(package_id, str(package.sdk_version)) - - def _subscribe(self, package_id: str, sdk_version: str) -> None: + def subscribeUserToPackage(self, package_id: str, sdk_version: str) -> None: if self._account.isLoggedIn: Logger.debug(f"Subscribing the user for package: {package_id}") HttpRequestManager.getInstance().put( @@ -195,32 +181,58 @@ class PackageList(ListModel): scope = self._scope ) - @pyqtSlot(str) - def uninstallPackage(self, package_id: str) -> None: - Logger.debug(f"Uninstalling {package_id}") - package = self._getPackageModel(package_id) - self._manager.removePackage(package_id) - package.setIsInstalling(False) - package.setManageInstallState(False) - self._unsunscribe(package_id) - - def _unsunscribe(self, package_id: str) -> None: + def unsunscribeUserFromPackage(self, package_id: str) -> None: if self._account.isLoggedIn: Logger.debug(f"Unsubscribing the user for package: {package_id}") HttpRequestManager.getInstance().delete(url = f"{USER_PACKAGES_URL}/{package_id}", scope = self._scope) + # --- Handle the manage package buttons --- + + def _connectManageButtonSignals(self, package: PackageModel) -> None: + package.installPackageTriggered.connect(self.installPackage) + package.uninstallPackageTriggered.connect(self.uninstallPackage) + package.updatePackageTriggered.connect(self.installPackage) + package.enablePackageTriggered.connect(self.enablePackage) + package.disablePackageTriggered.connect(self.disablePackage) + + @pyqtSlot(str) + def installPackage(self, package_id: str) -> None: + package = self.getPackageModel(package_id) + package.is_installing = True + url = package.download_url + Logger.debug(f"Trying to download and install {package_id} from {url}") + self.download(package_id, url) + + @pyqtSlot(str) + def uninstallPackage(self, package_id: str) -> None: + Logger.debug(f"Uninstalling {package_id}") + package = self.getPackageModel(package_id) + package.is_installing = True + self._manager.removePackage(package_id) + package.is_installing = False + self.unsunscribeUserFromPackage(package_id) + @pyqtSlot(str) def updatePackage(self, package_id: str) -> None: + package = self.getPackageModel(package_id) + package.is_updating = True self._manager.removePackage(package_id, force_add = True) - package = self._getPackageModel(package_id) url = package.download_url Logger.debug(f"Trying to download and update {package_id} from {url}") self.download(package_id, url, True) @pyqtSlot(str) def enablePackage(self, package_id: str) -> None: + package = self.getPackageModel(package_id) + package.is_enabling = True Logger.debug(f"Enabling {package_id}") + # TODO: implement enabling functionality + package.is_enabling = False @pyqtSlot(str) def disablePackage(self, package_id: str) -> None: + package = self.getPackageModel(package_id) + package.is_enabling = True Logger.debug(f"Disabling {package_id}") + # TODO: implement disabling functionality + package.is_enabling = False diff --git a/plugins/Marketplace/PackageModel.py b/plugins/Marketplace/PackageModel.py index 4a8254de7d..8ba712c19d 100644 --- a/plugins/Marketplace/PackageModel.py +++ b/plugins/Marketplace/PackageModel.py @@ -61,9 +61,11 @@ class PackageModel(QObject): if not self._icon_url or self._icon_url == "": self._icon_url = author_data.get("icon_url", "") - self._can_update = False self._is_installing = False + self.is_recently_installed = False + self._can_update = False self._is_updating = False + self._is_enabling = False self._section_title = section_title self.sdk_version = package_data.get("sdk_version_semver", "") # Note that there's a lot more info in the package_data than just these specified here. @@ -262,88 +264,9 @@ class PackageModel(QObject): def isCompatibleAirManager(self) -> bool: return self._is_compatible_air_manager - isInstallingChanged = pyqtSignal() + # --- manage buttons signals --- - def setIsInstalling(self, value: bool) -> None: - if value != self._is_installing: - self._is_installing = value - self.isInstallingChanged.emit() - - @pyqtProperty(bool, fset = setIsInstalling, notify = isInstallingChanged) - def isInstalling(self) -> bool: - return self._is_installing - - isUpdatingChanged = pyqtSignal() - - def setIsUpdating(self, value: bool) -> None: - if value != self._is_updating: - self._is_updating = value - self.isUpdatingChanged.emit() - - @pyqtProperty(bool, fset = setIsUpdating, notify = isUpdatingChanged) - def isUpdating(self) -> bool: - return self._is_updating - - isInstalledChanged = pyqtSignal() - - @pyqtProperty(bool, notify = isInstalledChanged) - def isInstalled(self): - return self._is_installed - - isEnabledChanged = pyqtSignal() - - @pyqtProperty(bool, notify = isEnabledChanged) - def isEnabled(self) -> bool: - return self._is_active - - manageEnableStateChanged = pyqtSignal() - - @pyqtProperty(str, notify = manageEnableStateChanged) - def manageEnableState(self) -> str: - # TODO: Handle manual installed packages - if self._is_installed: - if self._is_active: - return "secondary" - else: - return "primary" - else: - return "hidden" - - manageInstallStateChanged = pyqtSignal() - - def setManageInstallState(self, value: bool) -> None: - if value != self._is_installed: - self._is_installed = value - self.manageInstallStateChanged.emit() - self.manageEnableStateChanged.emit() - - @pyqtProperty(str, notify = manageInstallStateChanged) - def manageInstallState(self) -> str: - if self._is_installed: - if self._is_bundled: - return "hidden" - else: - return "secondary" - else: - return "primary" - - manageUpdateStateChanged = pyqtSignal() - - @pyqtProperty(str, notify = manageUpdateStateChanged) - def manageUpdateState(self) -> str: - if self._can_update: - return "primary" - return "hidden" - - @property - def canUpdate(self) -> bool: - return self._can_update - - @canUpdate.setter - def canUpdate(self, value): - if value != self._can_update: - self._can_update = value - self.manageUpdateStateChanged.emit() + stateManageButtonChanged = pyqtSignal() installPackageTriggered = pyqtSignal(str) @@ -354,3 +277,162 @@ class PackageModel(QObject): enablePackageTriggered = pyqtSignal(str) disablePackageTriggered = pyqtSignal(str) + + # --- enabling --- + + @pyqtProperty(str, notify = stateManageButtonChanged) + def stateManageEnableButton(self) -> str: + if self._is_enabling: + return "busy" + if self.is_recently_installed: + return "hidden" + if self._package_type == "material": + if self._is_bundled: # TODO: Check if a bundled material can/should be un-/install en-/disabled + return "secondary" + return "hidden" + if not self._is_installed: + return "hidden" + if self._is_installed and self._is_active: + return "secondary" + return "primary" + + @property + def is_enabling(self) -> bool: + return self._is_enabling + + @is_enabling.setter + def is_enabling(self, value: bool) -> None: + if value != self._is_enabling: + self._is_enabling = value + self.stateManageButtonChanged.emit() + + # --- Installing --- + + @pyqtProperty(str, notify = stateManageButtonChanged) + def stateManageInstallButton(self) -> str: + if self._is_installing: + return "busy" + if self.is_recently_installed: + return "secondary" + if self._is_installed: + if self._is_bundled: + return "hidden" + else: + return "secondary" + else: + return "primary" + + @property + def is_installing(self) -> bool: + return self._is_installing + + @is_installing.setter + def is_installing(self, value: bool) -> None: + if value != self._is_installing: + self._is_installing = value + self.stateManageButtonChanged.emit() + + # --- Updating --- + + @pyqtProperty(str, notify = stateManageButtonChanged) + def stateManageUpdateButton(self) -> str: + if self._is_updating: + return "busy" + if self._can_update: + return "primary" + return "hidden" + + @property + def is_updating(self) -> bool: + return self._is_updating + + @is_updating.setter + def is_updating(self, value: bool) -> None: + if value != self._is_updating: + self._is_updating = value + self.stateManageButtonChanged.emit() + + @property + def can_update(self) -> bool: + return self._can_update + + @can_update.setter + def can_update(self, value: bool) -> None: + if value != self._can_update: + self._can_update = value + self.stateManageButtonChanged.emit() + + # ---- + + + + + + + + + + + # isInstalledChanged = pyqtSignal() + # + # @pyqtProperty(bool, notify = isInstalledChanged) + # def isInstalled(self): + # return self._is_installed + # + # isEnabledChanged = pyqtSignal() + # + # + #f + # @pyqtProperty(bool, notify = isEnabledChanged) + # def isEnabled(self) -> bool: + # return self._is_active + # + # + # + # isManageEnableStateChanged = pyqtSignalf() + # + # @pyqtProperty(str, notify = isManageEnableStateChanged) + # def isManageEnableState(self) -> str: + # if self.isEnabling: + # return "busy" + # if self. + # + # manageEnableStateChanged = pyqtSignal() + # + # @pyqtProperty(str, notify = manageEnableStateChanged) + # def manageEnableState(self) -> str: + # # TODO: Handle manual installed packages + # if self._is_installed: + # if self._is_active: + # return "secondary" + # else: + # return "primary" + # else: + # return "hidden" + # + # manageInstallStateChanged = pyqtSignal() + # + # def setManageInstallState(self, value: bool) -> None: + # if value != self._is_installed: + # self._is_installed = value + # self.manageInstallStateChanged.emit() + # self.manageEnableStateChanged.emit() + # + # @pyqtProperty(str, notify = manageInstallStateChanged) + # def manageInstallState(self) -> str: + # if self._is_installed: + # if self._is_bundled: + # return "hidden" + # else: + # return "secondary" + # else: + # return "primary" + # + # manageUpdateStateChanged = pyqtSignal() + # + # @pyqtProperty(str, notify = manageUpdateStateChanged) + # def manageUpdateState(self) -> str: + # if self._can_update: + # return "primary" + # return "hidden" + # diff --git a/plugins/Marketplace/resources/qml/ManageButton.qml b/plugins/Marketplace/resources/qml/ManageButton.qml index 797e83830f..dd17d07d01 100644 --- a/plugins/Marketplace/resources/qml/ManageButton.qml +++ b/plugins/Marketplace/resources/qml/ManageButton.qml @@ -17,7 +17,7 @@ RowLayout property string busySecondaryText: busyMessageText.text property string mainState: "primary" property bool enabled: true - property bool busy: false + property bool busy signal clicked(bool primary_action) diff --git a/plugins/Marketplace/resources/qml/PackageCard.qml b/plugins/Marketplace/resources/qml/PackageCard.qml index 6331c9f58c..89ae6091fc 100644 --- a/plugins/Marketplace/resources/qml/PackageCard.qml +++ b/plugins/Marketplace/resources/qml/PackageCard.qml @@ -355,12 +355,12 @@ Rectangle ManageButton { id: enableManageButton + state: packageData.stateManageEnableButton Layout.alignment: Qt.AlignTop primaryText: catalog.i18nc("@button", "Enable") busyPrimaryText: catalog.i18nc("@button", "enabling...") secondaryText: catalog.i18nc("@button", "Disable") busySecondaryText: catalog.i18nc("@button", "disabling...") - mainState: packageData.manageEnableState enabled: !(installManageButton.busy || updateManageButton.busy) } Connections @@ -382,13 +382,12 @@ Rectangle ManageButton { id: installManageButton + state: packageData.stateManageInstallButton Layout.alignment: Qt.AlignTop primaryText: catalog.i18nc("@button", "Install") busyPrimaryText: catalog.i18nc("@button", "installing...") secondaryText: catalog.i18nc("@button", "Uninstall") busySecondaryText: catalog.i18nc("@button", "uninstalling...") - mainState: packageData.manageInstallState - busy: packageData.isInstalling enabled: !(enableManageButton.busy || updateManageButton.busy) } Connections @@ -396,7 +395,6 @@ Rectangle target: installManageButton function onClicked(primary_action) { - packageData.isInstalling = true if (primary_action) { packageData.installPackageTriggered(packageData.packageId) @@ -411,11 +409,10 @@ Rectangle ManageButton { id: updateManageButton + state: packageData.stateManageUpdateButton Layout.alignment: Qt.AlignTop primaryText: catalog.i18nc("@button", "Update") busyPrimaryText: catalog.i18nc("@button", "updating...") - mainState: packageData.manageUpdateState - busy: packageData.isUpdating enabled: !(installManageButton.busy || enableManageButton.busy) } Connections @@ -423,7 +420,6 @@ Rectangle target: updateManageButton function onClicked(primary_action) { - packageData.isUpdating = true packageData.updatePackageTriggered(packageData.packageId) } } From 2d9c557a13eb710f9d5de24d4e1aaf6505bc89ee Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Mon, 6 Dec 2021 09:43:09 +0100 Subject: [PATCH 071/195] Only check for updates when logged in Contributes to: CURA-8587 --- plugins/Marketplace/LocalPackageList.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/plugins/Marketplace/LocalPackageList.py b/plugins/Marketplace/LocalPackageList.py index ea259dbdc7..622a47429d 100644 --- a/plugins/Marketplace/LocalPackageList.py +++ b/plugins/Marketplace/LocalPackageList.py @@ -65,14 +65,15 @@ class LocalPackageList(PackageList): return package def checkForUpdates(self, packages: List[Dict[str, Any]]): - installed_packages = "installed_packages=".join([f"{package['package_id']}:{package['package_version']}&" for package in packages]) - request_url = f"{PACKAGE_UPDATES_URL}?installed_packages={installed_packages[:-1]}" + if self._account.isLoggedIn: + installed_packages = "installed_packages=".join([f"{package['package_id']}:{package['package_version']}&" for package in packages]) + request_url = f"{PACKAGE_UPDATES_URL}?installed_packages={installed_packages[:-1]}" - self._ongoing_request = HttpRequestManager.getInstance().get( - request_url, - scope = self._scope, - callback = self._parseResponse - ) + self._ongoing_request = HttpRequestManager.getInstance().get( + request_url, + scope = self._scope, + callback = self._parseResponse + ) def _parseResponse(self, reply: "QNetworkReply") -> None: """ From 325783ca46ea1f4de1819abdc5b9093c4b8aa2ee Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Mon, 6 Dec 2021 10:19:14 +0100 Subject: [PATCH 072/195] Persistent handled state across Package Lists Contributes to: CURA-8587 --- plugins/Marketplace/LocalPackageList.py | 5 ++++- plugins/Marketplace/PackageList.py | 11 +++++++---- plugins/Marketplace/PackageModel.py | 18 ++++++++++++++---- plugins/Marketplace/RemotePackageList.py | 3 +-- .../Marketplace/resources/qml/ManageButton.qml | 2 -- 5 files changed, 26 insertions(+), 13 deletions(-) diff --git a/plugins/Marketplace/LocalPackageList.py b/plugins/Marketplace/LocalPackageList.py index 622a47429d..e6ad425954 100644 --- a/plugins/Marketplace/LocalPackageList.py +++ b/plugins/Marketplace/LocalPackageList.py @@ -57,10 +57,13 @@ class LocalPackageList(PackageList): def _makePackageModel(self, package_info: Dict[str, Any]) -> PackageModel: """ Create a PackageModel from the package_info and determine its section_title""" - bundled_or_installed = "installed" if self._manager.isUserInstalledPackage(package_info["package_id"]) else "bundled" + + bundled_or_installed = "bundled" if self._manager.isBundledPackage(package_info["package_id"]) else "installed" package_type = package_info["package_type"] section_title = self.PACKAGE_CATEGORIES[bundled_or_installed][package_type] package = PackageModel(package_info, section_title = section_title, parent = self) + if package_info["package_id"] in self._manager.getPackagesToRemove() or package_info["package_id"] in self._manager.getPackagesToInstall(): + package.is_recently_managed = True self._connectManageButtonSignals(package) return package diff --git a/plugins/Marketplace/PackageList.py b/plugins/Marketplace/PackageList.py index 5ad2c50ddb..301f765dab 100644 --- a/plugins/Marketplace/PackageList.py +++ b/plugins/Marketplace/PackageList.py @@ -4,7 +4,7 @@ import tempfile import json from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, Qt -from typing import Dict, Optional, TYPE_CHECKING +from typing import Dict, Optional, Set, TYPE_CHECKING from UM.i18n import i18nCatalog from UM.Qt.ListModel import ListModel @@ -43,6 +43,7 @@ class PackageList(ListModel): self._has_footer = True self._to_install: Dict[str, str] = {} self.canInstallChanged.connect(self._install) + self._local_packages: Set[str] = {p["package_id"] for p in self._manager.local_packages} self._ongoing_request: Optional[HttpRequestData] = None self._scope = JsonDecoratorScope(UltimakerCloudScope(CuraApplication.getInstance())) @@ -128,7 +129,8 @@ class PackageList(ListModel): if update: package.is_updating = False else: - package.is_recently_installed = True + Logger.debug(f"Setting recently installed for package: {package_id}") + package.is_recently_managed = True package.is_installing = False self.subscribeUserToPackage(package_id, str(package.sdk_version)) @@ -201,7 +203,7 @@ class PackageList(ListModel): package.is_installing = True url = package.download_url Logger.debug(f"Trying to download and install {package_id} from {url}") - self.download(package_id, url) + self.download(package_id, url, False) @pyqtSlot(str) def uninstallPackage(self, package_id: str) -> None: @@ -209,8 +211,9 @@ class PackageList(ListModel): package = self.getPackageModel(package_id) package.is_installing = True self._manager.removePackage(package_id) - package.is_installing = False self.unsunscribeUserFromPackage(package_id) + package.is_installing = False + package.is_recently_managed = True @pyqtSlot(str) def updatePackage(self, package_id: str) -> None: diff --git a/plugins/Marketplace/PackageModel.py b/plugins/Marketplace/PackageModel.py index 8ba712c19d..58184ac1c3 100644 --- a/plugins/Marketplace/PackageModel.py +++ b/plugins/Marketplace/PackageModel.py @@ -62,7 +62,7 @@ class PackageModel(QObject): self._icon_url = author_data.get("icon_url", "") self._is_installing = False - self.is_recently_installed = False + self._is_recently_managed = False self._can_update = False self._is_updating = False self._is_enabling = False @@ -284,7 +284,7 @@ class PackageModel(QObject): def stateManageEnableButton(self) -> str: if self._is_enabling: return "busy" - if self.is_recently_installed: + if self._is_recently_managed: return "hidden" if self._package_type == "material": if self._is_bundled: # TODO: Check if a bundled material can/should be un-/install en-/disabled @@ -312,8 +312,8 @@ class PackageModel(QObject): def stateManageInstallButton(self) -> str: if self._is_installing: return "busy" - if self.is_recently_installed: - return "secondary" + if self._is_recently_managed: + return "hidden" if self._is_installed: if self._is_bundled: return "hidden" @@ -322,6 +322,16 @@ class PackageModel(QObject): else: return "primary" + @property + def is_recently_managed(self) -> bool: + return self._is_recently_managed + + @is_recently_managed.setter + def is_recently_managed(self, value: bool) -> None: + if value != self._is_recently_managed: + self._is_recently_managed = value + self.stateManageButtonChanged.emit() + @property def is_installing(self) -> bool: return self._is_installing diff --git a/plugins/Marketplace/RemotePackageList.py b/plugins/Marketplace/RemotePackageList.py index a994d3d2d2..88e1c28045 100644 --- a/plugins/Marketplace/RemotePackageList.py +++ b/plugins/Marketplace/RemotePackageList.py @@ -3,7 +3,7 @@ from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot from PyQt5.QtNetwork import QNetworkReply -from typing import Optional, Set, TYPE_CHECKING +from typing import Optional, TYPE_CHECKING from UM.i18n import i18nCatalog from UM.Logger import Logger @@ -31,7 +31,6 @@ class RemotePackageList(PackageList): self._request_url = self._initialRequestUrl() self.isLoadingChanged.connect(self._onLoadingChanged) self.isLoadingChanged.emit() - self._local_packages: Set[str] = { p["package_id"] for p in self._manager.local_packages } def __del__(self) -> None: """ diff --git a/plugins/Marketplace/resources/qml/ManageButton.qml b/plugins/Marketplace/resources/qml/ManageButton.qml index dd17d07d01..cdd52c9b90 100644 --- a/plugins/Marketplace/resources/qml/ManageButton.qml +++ b/plugins/Marketplace/resources/qml/ManageButton.qml @@ -21,8 +21,6 @@ RowLayout signal clicked(bool primary_action) - state: busy ? "busy" : mainState - Cura.PrimaryButton { id: primaryButton From 7a902f8314873336ea4f9a0ff65893e53e580850 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 6 Dec 2021 11:28:06 +0100 Subject: [PATCH 073/195] Add restart required banner CURA-8587 --- .../Marketplace/resources/qml/Marketplace.qml | 47 +++++++++++++++++++ resources/themes/cura-light/theme.json | 2 +- 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/plugins/Marketplace/resources/qml/Marketplace.qml b/plugins/Marketplace/resources/qml/Marketplace.qml index 44f7777b35..98de8b0534 100644 --- a/plugins/Marketplace/resources/qml/Marketplace.qml +++ b/plugins/Marketplace/resources/qml/Marketplace.qml @@ -228,4 +228,51 @@ Window } } } + + Rectangle + { + height: quitButton.height + 2 * UM.Theme.getSize("default_margin").width + color: UM.Theme.getColor("primary") + visible: false // TODO: enable this when restart is required + anchors + { + left: parent.left + right: parent.right + bottom: parent.bottom + } + + RowLayout + { + anchors + { + left: parent.left + right: parent.right + verticalCenter: parent.verticalCenter + margins: UM.Theme.getSize("default_margin").width + } + spacing: UM.Theme.getSize("default_margin").width + UM.RecolorImage + { + id: bannerIcon + source: UM.Theme.getIcon("Plugin") + + color: UM.Theme.getColor("primary_button_text") + implicitWidth: UM.Theme.getSize("banner_icon_size").width + implicitHeight: UM.Theme.getSize("banner_icon_size").height + } + Text + { + color: UM.Theme.getColor("primary_button_text") + text: catalog.i18nc("@button", "In order to use the package you will need to restart Cura") + font: UM.Theme.getFont("default") + renderType: Text.NativeRendering + Layout.fillWidth: true + } + Cura.SecondaryButton + { + id: quitButton + text: catalog.i18nc("@button", "Quit Ultimaker Cura") + } + } + } } diff --git a/resources/themes/cura-light/theme.json b/resources/themes/cura-light/theme.json index 8ca9f72ca8..149bfab308 100644 --- a/resources/themes/cura-light/theme.json +++ b/resources/themes/cura-light/theme.json @@ -179,7 +179,7 @@ "lining": [192, 193, 194, 255], "viewport_overlay": [246, 246, 246, 255], - "primary": [50, 130, 255, 255], + "primary": [25, 110, 240, 255], "primary_shadow": [64, 47, 205, 255], "primary_hover": [48, 182, 231, 255], "primary_text": [255, 255, 255, 255], From de0dc568cded847c6eb99fa7f328edb8ba760c27 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 6 Dec 2021 11:30:27 +0100 Subject: [PATCH 074/195] Change rectangle into item It had no color, so it doesn't need to be a rectangle Cura-8587 --- plugins/Marketplace/resources/qml/Marketplace.qml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/plugins/Marketplace/resources/qml/Marketplace.qml b/plugins/Marketplace/resources/qml/Marketplace.qml index 98de8b0534..62f9673072 100644 --- a/plugins/Marketplace/resources/qml/Marketplace.qml +++ b/plugins/Marketplace/resources/qml/Marketplace.qml @@ -106,9 +106,8 @@ Window height: UM.Theme.getSize("button_icon").height + UM.Theme.getSize("default_margin").height spacing: UM.Theme.getSize("thin_margin").width - Rectangle + Item { - color: "transparent" Layout.preferredHeight: parent.height Layout.preferredWidth: searchBar.visible ? UM.Theme.getSize("thin_margin").width : 0 Layout.fillWidth: ! searchBar.visible From ea85e59e501710478b592064420a183ecbac33b0 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 6 Dec 2021 11:34:19 +0100 Subject: [PATCH 075/195] Fix margin CURA-8587 --- plugins/Marketplace/resources/qml/PackageCard.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/Marketplace/resources/qml/PackageCard.qml b/plugins/Marketplace/resources/qml/PackageCard.qml index 89ae6091fc..31716ed005 100644 --- a/plugins/Marketplace/resources/qml/PackageCard.qml +++ b/plugins/Marketplace/resources/qml/PackageCard.qml @@ -94,7 +94,7 @@ Rectangle left: packageItem.right leftMargin: UM.Theme.getSize("default_margin").width right: parent.right - rightMargin: UM.Theme.getSize("thick_margin").width + rightMargin: UM.Theme.getSize("default_margin").width top: parent.top topMargin: UM.Theme.getSize("narrow_margin").height } From 579cc7bdbc582687ce73efc1b0ac26b9da93a788 Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Mon, 6 Dec 2021 11:34:23 +0100 Subject: [PATCH 076/195] Enable or disable a plugin functionality added Contributes to: CURA-8587 --- plugins/Marketplace/PackageList.py | 8 ++++++-- plugins/Marketplace/PackageModel.py | 12 ++++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/plugins/Marketplace/PackageList.py b/plugins/Marketplace/PackageList.py index 301f765dab..965484faff 100644 --- a/plugins/Marketplace/PackageList.py +++ b/plugins/Marketplace/PackageList.py @@ -11,6 +11,7 @@ from UM.Qt.ListModel import ListModel from UM.TaskManagement.HttpRequestScope import JsonDecoratorScope from UM.TaskManagement.HttpRequestManager import HttpRequestData, HttpRequestManager from UM.Logger import Logger +from UM import PluginRegistry from cura.CuraApplication import CuraApplication from cura import CuraPackageManager @@ -35,6 +36,7 @@ class PackageList(ListModel): def __init__(self, parent: Optional["QObject"] = None) -> None: super().__init__(parent) self._manager: CuraPackageManager = CuraApplication.getInstance().getPackageManager() + self._plugin_registry: PluginRegistry = CuraApplication.getInstance().getPluginRegistry() self._account = CuraApplication.getInstance().getCuraAPI().account self._error_message = "" self.addRoleName(self.PackageRole, "package") @@ -229,7 +231,8 @@ class PackageList(ListModel): package = self.getPackageModel(package_id) package.is_enabling = True Logger.debug(f"Enabling {package_id}") - # TODO: implement enabling functionality + self._plugin_registry.enablePlugin(package_id) + package.is_active = True package.is_enabling = False @pyqtSlot(str) @@ -237,5 +240,6 @@ class PackageList(ListModel): package = self.getPackageModel(package_id) package.is_enabling = True Logger.debug(f"Disabling {package_id}") - # TODO: implement disabling functionality + self._plugin_registry.disablePlugin(package_id) + package.is_active = False package.is_enabling = False diff --git a/plugins/Marketplace/PackageModel.py b/plugins/Marketplace/PackageModel.py index 58184ac1c3..f1b5b202a3 100644 --- a/plugins/Marketplace/PackageModel.py +++ b/plugins/Marketplace/PackageModel.py @@ -287,8 +287,6 @@ class PackageModel(QObject): if self._is_recently_managed: return "hidden" if self._package_type == "material": - if self._is_bundled: # TODO: Check if a bundled material can/should be un-/install en-/disabled - return "secondary" return "hidden" if not self._is_installed: return "hidden" @@ -306,6 +304,16 @@ class PackageModel(QObject): self._is_enabling = value self.stateManageButtonChanged.emit() + @property + def is_active(self) -> bool: + return self._is_active + + @is_active.setter + def is_active(self, value: bool) -> None: + if value != self._is_active: + self._is_active = value + self.stateManageButtonChanged.emit() + # --- Installing --- @pyqtProperty(str, notify = stateManageButtonChanged) From 18837a32b8e4a23abae31eb4190074aef81932bf Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 6 Dec 2021 11:54:14 +0100 Subject: [PATCH 077/195] Move verifiedIcon to it's own file Cleaning up the size of the package card a bit --- .../Marketplace/resources/qml/PackageCard.qml | 38 +--------------- .../resources/qml/VerifiedIcon.qml | 45 +++++++++++++++++++ 2 files changed, 47 insertions(+), 36 deletions(-) create mode 100644 plugins/Marketplace/resources/qml/VerifiedIcon.qml diff --git a/plugins/Marketplace/resources/qml/PackageCard.qml b/plugins/Marketplace/resources/qml/PackageCard.qml index 31716ed005..e4f56632df 100644 --- a/plugins/Marketplace/resources/qml/PackageCard.qml +++ b/plugins/Marketplace/resources/qml/PackageCard.qml @@ -114,47 +114,13 @@ Rectangle color: UM.Theme.getColor("text") verticalAlignment: Text.AlignTop } - - Control + VerifiedIcon { - Layout.preferredWidth: UM.Theme.getSize("card_tiny_icon").width - Layout.preferredHeight: UM.Theme.getSize("card_tiny_icon").height - enabled: packageData.isCheckedByUltimaker visible: packageData.isCheckedByUltimaker - - Cura.ToolTip - { - tooltipText: - { - switch(packageData.packageType) - { - case "plugin": return catalog.i18nc("@info", "Ultimaker Verified Plug-in"); - case "material": return catalog.i18nc("@info", "Ultimaker Certified Material"); - default: return catalog.i18nc("@info", "Ultimaker Verified Package"); - } - } - visible: parent.hovered - targetPoint: Qt.point(0, Math.round(parent.y + parent.height / 4)) - } - - Rectangle - { - anchors.fill: parent - color: UM.Theme.getColor("action_button_hovered") - radius: width - UM.RecolorImage - { - anchors.fill: parent - color: UM.Theme.getColor("primary") - source: packageData.packageType == "plugin" ? UM.Theme.getIcon("CheckCircle") : UM.Theme.getIcon("Certified") - } - } - - //NOTE: Can we link to something here? (Probably a static link explaining what verified is): - // onClicked: Qt.openUrlExternally( XXXXXX ) } + Control { Layout.preferredWidth: UM.Theme.getSize("card_tiny_icon").width diff --git a/plugins/Marketplace/resources/qml/VerifiedIcon.qml b/plugins/Marketplace/resources/qml/VerifiedIcon.qml new file mode 100644 index 0000000000..30ef3080a0 --- /dev/null +++ b/plugins/Marketplace/resources/qml/VerifiedIcon.qml @@ -0,0 +1,45 @@ +// Copyright (c) 2021 Ultimaker B.V. +// Cura is released under the terms of the LGPLv3 or higher. + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.1 + +import UM 1.6 as UM +import Cura 1.6 as Cura +Control +{ + implicitWidth: UM.Theme.getSize("card_tiny_icon").width + implicitHeight: UM.Theme.getSize("card_tiny_icon").height + + Cura.ToolTip + { + tooltipText: + { + switch(packageData.packageType) + { + case "plugin": return catalog.i18nc("@info", "Ultimaker Verified Plug-in"); + case "material": return catalog.i18nc("@info", "Ultimaker Certified Material"); + default: return catalog.i18nc("@info", "Ultimaker Verified Package"); + } + } + visible: parent.hovered + targetPoint: Qt.point(0, Math.round(parent.y + parent.height / 4)) + } + + Rectangle + { + anchors.fill: parent + color: UM.Theme.getColor("action_button_hovered") + radius: width + UM.RecolorImage + { + anchors.fill: parent + color: UM.Theme.getColor("primary") + source: packageData.packageType == "plugin" ? UM.Theme.getIcon("CheckCircle") : UM.Theme.getIcon("Certified") + } + } + + //NOTE: Can we link to something here? (Probably a static link explaining what verified is): + // onClicked: Qt.openUrlExternally( XXXXXX ) +} \ No newline at end of file From 09710c2d9f8b51bcc330e2d27a53af925102181a Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 6 Dec 2021 12:02:14 +0100 Subject: [PATCH 078/195] Remove unneeded connection The connection isn't needed to handle a signal. There is an easier shorthand CURA-8587 --- .../Marketplace/resources/qml/PackageCard.qml | 23 ++++--------------- 1 file changed, 4 insertions(+), 19 deletions(-) diff --git a/plugins/Marketplace/resources/qml/PackageCard.qml b/plugins/Marketplace/resources/qml/PackageCard.qml index e4f56632df..bf0dda217b 100644 --- a/plugins/Marketplace/resources/qml/PackageCard.qml +++ b/plugins/Marketplace/resources/qml/PackageCard.qml @@ -328,12 +328,8 @@ Rectangle secondaryText: catalog.i18nc("@button", "Disable") busySecondaryText: catalog.i18nc("@button", "disabling...") enabled: !(installManageButton.busy || updateManageButton.busy) - } - Connections - { - target: enableManageButton - function onClicked(primary_action) - { + + onClicked: { if (primary_action) { packageData.enablePackageTriggered(packageData.packageId) @@ -355,11 +351,7 @@ Rectangle secondaryText: catalog.i18nc("@button", "Uninstall") busySecondaryText: catalog.i18nc("@button", "uninstalling...") enabled: !(enableManageButton.busy || updateManageButton.busy) - } - Connections - { - target: installManageButton - function onClicked(primary_action) + onClicked: { if (primary_action) { @@ -380,14 +372,7 @@ Rectangle primaryText: catalog.i18nc("@button", "Update") busyPrimaryText: catalog.i18nc("@button", "updating...") enabled: !(installManageButton.busy || enableManageButton.busy) - } - Connections - { - target: updateManageButton - function onClicked(primary_action) - { - packageData.updatePackageTriggered(packageData.packageId) - } + onClicked: packageData.updatePackageTriggered(packageData.packageId) } } } From 28b6bfb72906057cbc5368494dc582c30e25bbf0 Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Mon, 6 Dec 2021 13:53:44 +0100 Subject: [PATCH 079/195] Fixed the update button busy state Contributes to: CURA-8587 --- plugins/Marketplace/LocalPackageList.py | 17 ++--- plugins/Marketplace/PackageList.py | 3 +- plugins/Marketplace/PackageModel.py | 75 ------------------- .../resources/qml/ManageButton.qml | 5 +- 4 files changed, 11 insertions(+), 89 deletions(-) diff --git a/plugins/Marketplace/LocalPackageList.py b/plugins/Marketplace/LocalPackageList.py index e6ad425954..5b3c05609e 100644 --- a/plugins/Marketplace/LocalPackageList.py +++ b/plugins/Marketplace/LocalPackageList.py @@ -68,15 +68,14 @@ class LocalPackageList(PackageList): return package def checkForUpdates(self, packages: List[Dict[str, Any]]): - if self._account.isLoggedIn: - installed_packages = "installed_packages=".join([f"{package['package_id']}:{package['package_version']}&" for package in packages]) - request_url = f"{PACKAGE_UPDATES_URL}?installed_packages={installed_packages[:-1]}" + installed_packages = "installed_packages=".join([f"{package['package_id']}:{package['package_version']}&" for package in packages]) + request_url = f"{PACKAGE_UPDATES_URL}?installed_packages={installed_packages[:-1]}" - self._ongoing_request = HttpRequestManager.getInstance().get( - request_url, - scope = self._scope, - callback = self._parseResponse - ) + self._ongoing_request = HttpRequestManager.getInstance().get( + request_url, + scope = self._scope, + callback = self._parseResponse + ) def _parseResponse(self, reply: "QNetworkReply") -> None: """ @@ -95,6 +94,6 @@ class LocalPackageList(PackageList): for package_data in response_data["data"]: package = self.getPackageModel(package_data["package_id"]) package.download_url = package_data.get("download_url", "") - package.canUpdate = True + package.can_update = True self.sort(attrgetter("sectionTitle", "can_update", "displayName"), key = "package", reverse = True) diff --git a/plugins/Marketplace/PackageList.py b/plugins/Marketplace/PackageList.py index 965484faff..6a931c1e4b 100644 --- a/plugins/Marketplace/PackageList.py +++ b/plugins/Marketplace/PackageList.py @@ -131,7 +131,6 @@ class PackageList(ListModel): if update: package.is_updating = False else: - Logger.debug(f"Setting recently installed for package: {package_id}") package.is_recently_managed = True package.is_installing = False self.subscribeUserToPackage(package_id, str(package.sdk_version)) @@ -195,7 +194,7 @@ class PackageList(ListModel): def _connectManageButtonSignals(self, package: PackageModel) -> None: package.installPackageTriggered.connect(self.installPackage) package.uninstallPackageTriggered.connect(self.uninstallPackage) - package.updatePackageTriggered.connect(self.installPackage) + package.updatePackageTriggered.connect(self.updatePackage) package.enablePackageTriggered.connect(self.enablePackage) package.disablePackageTriggered.connect(self.disablePackage) diff --git a/plugins/Marketplace/PackageModel.py b/plugins/Marketplace/PackageModel.py index f1b5b202a3..5251f03524 100644 --- a/plugins/Marketplace/PackageModel.py +++ b/plugins/Marketplace/PackageModel.py @@ -379,78 +379,3 @@ class PackageModel(QObject): if value != self._can_update: self._can_update = value self.stateManageButtonChanged.emit() - - # ---- - - - - - - - - - - - # isInstalledChanged = pyqtSignal() - # - # @pyqtProperty(bool, notify = isInstalledChanged) - # def isInstalled(self): - # return self._is_installed - # - # isEnabledChanged = pyqtSignal() - # - # - #f - # @pyqtProperty(bool, notify = isEnabledChanged) - # def isEnabled(self) -> bool: - # return self._is_active - # - # - # - # isManageEnableStateChanged = pyqtSignalf() - # - # @pyqtProperty(str, notify = isManageEnableStateChanged) - # def isManageEnableState(self) -> str: - # if self.isEnabling: - # return "busy" - # if self. - # - # manageEnableStateChanged = pyqtSignal() - # - # @pyqtProperty(str, notify = manageEnableStateChanged) - # def manageEnableState(self) -> str: - # # TODO: Handle manual installed packages - # if self._is_installed: - # if self._is_active: - # return "secondary" - # else: - # return "primary" - # else: - # return "hidden" - # - # manageInstallStateChanged = pyqtSignal() - # - # def setManageInstallState(self, value: bool) -> None: - # if value != self._is_installed: - # self._is_installed = value - # self.manageInstallStateChanged.emit() - # self.manageEnableStateChanged.emit() - # - # @pyqtProperty(str, notify = manageInstallStateChanged) - # def manageInstallState(self) -> str: - # if self._is_installed: - # if self._is_bundled: - # return "hidden" - # else: - # return "secondary" - # else: - # return "primary" - # - # manageUpdateStateChanged = pyqtSignal() - # - # @pyqtProperty(str, notify = manageUpdateStateChanged) - # def manageUpdateState(self) -> str: - # if self._can_update: - # return "primary" - # return "hidden" - # diff --git a/plugins/Marketplace/resources/qml/ManageButton.qml b/plugins/Marketplace/resources/qml/ManageButton.qml index cdd52c9b90..323fea03f1 100644 --- a/plugins/Marketplace/resources/qml/ManageButton.qml +++ b/plugins/Marketplace/resources/qml/ManageButton.qml @@ -15,9 +15,8 @@ RowLayout property alias secondaryText: secondaryButton.text property string busyPrimaryText: busyMessageText.text property string busySecondaryText: busyMessageText.text - property string mainState: "primary" property bool enabled: true - property bool busy + property bool busy: state == "busy" signal clicked(bool primary_action) @@ -77,7 +76,7 @@ RowLayout { id: busyMessageText visible: parent.visible - text: manageButton.mainState == "primary" ? manageButton.busyPrimaryText : manageButton.busySecondaryText + text: manageButton.state == "primary" ? manageButton.busyPrimaryText : manageButton.busySecondaryText anchors.left: busyIndicator.right anchors.verticalCenter: parent.verticalCenter From 1cc3d374a02ef3bbc4140b6b1916b41e3013bb6f Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Mon, 6 Dec 2021 14:11:31 +0100 Subject: [PATCH 080/195] Show the uninstall button for packages which can be downgraded to the bundled version Contributes to: CURA-8587 --- plugins/Marketplace/LocalPackageList.py | 6 ++++-- plugins/Marketplace/PackageModel.py | 13 ++++++++++++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/plugins/Marketplace/LocalPackageList.py b/plugins/Marketplace/LocalPackageList.py index 5b3c05609e..a1f3f45e7e 100644 --- a/plugins/Marketplace/LocalPackageList.py +++ b/plugins/Marketplace/LocalPackageList.py @@ -58,12 +58,14 @@ class LocalPackageList(PackageList): def _makePackageModel(self, package_info: Dict[str, Any]) -> PackageModel: """ Create a PackageModel from the package_info and determine its section_title""" - bundled_or_installed = "bundled" if self._manager.isBundledPackage(package_info["package_id"]) else "installed" + package_id = package_info["package_id"] + bundled_or_installed = "bundled" if self._manager.isBundledPackage(package_id) else "installed" package_type = package_info["package_type"] section_title = self.PACKAGE_CATEGORIES[bundled_or_installed][package_type] package = PackageModel(package_info, section_title = section_title, parent = self) - if package_info["package_id"] in self._manager.getPackagesToRemove() or package_info["package_id"] in self._manager.getPackagesToInstall(): + if package_id in self._manager.getPackagesToRemove() or package_id in self._manager.getPackagesToInstall(): package.is_recently_managed = True + package.can_downgrade = self._manager.canDowngrade(package_id) self._connectManageButtonSignals(package) return package diff --git a/plugins/Marketplace/PackageModel.py b/plugins/Marketplace/PackageModel.py index 5251f03524..92daf310a3 100644 --- a/plugins/Marketplace/PackageModel.py +++ b/plugins/Marketplace/PackageModel.py @@ -66,6 +66,7 @@ class PackageModel(QObject): self._can_update = False self._is_updating = False self._is_enabling = False + self._can_downgrade = False self._section_title = section_title self.sdk_version = package_data.get("sdk_version_semver", "") # Note that there's a lot more info in the package_data than just these specified here. @@ -323,7 +324,7 @@ class PackageModel(QObject): if self._is_recently_managed: return "hidden" if self._is_installed: - if self._is_bundled: + if self._is_bundled and not self._can_downgrade: return "hidden" else: return "secondary" @@ -350,6 +351,16 @@ class PackageModel(QObject): self._is_installing = value self.stateManageButtonChanged.emit() + @property + def can_downgrade(self) -> bool: + return self._can_downgrade + + @can_downgrade.setter + def can_downgrade(self, value: bool) -> None: + if value != self._can_downgrade: + self._can_downgrade = value + self.stateManageButtonChanged.emit() + # --- Updating --- @pyqtProperty(str, notify = stateManageButtonChanged) From 09e221d64a3954b515e86dccb786b0a9101804f6 Mon Sep 17 00:00:00 2001 From: casper Date: Mon, 6 Dec 2021 14:49:50 +0100 Subject: [PATCH 081/195] Add license dialog to the Marketplace cura 8587 --- plugins/Marketplace/LicenseModel.py | 66 +++++++++++ plugins/Marketplace/PackageList.py | 39 ++++++- .../resources/qml/LicenseDialog.qml | 110 ++++++++++++++++++ 3 files changed, 214 insertions(+), 1 deletion(-) create mode 100644 plugins/Marketplace/LicenseModel.py create mode 100644 plugins/Marketplace/resources/qml/LicenseDialog.qml diff --git a/plugins/Marketplace/LicenseModel.py b/plugins/Marketplace/LicenseModel.py new file mode 100644 index 0000000000..cb85b33430 --- /dev/null +++ b/plugins/Marketplace/LicenseModel.py @@ -0,0 +1,66 @@ +from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal +from UM.i18n import i18nCatalog + +catalog = i18nCatalog("cura") + +# Model for the LicenseDialog +class LicenseModel(QObject): + DEFAULT_DECLINE_BUTTON_TEXT = catalog.i18nc("@button", "Decline") + ACCEPT_BUTTON_TEXT = catalog.i18nc("@button", "Agree") + + dialogTitleChanged = pyqtSignal() + packageNameChanged = pyqtSignal() + licenseTextChanged = pyqtSignal() + iconChanged = pyqtSignal() + + def __init__(self, decline_button_text: str = DEFAULT_DECLINE_BUTTON_TEXT) -> None: + super().__init__() + + self._dialogTitle = "" + self._license_text = "" + self._package_name = "" + self._icon_url = "" + self._decline_button_text = decline_button_text + + @pyqtProperty(str, constant = True) + def acceptButtonText(self): + return self.ACCEPT_BUTTON_TEXT + + @pyqtProperty(str, constant = True) + def declineButtonText(self): + return self._decline_button_text + + @pyqtProperty(str, notify=dialogTitleChanged) + def dialogTitle(self) -> str: + return self._dialogTitle + + @pyqtProperty(str, notify=packageNameChanged) + def packageName(self) -> str: + return self._package_name + + def setPackageName(self, name: str) -> None: + self._package_name = name + self.packageNameChanged.emit() + + @pyqtProperty(str, notify=iconChanged) + def iconUrl(self) -> str: + return self._icon_url + + def setIconUrl(self, url: str): + self._icon_url = url + self.iconChanged.emit() + + @pyqtProperty(str, notify=licenseTextChanged) + def licenseText(self) -> str: + return self._license_text + + def setLicenseText(self, license_text: str) -> None: + if self._license_text != license_text: + self._license_text = license_text + self.licenseTextChanged.emit() + + def _updateDialogTitle(self): + self._dialogTitle = catalog.i18nc("@title:window", "Plugin License Agreement") + if self._page_count > 1: + self._dialogTitle = self._dialogTitle + " ({}/{})".format(self._current_page_idx + 1, self._page_count) + self.dialogTitleChanged.emit() diff --git a/plugins/Marketplace/PackageList.py b/plugins/Marketplace/PackageList.py index 301f765dab..0c5895866c 100644 --- a/plugins/Marketplace/PackageList.py +++ b/plugins/Marketplace/PackageList.py @@ -2,6 +2,7 @@ # Cura is released under the terms of the LGPLv3 or higher. import tempfile import json +import os.path from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, Qt from typing import Dict, Optional, Set, TYPE_CHECKING @@ -11,6 +12,7 @@ from UM.Qt.ListModel import ListModel from UM.TaskManagement.HttpRequestScope import JsonDecoratorScope from UM.TaskManagement.HttpRequestManager import HttpRequestData, HttpRequestManager from UM.Logger import Logger +from UM.PluginRegistry import PluginRegistry from cura.CuraApplication import CuraApplication from cura import CuraPackageManager @@ -18,6 +20,7 @@ from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope # To ma from .PackageModel import PackageModel from .Constants import USER_PACKAGES_URL +from .LicenseModel import LicenseModel if TYPE_CHECKING: from PyQt5.QtCore import QObject @@ -42,12 +45,24 @@ class PackageList(ListModel): self._has_more = False self._has_footer = True self._to_install: Dict[str, str] = {} - self.canInstallChanged.connect(self._install) + self.canInstallChanged.connect(self._requestInstall) self._local_packages: Set[str] = {p["package_id"] for p in self._manager.local_packages} self._ongoing_request: Optional[HttpRequestData] = None self._scope = JsonDecoratorScope(UltimakerCloudScope(CuraApplication.getInstance())) + self._license_model = LicenseModel() + + plugin_path = PluginRegistry.getInstance().getPluginPath("Marketplace") + if plugin_path is None: + plugin_path = os.path.dirname(__file__) + + # create a QML component for the license dialog + license_dialog_component_path = os.path.join(plugin_path, "resources", "qml", "LicenseDialog.qml") + self._license_dialog = CuraApplication.getInstance().createQmlComponent(license_dialog_component_path, { + "licenseModel": self._license_model + }) + @pyqtSlot() def updatePackages(self) -> None: """ A Qt slot which will update the List from a source. Actual implementation should be done in the child class""" @@ -119,6 +134,28 @@ class PackageList(ListModel): canInstallChanged = pyqtSignal(str, bool) + def _openLicenseDialog(self, plugin_name: str, license_content: str, icon_url: str) -> None: + self._license_model.setIconUrl(icon_url) + self._license_model.setPackageName(plugin_name) + self._license_model.setLicenseText(license_content) + self._license_dialog.show() + + def _requestInstall(self, package_id: str, update: bool = False) -> None: + Logger.debug(f"Request installing {package_id}") + + package_path = self._to_install.pop(package_id) + license_content = self._manager.getPackageLicense(package_path) + + if not update and license_content is not None: + # open dialog, prompting the using to accept the plugin license + package = self.getPackageModel(package_id) + plugin_name = package.displayName + icon_url = package.iconUrl + self._openLicenseDialog(plugin_name, license_content, icon_url) + else: + # Otherwise continue the installation + self._install(package_id, update) + def _install(self, package_id: str, update: bool = False) -> None: package_path = self._to_install.pop(package_id) Logger.debug(f"Installing {package_id}") diff --git a/plugins/Marketplace/resources/qml/LicenseDialog.qml b/plugins/Marketplace/resources/qml/LicenseDialog.qml new file mode 100644 index 0000000000..9219f4ed32 --- /dev/null +++ b/plugins/Marketplace/resources/qml/LicenseDialog.qml @@ -0,0 +1,110 @@ +// Copyright (c) 2018 Ultimaker B.V. +// Toolbox is released under the terms of the LGPLv3 or higher. + +import QtQuick 2.10 +import QtQuick.Dialogs 1.1 +import QtQuick.Window 2.2 +import QtQuick.Controls 2.3 +import QtQuick.Layouts 1.3 +import QtQuick.Controls.Styles 1.4 + +import UM 1.1 as UM +import Cura 1.6 as Cura + +UM.Dialog +{ + id: licenseDialog + title: licenseModel.dialogTitle + minimumWidth: UM.Theme.getSize("license_window_minimum").width + minimumHeight: UM.Theme.getSize("license_window_minimum").height + width: minimumWidth + height: minimumHeight + backgroundColor: UM.Theme.getColor("main_background") + margin: screenScaleFactor * 10 + + ColumnLayout + { + anchors.fill: parent + spacing: UM.Theme.getSize("thick_margin").height + + UM.I18nCatalog{id: catalog; name: "cura"} + + Label + { + id: licenseHeader + Layout.fillWidth: true + text: catalog.i18nc("@label", "You need to accept the license to install the package") + color: UM.Theme.getColor("text") + wrapMode: Text.Wrap + renderType: Text.NativeRendering + } + + Row { + id: packageRow + + Layout.fillWidth: true + height: childrenRect.height + spacing: UM.Theme.getSize("default_margin").width + leftPadding: UM.Theme.getSize("narrow_margin").width + + Image + { + id: icon + width: 30 * screenScaleFactor + height: width + sourceSize.width: width + sourceSize.height: height + fillMode: Image.PreserveAspectFit + source: licenseModel.iconUrl || "../../images/placeholder.svg" + mipmap: true + } + + Label + { + id: packageName + text: licenseModel.packageName + color: UM.Theme.getColor("text") + font.bold: true + anchors.verticalCenter: icon.verticalCenter + height: contentHeight + wrapMode: Text.Wrap + renderType: Text.NativeRendering + } + + + } + + Cura.ScrollableTextArea + { + + Layout.fillWidth: true + Layout.fillHeight: true + anchors.topMargin: UM.Theme.getSize("default_margin").height + + textArea.text: licenseModel.licenseText + textArea.readOnly: true + } + + } + rightButtons: + [ + Cura.PrimaryButton + { + leftPadding: UM.Theme.getSize("dialog_primary_button_padding").width + rightPadding: UM.Theme.getSize("dialog_primary_button_padding").width + + text: licenseModel.acceptButtonText + onClicked: { handler.onLicenseAccepted() } + } + ] + + leftButtons: + [ + Cura.SecondaryButton + { + id: declineButton + text: licenseModel.declineButtonText + onClicked: { handler.onLicenseDeclined() } + } + ] +} From c3665d7440d1beaa1a826d1fc874cfe39b1f70aa Mon Sep 17 00:00:00 2001 From: casper Date: Mon, 6 Dec 2021 15:45:27 +0100 Subject: [PATCH 082/195] Correctly handle actions from marketplace license prompt Install package on accept And stop spinner on decline cura 8587 --- plugins/Marketplace/PackageList.py | 36 ++++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/plugins/Marketplace/PackageList.py b/plugins/Marketplace/PackageList.py index 0c5895866c..7bb1076988 100644 --- a/plugins/Marketplace/PackageList.py +++ b/plugins/Marketplace/PackageList.py @@ -60,7 +60,8 @@ class PackageList(ListModel): # create a QML component for the license dialog license_dialog_component_path = os.path.join(plugin_path, "resources", "qml", "LicenseDialog.qml") self._license_dialog = CuraApplication.getInstance().createQmlComponent(license_dialog_component_path, { - "licenseModel": self._license_model + "licenseModel": self._license_model, + "handler": self }) @pyqtSlot() @@ -135,11 +136,32 @@ class PackageList(ListModel): canInstallChanged = pyqtSignal(str, bool) def _openLicenseDialog(self, plugin_name: str, license_content: str, icon_url: str) -> None: + Logger.debug(f"Prompting license for {plugin_name}") self._license_model.setIconUrl(icon_url) self._license_model.setPackageName(plugin_name) self._license_model.setLicenseText(license_content) self._license_dialog.show() + @pyqtSlot() + def onLicenseAccepted(self) -> None: + package_id = self._to_be_installed_package_id + package_path = self._to_be_installed_package_path + del self._to_be_installed_package_id + del self._to_be_installed_package_path + Logger.debug(f"Accepted license for {package_id}") + self._license_dialog.close() + self._install(package_id, package_path) + + @pyqtSlot() + def onLicenseDeclined(self) -> None: + package_id = self._to_be_installed_package_id + del self._to_be_installed_package_id + del self._to_be_installed_package_path + Logger.debug(f"Declined license for {package_id}") + self._license_dialog.close() + package = self.getPackageModel(package_id) + package.is_installing = False + def _requestInstall(self, package_id: str, update: bool = False) -> None: Logger.debug(f"Request installing {package_id}") @@ -147,17 +169,23 @@ class PackageList(ListModel): license_content = self._manager.getPackageLicense(package_path) if not update and license_content is not None: + # If installation is not and update, and the packages contains a license then # open dialog, prompting the using to accept the plugin license + + # Store some properties that are needed after the dialog is closed + self._to_be_installed_package_id = package_id + self._to_be_installed_package_path = package_path + + # Open actual dialog package = self.getPackageModel(package_id) plugin_name = package.displayName icon_url = package.iconUrl self._openLicenseDialog(plugin_name, license_content, icon_url) else: # Otherwise continue the installation - self._install(package_id, update) + self._install(package_id, package_path, update) - def _install(self, package_id: str, update: bool = False) -> None: - package_path = self._to_install.pop(package_id) + def _install(self, package_id: str, package_path: str, update: bool = False) -> None: Logger.debug(f"Installing {package_id}") to_be_installed = self._manager.installPackage(package_path) is not None package = self.getPackageModel(package_id) From 0cc9d8db6875b4c3bc3521fbf6ca2e8ad9e52c90 Mon Sep 17 00:00:00 2001 From: casper Date: Mon, 6 Dec 2021 15:51:19 +0100 Subject: [PATCH 083/195] Use existing plugin registry Fix after merge cura 8587 --- plugins/Marketplace/PackageList.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/Marketplace/PackageList.py b/plugins/Marketplace/PackageList.py index a9d4e698ed..d50e19d514 100644 --- a/plugins/Marketplace/PackageList.py +++ b/plugins/Marketplace/PackageList.py @@ -54,7 +54,7 @@ class PackageList(ListModel): self._license_model = LicenseModel() - plugin_path = PluginRegistry.getInstance().getPluginPath("Marketplace") + plugin_path = self._plugin_registry.getPluginPath("Marketplace") if plugin_path is None: plugin_path = os.path.dirname(__file__) From 3cae6d80b9c06dfe1ca04e8bd7d38655d01fc10b Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Mon, 6 Dec 2021 16:00:51 +0100 Subject: [PATCH 084/195] Show restart banner only when a restart is required Contributes to issue CURA-8587. --- plugins/Marketplace/resources/qml/Marketplace.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/Marketplace/resources/qml/Marketplace.qml b/plugins/Marketplace/resources/qml/Marketplace.qml index 62f9673072..bda153ee23 100644 --- a/plugins/Marketplace/resources/qml/Marketplace.qml +++ b/plugins/Marketplace/resources/qml/Marketplace.qml @@ -232,7 +232,7 @@ Window { height: quitButton.height + 2 * UM.Theme.getSize("default_margin").width color: UM.Theme.getColor("primary") - visible: false // TODO: enable this when restart is required + visible: CuraApplication.getPackageManager().hasPackagesToRemoveOrInstall anchors { left: parent.left From 3c5496441761f7475428b33d444a1c8050d8a980 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Mon, 6 Dec 2021 16:02:42 +0100 Subject: [PATCH 085/195] Use same content for button as old one So make it dynamic based on the application name. This also causes the translation to be re-used. Contributes to issue CURA-8587. --- plugins/Marketplace/resources/qml/Marketplace.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/Marketplace/resources/qml/Marketplace.qml b/plugins/Marketplace/resources/qml/Marketplace.qml index bda153ee23..e765f0f009 100644 --- a/plugins/Marketplace/resources/qml/Marketplace.qml +++ b/plugins/Marketplace/resources/qml/Marketplace.qml @@ -270,7 +270,7 @@ Window Cura.SecondaryButton { id: quitButton - text: catalog.i18nc("@button", "Quit Ultimaker Cura") + text: catalog.i18nc("@info:button, %1 is the application name", "Quit %1").arg(CuraApplication.applicationDisplayName) } } } From e646d53b539aacd2b0daea0a18fe15e1cb70c95a Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Mon, 6 Dec 2021 16:05:38 +0100 Subject: [PATCH 086/195] Make quit button quit Cura Contributes to issue CURA-8587. --- plugins/Marketplace/resources/qml/Marketplace.qml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/plugins/Marketplace/resources/qml/Marketplace.qml b/plugins/Marketplace/resources/qml/Marketplace.qml index e765f0f009..cde33faa97 100644 --- a/plugins/Marketplace/resources/qml/Marketplace.qml +++ b/plugins/Marketplace/resources/qml/Marketplace.qml @@ -271,6 +271,11 @@ Window { id: quitButton text: catalog.i18nc("@info:button, %1 is the application name", "Quit %1").arg(CuraApplication.applicationDisplayName) + onClicked: + { + marketplaceDialog.hide(); + CuraApplication.closeApplication(); + } } } } From e3eb82b022f19d6f13cba5ff8be0858fed2f43ef Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Mon, 6 Dec 2021 16:21:58 +0100 Subject: [PATCH 087/195] Align read more label to the text, not the parent box This is the same alignment as the ellipsis shown when the description is abbreviated. Looks correct, and it should be correct. Contributes to issue CURA-8587. --- plugins/Marketplace/resources/qml/PackageCard.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/Marketplace/resources/qml/PackageCard.qml b/plugins/Marketplace/resources/qml/PackageCard.qml index bf0dda217b..2112dc6a81 100644 --- a/plugins/Marketplace/resources/qml/PackageCard.qml +++ b/plugins/Marketplace/resources/qml/PackageCard.qml @@ -239,7 +239,7 @@ Rectangle { id: readMoreButton anchors.right: parent.right - anchors.bottom: parent.bottom + anchors.bottom: descriptionLabel.bottom height: fontMetrics.height //Height of a single line. text: catalog.i18nc("@info", "Read more") From c6501d6adeb1b155f35eca72626b8d2ed00a397d Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Mon, 6 Dec 2021 16:25:50 +0100 Subject: [PATCH 088/195] Use capital letters for busy text It seems though that the wrong text is shown here. In any case this looks more consistent. Contributes to issue CURA-8587. --- plugins/Marketplace/resources/qml/PackageCard.qml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/Marketplace/resources/qml/PackageCard.qml b/plugins/Marketplace/resources/qml/PackageCard.qml index 2112dc6a81..7bfee42457 100644 --- a/plugins/Marketplace/resources/qml/PackageCard.qml +++ b/plugins/Marketplace/resources/qml/PackageCard.qml @@ -324,9 +324,9 @@ Rectangle state: packageData.stateManageEnableButton Layout.alignment: Qt.AlignTop primaryText: catalog.i18nc("@button", "Enable") - busyPrimaryText: catalog.i18nc("@button", "enabling...") + busyPrimaryText: catalog.i18nc("@button", "Inabling...") secondaryText: catalog.i18nc("@button", "Disable") - busySecondaryText: catalog.i18nc("@button", "disabling...") + busySecondaryText: catalog.i18nc("@button", "Disabling...") enabled: !(installManageButton.busy || updateManageButton.busy) onClicked: { @@ -347,9 +347,9 @@ Rectangle state: packageData.stateManageInstallButton Layout.alignment: Qt.AlignTop primaryText: catalog.i18nc("@button", "Install") - busyPrimaryText: catalog.i18nc("@button", "installing...") + busyPrimaryText: catalog.i18nc("@button", "Installing...") secondaryText: catalog.i18nc("@button", "Uninstall") - busySecondaryText: catalog.i18nc("@button", "uninstalling...") + busySecondaryText: catalog.i18nc("@button", "Uninstalling...") enabled: !(enableManageButton.busy || updateManageButton.busy) onClicked: { From c73ef8439505c66a8630820517aa16db224cf9c8 Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Mon, 6 Dec 2021 16:47:53 +0100 Subject: [PATCH 089/195] Show the correct busy message Contributes to: CURA-8587 --- plugins/Marketplace/resources/qml/ManageButton.qml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/Marketplace/resources/qml/ManageButton.qml b/plugins/Marketplace/resources/qml/ManageButton.qml index 323fea03f1..5e1aad03e7 100644 --- a/plugins/Marketplace/resources/qml/ManageButton.qml +++ b/plugins/Marketplace/resources/qml/ManageButton.qml @@ -28,6 +28,7 @@ RowLayout onClicked: { + busyMessageText.text = manageButton.busyPrimaryText manageButton.clicked(true) } } @@ -40,6 +41,7 @@ RowLayout onClicked: { + busyMessageText.text = manageButton.busySecondaryText manageButton.clicked(false) } } @@ -76,7 +78,6 @@ RowLayout { id: busyMessageText visible: parent.visible - text: manageButton.state == "primary" ? manageButton.busyPrimaryText : manageButton.busySecondaryText anchors.left: busyIndicator.right anchors.verticalCenter: parent.verticalCenter From f932239b6a0ecdb8a227b02878e8ddc4ff4e4fbe Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Mon, 6 Dec 2021 16:52:38 +0100 Subject: [PATCH 090/195] Implement aesthetics of busy indicator Don't use the default spinner but use an image from our theme. A new image. And change the font size, colour and spacings a bit according to our designs. Contributes to issue CURA-8587. --- .../Marketplace/resources/qml/ManageButton.qml | 16 ++++++++++------ .../themes/cura-light/icons/default/Spinner.svg | 3 +++ 2 files changed, 13 insertions(+), 6 deletions(-) create mode 100644 resources/themes/cura-light/icons/default/Spinner.svg diff --git a/plugins/Marketplace/resources/qml/ManageButton.qml b/plugins/Marketplace/resources/qml/ManageButton.qml index 5e1aad03e7..39be04d386 100644 --- a/plugins/Marketplace/resources/qml/ManageButton.qml +++ b/plugins/Marketplace/resources/qml/ManageButton.qml @@ -53,21 +53,24 @@ RowLayout height: UM.Theme.getSize("action_button").height width: childrenRect.width - BusyIndicator + UM.RecolorImage { id: busyIndicator visible: parent.visible width: height anchors.left: parent.left anchors.top: parent.top + anchors.topMargin: UM.Theme.getSize("narrow_margin").height anchors.bottom: parent.bottom + anchors.bottomMargin: anchors.topMargin - palette.dark: UM.Theme.getColor("text") + source: UM.Theme.getIcon("Spinner") + color: UM.Theme.getColor("primary") RotationAnimator { - target: busyIndicator.contentItem - running: busyIndicator.visible && busyIndicator.running + target: busyIndicator + running: busyIndicator.visible from: 0 to: 360 loops: Animation.Infinite @@ -79,10 +82,11 @@ RowLayout id: busyMessageText visible: parent.visible anchors.left: busyIndicator.right + anchors.leftMargin: UM.Theme.getSize("narrow_margin").width anchors.verticalCenter: parent.verticalCenter - font: UM.Theme.getFont("default") - color: UM.Theme.getColor("text") + font: UM.Theme.getFont("medium_bold") + color: UM.Theme.getColor("primary") } } diff --git a/resources/themes/cura-light/icons/default/Spinner.svg b/resources/themes/cura-light/icons/default/Spinner.svg new file mode 100644 index 0000000000..22a8f4dfd9 --- /dev/null +++ b/resources/themes/cura-light/icons/default/Spinner.svg @@ -0,0 +1,3 @@ + + + From 4848c474e81bbe60a1e3a764c474d7a0efc821cb Mon Sep 17 00:00:00 2001 From: casper Date: Mon, 6 Dec 2021 17:10:13 +0100 Subject: [PATCH 091/195] Implement UX design for maketplace license dialog cura 8587 --- plugins/Marketplace/LicenseModel.py | 10 ----- plugins/Marketplace/PackageList.py | 6 +-- .../resources/qml/LicenseDialog.qml | 40 +++++-------------- .../cura-light/icons/high/Certificate.svg | 3 ++ resources/themes/cura-light/theme.json | 4 +- 5 files changed, 17 insertions(+), 46 deletions(-) create mode 100644 resources/themes/cura-light/icons/high/Certificate.svg diff --git a/plugins/Marketplace/LicenseModel.py b/plugins/Marketplace/LicenseModel.py index cb85b33430..199ddc9ee0 100644 --- a/plugins/Marketplace/LicenseModel.py +++ b/plugins/Marketplace/LicenseModel.py @@ -11,7 +11,6 @@ class LicenseModel(QObject): dialogTitleChanged = pyqtSignal() packageNameChanged = pyqtSignal() licenseTextChanged = pyqtSignal() - iconChanged = pyqtSignal() def __init__(self, decline_button_text: str = DEFAULT_DECLINE_BUTTON_TEXT) -> None: super().__init__() @@ -19,7 +18,6 @@ class LicenseModel(QObject): self._dialogTitle = "" self._license_text = "" self._package_name = "" - self._icon_url = "" self._decline_button_text = decline_button_text @pyqtProperty(str, constant = True) @@ -42,14 +40,6 @@ class LicenseModel(QObject): self._package_name = name self.packageNameChanged.emit() - @pyqtProperty(str, notify=iconChanged) - def iconUrl(self) -> str: - return self._icon_url - - def setIconUrl(self, url: str): - self._icon_url = url - self.iconChanged.emit() - @pyqtProperty(str, notify=licenseTextChanged) def licenseText(self) -> str: return self._license_text diff --git a/plugins/Marketplace/PackageList.py b/plugins/Marketplace/PackageList.py index d50e19d514..e41d28721c 100644 --- a/plugins/Marketplace/PackageList.py +++ b/plugins/Marketplace/PackageList.py @@ -136,9 +136,8 @@ class PackageList(ListModel): canInstallChanged = pyqtSignal(str, bool) - def _openLicenseDialog(self, plugin_name: str, license_content: str, icon_url: str) -> None: + def _openLicenseDialog(self, plugin_name: str, license_content: str) -> None: Logger.debug(f"Prompting license for {plugin_name}") - self._license_model.setIconUrl(icon_url) self._license_model.setPackageName(plugin_name) self._license_model.setLicenseText(license_content) self._license_dialog.show() @@ -180,8 +179,7 @@ class PackageList(ListModel): # Open actual dialog package = self.getPackageModel(package_id) plugin_name = package.displayName - icon_url = package.iconUrl - self._openLicenseDialog(plugin_name, license_content, icon_url) + self._openLicenseDialog(plugin_name, license_content) else: # Otherwise continue the installation self._install(package_id, package_path, update) diff --git a/plugins/Marketplace/resources/qml/LicenseDialog.qml b/plugins/Marketplace/resources/qml/LicenseDialog.qml index 9219f4ed32..0640379895 100644 --- a/plugins/Marketplace/resources/qml/LicenseDialog.qml +++ b/plugins/Marketplace/resources/qml/LicenseDialog.qml @@ -20,7 +20,6 @@ UM.Dialog width: minimumWidth height: minimumHeight backgroundColor: UM.Theme.getColor("main_background") - margin: screenScaleFactor * 10 ColumnLayout { @@ -29,49 +28,32 @@ UM.Dialog UM.I18nCatalog{id: catalog; name: "cura"} - Label - { - id: licenseHeader - Layout.fillWidth: true - text: catalog.i18nc("@label", "You need to accept the license to install the package") - color: UM.Theme.getColor("text") - wrapMode: Text.Wrap - renderType: Text.NativeRendering - } - Row { - id: packageRow - Layout.fillWidth: true height: childrenRect.height spacing: UM.Theme.getSize("default_margin").width leftPadding: UM.Theme.getSize("narrow_margin").width - Image + UM.RecolorImage { - id: icon - width: 30 * screenScaleFactor - height: width - sourceSize.width: width - sourceSize.height: height - fillMode: Image.PreserveAspectFit - source: licenseModel.iconUrl || "../../images/placeholder.svg" - mipmap: true + width: UM.Theme.getSize("marketplace_large_icon").width + height: UM.Theme.getSize("marketplace_large_icon").height + color: UM.Theme.getColor("text") + source: UM.Theme.getIcon("Certificate", "high") } Label { - id: packageName - text: licenseModel.packageName + text: catalog.i18nc("@text", "Please read and agree with the plugin licence.") color: UM.Theme.getColor("text") - font.bold: true + font: UM.Theme.getFont("large") anchors.verticalCenter: icon.verticalCenter - height: contentHeight + height: UM.Theme.getSize("marketplace_large_icon").height + verticalAlignment: Qt.AlignVCenter wrapMode: Text.Wrap renderType: Text.NativeRendering } - } Cura.ScrollableTextArea @@ -90,9 +72,6 @@ UM.Dialog [ Cura.PrimaryButton { - leftPadding: UM.Theme.getSize("dialog_primary_button_padding").width - rightPadding: UM.Theme.getSize("dialog_primary_button_padding").width - text: licenseModel.acceptButtonText onClicked: { handler.onLicenseAccepted() } } @@ -102,7 +81,6 @@ UM.Dialog [ Cura.SecondaryButton { - id: declineButton text: licenseModel.declineButtonText onClicked: { handler.onLicenseDeclined() } } diff --git a/resources/themes/cura-light/icons/high/Certificate.svg b/resources/themes/cura-light/icons/high/Certificate.svg new file mode 100644 index 0000000000..b588bddd8b --- /dev/null +++ b/resources/themes/cura-light/icons/high/Certificate.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/themes/cura-light/theme.json b/resources/themes/cura-light/theme.json index 149bfab308..8e9db0e9fe 100644 --- a/resources/themes/cura-light/theme.json +++ b/resources/themes/cura-light/theme.json @@ -686,6 +686,8 @@ "welcome_wizard_content_image_big": [18, 15], "welcome_wizard_cloud_content_image": [4, 4], - "banner_icon_size": [2.0, 2.0] + "banner_icon_size": [2.0, 2.0], + + "marketplace_large_icon": [4.0, 4.0] } } From e5257703321825d9ca488f7b60e18900b91fd869 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Mon, 6 Dec 2021 17:14:02 +0100 Subject: [PATCH 092/195] Cast result to list after _updateLocalPackages Because the _updateLocalPackages function guarantees it to be a list now, not None any more. Contributes to issue CURA-8587. --- cura/CuraPackageManager.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cura/CuraPackageManager.py b/cura/CuraPackageManager.py index 3ef24368fd..e3595ef4bb 100644 --- a/cura/CuraPackageManager.py +++ b/cura/CuraPackageManager.py @@ -1,7 +1,7 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from typing import Any, Dict, List, Tuple, TYPE_CHECKING, Optional, Generator +from typing import Any, cast, Dict, List, Tuple, TYPE_CHECKING, Optional, Generator from cura.CuraApplication import CuraApplication #To find some resource types. from cura.Settings.GlobalStack import GlobalStack @@ -30,7 +30,9 @@ class CuraPackageManager(PackageManager): """locally installed packages, lazy execution""" if self._local_packages is None: self._updateLocalPackages() - return self._local_packages + # _updateLocalPackages always results in a list of packages, not None. + # It's guaranteed to be a list now. + return cast(List[Dict[str, Any]], self._local_packages) def initialize(self) -> None: self._installation_dirs_dict["materials"] = Resources.getStoragePath(CuraApplication.ResourceTypes.MaterialInstanceContainer) From aec643fd44157e3101040beb513405280e2e1c77 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Mon, 6 Dec 2021 17:22:07 +0100 Subject: [PATCH 093/195] Fix several typing issues Contributes to issue CURA-8587. --- plugins/Marketplace/PackageList.py | 7 ++++--- plugins/Marketplace/PackageModel.py | 6 ++++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/plugins/Marketplace/PackageList.py b/plugins/Marketplace/PackageList.py index 6a931c1e4b..665451ba18 100644 --- a/plugins/Marketplace/PackageList.py +++ b/plugins/Marketplace/PackageList.py @@ -4,7 +4,7 @@ import tempfile import json from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, Qt -from typing import Dict, Optional, Set, TYPE_CHECKING +from typing import cast, Dict, Optional, Set, TYPE_CHECKING from UM.i18n import i18nCatalog from UM.Qt.ListModel import ListModel @@ -14,7 +14,7 @@ from UM.Logger import Logger from UM import PluginRegistry from cura.CuraApplication import CuraApplication -from cura import CuraPackageManager +from cura.CuraPackageManager import CuraPackageManager from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope # To make requests to the Ultimaker API with correct authorization. from .PackageModel import PackageModel @@ -22,6 +22,7 @@ from .Constants import USER_PACKAGES_URL if TYPE_CHECKING: from PyQt5.QtCore import QObject + from PyQt5.QtNetwork import QNetworkReply catalog = i18nCatalog("cura") @@ -35,7 +36,7 @@ class PackageList(ListModel): def __init__(self, parent: Optional["QObject"] = None) -> None: super().__init__(parent) - self._manager: CuraPackageManager = CuraApplication.getInstance().getPackageManager() + self._manager: CuraPackageManager = cast(CuraPackageManager, CuraApplication.getInstance().getPackageManager()) self._plugin_registry: PluginRegistry = CuraApplication.getInstance().getPluginRegistry() self._account = CuraApplication.getInstance().getCuraAPI().account self._error_message = "" diff --git a/plugins/Marketplace/PackageModel.py b/plugins/Marketplace/PackageModel.py index 92daf310a3..f7682340c9 100644 --- a/plugins/Marketplace/PackageModel.py +++ b/plugins/Marketplace/PackageModel.py @@ -71,11 +71,13 @@ class PackageModel(QObject): self.sdk_version = package_data.get("sdk_version_semver", "") # Note that there's a lot more info in the package_data than just these specified here. - def __eq__(self, other: Union[str, "PackageModel"]): + def __eq__(self, other: object): if isinstance(other, PackageModel): return other == self - else: + elif isinstance(other, str): return other == self._package_id + else: + return False def __repr__(self): return f"<{self._package_id} : {self._package_version} : {self._section_title}>" From a084b689b5de203758c60acad694c0c84ad157c2 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Mon, 6 Dec 2021 17:26:12 +0100 Subject: [PATCH 094/195] Add one remaining forgotten type import Contributes to issue CURA-8587. --- plugins/Marketplace/LocalPackageList.py | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/Marketplace/LocalPackageList.py b/plugins/Marketplace/LocalPackageList.py index a1f3f45e7e..196f3f19c6 100644 --- a/plugins/Marketplace/LocalPackageList.py +++ b/plugins/Marketplace/LocalPackageList.py @@ -8,6 +8,7 @@ from PyQt5.QtCore import pyqtSlot, QObject if TYPE_CHECKING: from PyQt5.QtCore import QObject + from PyQt5.QtNetwork import QNetworkReply from UM.i18n import i18nCatalog from UM.TaskManagement.HttpRequestManager import HttpRequestManager From 119b86b9579910e31d2ef7063aca86242e9df2a7 Mon Sep 17 00:00:00 2001 From: casper Date: Mon, 6 Dec 2021 17:32:10 +0100 Subject: [PATCH 095/195] Remove page-count logic from LicenceModel title dialog cura 8587 --- plugins/Marketplace/LicenseModel.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/plugins/Marketplace/LicenseModel.py b/plugins/Marketplace/LicenseModel.py index 199ddc9ee0..1bedb02cad 100644 --- a/plugins/Marketplace/LicenseModel.py +++ b/plugins/Marketplace/LicenseModel.py @@ -51,6 +51,4 @@ class LicenseModel(QObject): def _updateDialogTitle(self): self._dialogTitle = catalog.i18nc("@title:window", "Plugin License Agreement") - if self._page_count > 1: - self._dialogTitle = self._dialogTitle + " ({}/{})".format(self._current_page_idx + 1, self._page_count) self.dialogTitleChanged.emit() From 49a6a83fe93e864c53865319444dc388c277c66e Mon Sep 17 00:00:00 2001 From: casper Date: Mon, 6 Dec 2021 17:50:18 +0100 Subject: [PATCH 096/195] Move button text inside QML element cura 8587 --- plugins/Marketplace/LicenseModel.py | 23 +------------------ .../resources/qml/LicenseDialog.qml | 6 ++--- 2 files changed, 4 insertions(+), 25 deletions(-) diff --git a/plugins/Marketplace/LicenseModel.py b/plugins/Marketplace/LicenseModel.py index 1bedb02cad..64625185d7 100644 --- a/plugins/Marketplace/LicenseModel.py +++ b/plugins/Marketplace/LicenseModel.py @@ -5,32 +5,15 @@ catalog = i18nCatalog("cura") # Model for the LicenseDialog class LicenseModel(QObject): - DEFAULT_DECLINE_BUTTON_TEXT = catalog.i18nc("@button", "Decline") - ACCEPT_BUTTON_TEXT = catalog.i18nc("@button", "Agree") dialogTitleChanged = pyqtSignal() packageNameChanged = pyqtSignal() licenseTextChanged = pyqtSignal() - def __init__(self, decline_button_text: str = DEFAULT_DECLINE_BUTTON_TEXT) -> None: + def __init__(self, licence_text: str, package_name: str) -> None: super().__init__() - - self._dialogTitle = "" self._license_text = "" self._package_name = "" - self._decline_button_text = decline_button_text - - @pyqtProperty(str, constant = True) - def acceptButtonText(self): - return self.ACCEPT_BUTTON_TEXT - - @pyqtProperty(str, constant = True) - def declineButtonText(self): - return self._decline_button_text - - @pyqtProperty(str, notify=dialogTitleChanged) - def dialogTitle(self) -> str: - return self._dialogTitle @pyqtProperty(str, notify=packageNameChanged) def packageName(self) -> str: @@ -48,7 +31,3 @@ class LicenseModel(QObject): if self._license_text != license_text: self._license_text = license_text self.licenseTextChanged.emit() - - def _updateDialogTitle(self): - self._dialogTitle = catalog.i18nc("@title:window", "Plugin License Agreement") - self.dialogTitleChanged.emit() diff --git a/plugins/Marketplace/resources/qml/LicenseDialog.qml b/plugins/Marketplace/resources/qml/LicenseDialog.qml index 0640379895..6d863ecce5 100644 --- a/plugins/Marketplace/resources/qml/LicenseDialog.qml +++ b/plugins/Marketplace/resources/qml/LicenseDialog.qml @@ -14,7 +14,7 @@ import Cura 1.6 as Cura UM.Dialog { id: licenseDialog - title: licenseModel.dialogTitle + title: catalog.i18nc("@button", "Plugin license agreement") minimumWidth: UM.Theme.getSize("license_window_minimum").width minimumHeight: UM.Theme.getSize("license_window_minimum").height width: minimumWidth @@ -72,7 +72,7 @@ UM.Dialog [ Cura.PrimaryButton { - text: licenseModel.acceptButtonText + text: catalog.i18nc("@button", "Accept") onClicked: { handler.onLicenseAccepted() } } ] @@ -81,7 +81,7 @@ UM.Dialog [ Cura.SecondaryButton { - text: licenseModel.declineButtonText + text: catalog.i18nc("@button", "Decline") onClicked: { handler.onLicenseDeclined() } } ] From 28b21628b44748b0a8d52147e1b361a1f615a664 Mon Sep 17 00:00:00 2001 From: casper Date: Mon, 6 Dec 2021 18:07:44 +0100 Subject: [PATCH 097/195] Remove unused package_name property and add package_id in license model The `_to_be_installed_package_id` and `_to_be_installed_package_path` can now be removed from the class as they can requested from the model itself. This we make sure that the we alway install the package for which the license was recently accepted. cura 8587 --- plugins/Marketplace/LicenseModel.py | 20 +++++++++---------- plugins/Marketplace/PackageList.py | 30 +++++++++-------------------- 2 files changed, 18 insertions(+), 32 deletions(-) diff --git a/plugins/Marketplace/LicenseModel.py b/plugins/Marketplace/LicenseModel.py index 64625185d7..8eb439d4d6 100644 --- a/plugins/Marketplace/LicenseModel.py +++ b/plugins/Marketplace/LicenseModel.py @@ -5,23 +5,21 @@ catalog = i18nCatalog("cura") # Model for the LicenseDialog class LicenseModel(QObject): - - dialogTitleChanged = pyqtSignal() - packageNameChanged = pyqtSignal() + packageIdChanged = pyqtSignal() licenseTextChanged = pyqtSignal() - def __init__(self, licence_text: str, package_name: str) -> None: + def __init__(self) -> None: super().__init__() self._license_text = "" - self._package_name = "" + self._package_id = "" - @pyqtProperty(str, notify=packageNameChanged) - def packageName(self) -> str: - return self._package_name + @pyqtProperty(str, notify=packageIdChanged) + def packageId(self) -> str: + return self._package_id - def setPackageName(self, name: str) -> None: - self._package_name = name - self.packageNameChanged.emit() + def setPackageId(self, name: str) -> None: + self._package_id = name + self.packageIdChanged.emit() @pyqtProperty(str, notify=licenseTextChanged) def licenseText(self) -> str: diff --git a/plugins/Marketplace/PackageList.py b/plugins/Marketplace/PackageList.py index 9414bb9b28..39d1d9fab6 100644 --- a/plugins/Marketplace/PackageList.py +++ b/plugins/Marketplace/PackageList.py @@ -139,25 +139,20 @@ class PackageList(ListModel): def _openLicenseDialog(self, plugin_name: str, license_content: str) -> None: Logger.debug(f"Prompting license for {plugin_name}") - self._license_model.setPackageName(plugin_name) + self._license_model.setPackageId(plugin_name) self._license_model.setLicenseText(license_content) self._license_dialog.show() @pyqtSlot() def onLicenseAccepted(self) -> None: - package_id = self._to_be_installed_package_id - package_path = self._to_be_installed_package_path - del self._to_be_installed_package_id - del self._to_be_installed_package_path + package_id = self._license_model.packageId Logger.debug(f"Accepted license for {package_id}") self._license_dialog.close() - self._install(package_id, package_path) + self._install(package_id) @pyqtSlot() def onLicenseDeclined(self) -> None: - package_id = self._to_be_installed_package_id - del self._to_be_installed_package_id - del self._to_be_installed_package_path + package_id = self._license_model.packageId Logger.debug(f"Declined license for {package_id}") self._license_dialog.close() package = self.getPackageModel(package_id) @@ -166,27 +161,20 @@ class PackageList(ListModel): def _requestInstall(self, package_id: str, update: bool = False) -> None: Logger.debug(f"Request installing {package_id}") - package_path = self._to_install.pop(package_id) + package_path = self._to_install[package_id] license_content = self._manager.getPackageLicense(package_path) if not update and license_content is not None: # If installation is not and update, and the packages contains a license then # open dialog, prompting the using to accept the plugin license - - # Store some properties that are needed after the dialog is closed - self._to_be_installed_package_id = package_id - self._to_be_installed_package_path = package_path - - # Open actual dialog - package = self.getPackageModel(package_id) - plugin_name = package.displayName - self._openLicenseDialog(plugin_name, license_content) + self._openLicenseDialog(package_id, license_content) else: # Otherwise continue the installation - self._install(package_id, package_path, update) + self._install(package_id, update) - def _install(self, package_id: str, package_path: str, update: bool = False) -> None: + def _install(self, package_id: str, update: bool = False) -> None: Logger.debug(f"Installing {package_id}") + package_path = self._to_install.pop(package_id) to_be_installed = self._manager.installPackage(package_path) is not None package = self.getPackageModel(package_id) if package.can_update and to_be_installed: From 455f6f17915485ac5e778f3d3b03cb720a839fe6 Mon Sep 17 00:00:00 2001 From: casper Date: Mon, 6 Dec 2021 18:12:58 +0100 Subject: [PATCH 098/195] Update licencing comment cura 8587 --- plugins/Marketplace/resources/qml/LicenseDialog.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/Marketplace/resources/qml/LicenseDialog.qml b/plugins/Marketplace/resources/qml/LicenseDialog.qml index 6d863ecce5..7bee6f9108 100644 --- a/plugins/Marketplace/resources/qml/LicenseDialog.qml +++ b/plugins/Marketplace/resources/qml/LicenseDialog.qml @@ -1,5 +1,5 @@ -// Copyright (c) 2018 Ultimaker B.V. -// Toolbox is released under the terms of the LGPLv3 or higher. +//Copyright (c) 2021 Ultimaker B.V. +//Cura is released under the terms of the LGPLv3 or higher. import QtQuick 2.10 import QtQuick.Dialogs 1.1 From 71a43060a60d697c4cad2394f919492560de9a48 Mon Sep 17 00:00:00 2001 From: casper Date: Mon, 6 Dec 2021 19:34:02 +0100 Subject: [PATCH 099/195] Open separate license dialog with each plugin install Previously the license dialog was instanciated once and re-used for each install. As the dialog is only shown after the plugin is downloaded it was possible to click install for multiple plugins. Plugins that finish downloading later would override the dialog of earlier downloaded plugins. Clicking "accept" would then only install the latest downloaded plugin. Now for each install a separate dialog is shown. Accepting the license agreement would only install the recently accepted plugin. Note: in the current form the license agreement does not show any identification to what plugin triggered the dialog. As multiple dialogs can be shown at once this might be unclear. cura 8587 --- plugins/Marketplace/LicenseModel.py | 31 ---------- plugins/Marketplace/PackageList.py | 59 ++++++++++--------- .../resources/qml/LicenseDialog.qml | 6 +- 3 files changed, 35 insertions(+), 61 deletions(-) delete mode 100644 plugins/Marketplace/LicenseModel.py diff --git a/plugins/Marketplace/LicenseModel.py b/plugins/Marketplace/LicenseModel.py deleted file mode 100644 index 8eb439d4d6..0000000000 --- a/plugins/Marketplace/LicenseModel.py +++ /dev/null @@ -1,31 +0,0 @@ -from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal -from UM.i18n import i18nCatalog - -catalog = i18nCatalog("cura") - -# Model for the LicenseDialog -class LicenseModel(QObject): - packageIdChanged = pyqtSignal() - licenseTextChanged = pyqtSignal() - - def __init__(self) -> None: - super().__init__() - self._license_text = "" - self._package_id = "" - - @pyqtProperty(str, notify=packageIdChanged) - def packageId(self) -> str: - return self._package_id - - def setPackageId(self, name: str) -> None: - self._package_id = name - self.packageIdChanged.emit() - - @pyqtProperty(str, notify=licenseTextChanged) - def licenseText(self) -> str: - return self._license_text - - def setLicenseText(self, license_text: str) -> None: - if self._license_text != license_text: - self._license_text = license_text - self.licenseTextChanged.emit() diff --git a/plugins/Marketplace/PackageList.py b/plugins/Marketplace/PackageList.py index 39d1d9fab6..f0a6d1de06 100644 --- a/plugins/Marketplace/PackageList.py +++ b/plugins/Marketplace/PackageList.py @@ -20,7 +20,6 @@ from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope # To ma from .PackageModel import PackageModel from .Constants import USER_PACKAGES_URL -from .LicenseModel import LicenseModel if TYPE_CHECKING: from PyQt5.QtCore import QObject @@ -52,19 +51,7 @@ class PackageList(ListModel): self._ongoing_request: Optional[HttpRequestData] = None self._scope = JsonDecoratorScope(UltimakerCloudScope(CuraApplication.getInstance())) - - self._license_model = LicenseModel() - - plugin_path = self._plugin_registry.getPluginPath("Marketplace") - if plugin_path is None: - plugin_path = os.path.dirname(__file__) - - # create a QML component for the license dialog - license_dialog_component_path = os.path.join(plugin_path, "resources", "qml", "LicenseDialog.qml") - self._license_dialog = CuraApplication.getInstance().createQmlComponent(license_dialog_component_path, { - "licenseModel": self._license_model, - "handler": self - }) + self._license_dialogs: Dict[str, QObject] = {} @pyqtSlot() def updatePackages(self) -> None: @@ -137,24 +124,42 @@ class PackageList(ListModel): canInstallChanged = pyqtSignal(str, bool) - def _openLicenseDialog(self, plugin_name: str, license_content: str) -> None: - Logger.debug(f"Prompting license for {plugin_name}") - self._license_model.setPackageId(plugin_name) - self._license_model.setLicenseText(license_content) - self._license_dialog.show() + def _openLicenseDialog(self, package_id: str, license_content: str) -> None: + Logger.debug(f"Prompting license for {package_id}") - @pyqtSlot() - def onLicenseAccepted(self) -> None: - package_id = self._license_model.packageId + plugin_path = self._plugin_registry.getPluginPath("Marketplace") + if plugin_path is None: + plugin_path = os.path.dirname(__file__) + + # create a QML component for the license dialog + license_dialog_component_path = os.path.join(plugin_path, "resources", "qml", "LicenseDialog.qml") + dialog = CuraApplication.getInstance().createQmlComponent(license_dialog_component_path, { + "licenseContent": license_content, + "packageId": package_id, + "handler": self + }) + dialog.show() + # place dialog in class such that it does not get remove by garbage collector + self._license_dialogs[package_id] = dialog + + @pyqtSlot(str) + def onLicenseAccepted(self, package_id: str) -> None: Logger.debug(f"Accepted license for {package_id}") - self._license_dialog.close() + # close dialog + dialog = self._license_dialogs.pop(package_id) + if dialog is not None: + dialog.deleteLater() + # install relevant package self._install(package_id) - @pyqtSlot() - def onLicenseDeclined(self) -> None: - package_id = self._license_model.packageId + @pyqtSlot(str) + def onLicenseDeclined(self, package_id: str) -> None: Logger.debug(f"Declined license for {package_id}") - self._license_dialog.close() + # close dialog + dialog = self._license_dialogs.pop(package_id) + if dialog is not None: + dialog.deleteLater() + # reset package card package = self.getPackageModel(package_id) package.is_installing = False diff --git a/plugins/Marketplace/resources/qml/LicenseDialog.qml b/plugins/Marketplace/resources/qml/LicenseDialog.qml index 7bee6f9108..32c4ec699c 100644 --- a/plugins/Marketplace/resources/qml/LicenseDialog.qml +++ b/plugins/Marketplace/resources/qml/LicenseDialog.qml @@ -63,7 +63,7 @@ UM.Dialog Layout.fillHeight: true anchors.topMargin: UM.Theme.getSize("default_margin").height - textArea.text: licenseModel.licenseText + textArea.text: licenseContent textArea.readOnly: true } @@ -73,7 +73,7 @@ UM.Dialog Cura.PrimaryButton { text: catalog.i18nc("@button", "Accept") - onClicked: { handler.onLicenseAccepted() } + onClicked: { handler.onLicenseAccepted(packageId) } } ] @@ -82,7 +82,7 @@ UM.Dialog Cura.SecondaryButton { text: catalog.i18nc("@button", "Decline") - onClicked: { handler.onLicenseDeclined() } + onClicked: { handler.onLicenseDeclined(packageId) } } ] } From dc03a7fdcb99ea8df3ca1a3404a12a78a0499587 Mon Sep 17 00:00:00 2001 From: casper Date: Tue, 7 Dec 2021 08:18:26 +0100 Subject: [PATCH 100/195] Correctly handle accept and reject hotkeys in marketplace license dialog Accept license on "enter" Reject license on "escape" or dialog close cura 8587 --- plugins/Marketplace/resources/qml/LicenseDialog.qml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plugins/Marketplace/resources/qml/LicenseDialog.qml b/plugins/Marketplace/resources/qml/LicenseDialog.qml index 32c4ec699c..d37fe38bd9 100644 --- a/plugins/Marketplace/resources/qml/LicenseDialog.qml +++ b/plugins/Marketplace/resources/qml/LicenseDialog.qml @@ -85,4 +85,8 @@ UM.Dialog onClicked: { handler.onLicenseDeclined(packageId) } } ] + + onAccepted: { handler.onLicenseAccepted(packageId) } + onRejected: { handler.onLicenseDeclined(packageId) } + onClosing: { handler.onLicenseDeclined(packageId) } } From 0fefe85fca7518d4fa22992511d884d02e40ac28 Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Tue, 7 Dec 2021 08:57:49 +0100 Subject: [PATCH 101/195] Present restart banner if plugin has been en-/disabled Contributes to: CURA-8587 --- plugins/Marketplace/Marketplace.py | 15 +++++++++------ plugins/Marketplace/resources/qml/Marketplace.qml | 2 +- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/plugins/Marketplace/Marketplace.py b/plugins/Marketplace/Marketplace.py index 89ad986920..228ee1e506 100644 --- a/plugins/Marketplace/Marketplace.py +++ b/plugins/Marketplace/Marketplace.py @@ -2,7 +2,7 @@ # Cura is released under the terms of the LGPLv3 or higher. import os.path -from PyQt5.QtCore import pyqtSlot +from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject from PyQt5.QtQml import qmlRegisterType from typing import Optional, TYPE_CHECKING @@ -18,14 +18,16 @@ if TYPE_CHECKING: from PyQt5.QtCore import QObject -class Marketplace(Extension): +class Marketplace(Extension, QObject): """ The main managing object for the Marketplace plug-in. """ - def __init__(self) -> None: - super().__init__() - self._window: Optional["QObject"] = None # If the window has been loaded yet, it'll be cached in here. + def __init__(self, parent: Optional[QObject] = None) -> None: + QObject.__init__(self, parent = parent) + Extension.__init__(self) + self._window: Optional[QObject] = None # If the window has been loaded yet, it'll be cached in here. + self.plugin_registry: Optional[PluginRegistry] = None qmlRegisterType(RemotePackageList, "Marketplace", 1, 0, "RemotePackageList") qmlRegisterType(LocalPackageList, "Marketplace", 1, 0, "LocalPackageList") @@ -38,11 +40,12 @@ class Marketplace(Extension): If the window hadn't been loaded yet into Qt, it will be created lazily. """ if self._window is None: + self.plugin_registry = PluginRegistry.getInstance() plugin_path = PluginRegistry.getInstance().getPluginPath(self.getPluginId()) if plugin_path is None: plugin_path = os.path.dirname(__file__) path = os.path.join(plugin_path, "resources", "qml", "Marketplace.qml") - self._window = CuraApplication.getInstance().createQmlComponent(path, {}) + self._window = CuraApplication.getInstance().createQmlComponent(path, {"plugin_registry": self.plugin_registry}) if self._window is None: # Still None? Failed to load the QML then. return self._window.show() diff --git a/plugins/Marketplace/resources/qml/Marketplace.qml b/plugins/Marketplace/resources/qml/Marketplace.qml index cde33faa97..5c56b0c41d 100644 --- a/plugins/Marketplace/resources/qml/Marketplace.qml +++ b/plugins/Marketplace/resources/qml/Marketplace.qml @@ -232,7 +232,7 @@ Window { height: quitButton.height + 2 * UM.Theme.getSize("default_margin").width color: UM.Theme.getColor("primary") - visible: CuraApplication.getPackageManager().hasPackagesToRemoveOrInstall + visible: CuraApplication.getPackageManager().hasPackagesToRemoveOrInstall || plugin_registry.hasPluginsEnabledOrDisabled anchors { left: parent.left From f6966c25fb28214cedaf9ff84a22624fb013c11b Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Tue, 7 Dec 2021 09:48:48 +0100 Subject: [PATCH 102/195] Some final tweaks and added missing documentation Contributes to: CURA-8587 --- cura/CuraPackageManager.py | 8 ++--- plugins/Marketplace/Constants.py | 1 + plugins/Marketplace/Marketplace.py | 2 +- plugins/Marketplace/PackageList.py | 35 +++++++++++++++++++ plugins/Marketplace/PackageModel.py | 19 ++++++---- .../resources/qml/LicenseDialog.qml | 22 ++++++------ .../Toolbox/src/CloudSync/CloudApiClient.py | 2 +- 7 files changed, 65 insertions(+), 24 deletions(-) diff --git a/cura/CuraPackageManager.py b/cura/CuraPackageManager.py index e3595ef4bb..bca6494f37 100644 --- a/cura/CuraPackageManager.py +++ b/cura/CuraPackageManager.py @@ -1,13 +1,13 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from typing import Any, cast, Dict, List, Tuple, TYPE_CHECKING, Optional, Generator +from typing import Any, cast, Dict, List, Tuple, TYPE_CHECKING, Optional -from cura.CuraApplication import CuraApplication #To find some resource types. +from cura.CuraApplication import CuraApplication # To find some resource types. from cura.Settings.GlobalStack import GlobalStack -from UM.PackageManager import PackageManager #The class we're extending. -from UM.Resources import Resources #To find storage paths for some resource types. +from UM.PackageManager import PackageManager # The class we're extending. +from UM.Resources import Resources # To find storage paths for some resource types. from UM.i18n import i18nCatalog catalog = i18nCatalog("cura") diff --git a/plugins/Marketplace/Constants.py b/plugins/Marketplace/Constants.py index bc6d1f05fa..9f0f78b966 100644 --- a/plugins/Marketplace/Constants.py +++ b/plugins/Marketplace/Constants.py @@ -1,5 +1,6 @@ # Copyright (c) 2021 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. + from cura.UltimakerCloud import UltimakerCloudConstants from cura.ApplicationMetadata import CuraSDKVersion diff --git a/plugins/Marketplace/Marketplace.py b/plugins/Marketplace/Marketplace.py index 228ee1e506..d55e538f6c 100644 --- a/plugins/Marketplace/Marketplace.py +++ b/plugins/Marketplace/Marketplace.py @@ -2,7 +2,7 @@ # Cura is released under the terms of the LGPLv3 or higher. import os.path -from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject +from PyQt5.QtCore import pyqtSlot, QObject from PyQt5.QtQml import qmlRegisterType from typing import Optional, TYPE_CHECKING diff --git a/plugins/Marketplace/PackageList.py b/plugins/Marketplace/PackageList.py index f0a6d1de06..346b7a2b67 100644 --- a/plugins/Marketplace/PackageList.py +++ b/plugins/Marketplace/PackageList.py @@ -192,6 +192,12 @@ class PackageList(ListModel): self.subscribeUserToPackage(package_id, str(package.sdk_version)) def download(self, package_id: str, url: str, update: bool = False) -> None: + """Initiate the download request + + :param package_id: the package identification string + :param url: the URL from which the package needs to be obtained + :param update: A flag if this is download request is an update process + """ def downloadFinished(reply: "QNetworkReply") -> None: self._downloadFinished(package_id, reply, update) @@ -232,6 +238,11 @@ class PackageList(ListModel): package.is_installing = False def subscribeUserToPackage(self, package_id: str, sdk_version: str) -> None: + """Subscribe the user (if logged in) to the package for a given SDK + + :param package_id: the package identification string + :param sdk_version: the SDK version + """ if self._account.isLoggedIn: Logger.debug(f"Subscribing the user for package: {package_id}") HttpRequestManager.getInstance().put( @@ -241,6 +252,10 @@ class PackageList(ListModel): ) def unsunscribeUserFromPackage(self, package_id: str) -> None: + """Unsubscribe the user (if logged in) from the package + + :param package_id: the package identification string + """ if self._account.isLoggedIn: Logger.debug(f"Unsubscribing the user for package: {package_id}") HttpRequestManager.getInstance().delete(url = f"{USER_PACKAGES_URL}/{package_id}", scope = self._scope) @@ -256,6 +271,10 @@ class PackageList(ListModel): @pyqtSlot(str) def installPackage(self, package_id: str) -> None: + """Install a package from the Marketplace + + :param package_id: the package identification string + """ package = self.getPackageModel(package_id) package.is_installing = True url = package.download_url @@ -264,6 +283,10 @@ class PackageList(ListModel): @pyqtSlot(str) def uninstallPackage(self, package_id: str) -> None: + """Uninstall a package from the Marketplace + + :param package_id: the package identification string + """ Logger.debug(f"Uninstalling {package_id}") package = self.getPackageModel(package_id) package.is_installing = True @@ -274,6 +297,10 @@ class PackageList(ListModel): @pyqtSlot(str) def updatePackage(self, package_id: str) -> None: + """Update a package from the Marketplace + + :param package_id: the package identification string + """ package = self.getPackageModel(package_id) package.is_updating = True self._manager.removePackage(package_id, force_add = True) @@ -283,6 +310,10 @@ class PackageList(ListModel): @pyqtSlot(str) def enablePackage(self, package_id: str) -> None: + """Enable a package in the plugin registry + + :param package_id: the package identification string + """ package = self.getPackageModel(package_id) package.is_enabling = True Logger.debug(f"Enabling {package_id}") @@ -292,6 +323,10 @@ class PackageList(ListModel): @pyqtSlot(str) def disablePackage(self, package_id: str) -> None: + """Disable a package in the plugin registry + + :param package_id: the package identification string + """ package = self.getPackageModel(package_id) package.is_enabling = True Logger.debug(f"Disabling {package_id}") diff --git a/plugins/Marketplace/PackageModel.py b/plugins/Marketplace/PackageModel.py index f7682340c9..2231f6adbd 100644 --- a/plugins/Marketplace/PackageModel.py +++ b/plugins/Marketplace/PackageModel.py @@ -3,10 +3,9 @@ from PyQt5.QtCore import pyqtProperty, QObject, pyqtSignal import re -from typing import Any, Dict, List, Optional, Union +from typing import Any, Dict, List, Optional from cura.Settings.CuraContainerRegistry import CuraContainerRegistry # To get names of materials we're compatible with. -from UM.Logger import Logger from UM.i18n import i18nCatalog # To translate placeholder names if data is not present. catalog = i18nCatalog("cura") @@ -285,13 +284,10 @@ class PackageModel(QObject): @pyqtProperty(str, notify = stateManageButtonChanged) def stateManageEnableButton(self) -> str: + """The state of the manage Enable Button of this package""" if self._is_enabling: return "busy" - if self._is_recently_managed: - return "hidden" - if self._package_type == "material": - return "hidden" - if not self._is_installed: + if self._is_recently_managed or self._package_type == "material" or not self._is_installed: return "hidden" if self._is_installed and self._is_active: return "secondary" @@ -299,6 +295,7 @@ class PackageModel(QObject): @property def is_enabling(self) -> bool: + """Flag if the package is being enabled/disabled""" return self._is_enabling @is_enabling.setter @@ -309,6 +306,7 @@ class PackageModel(QObject): @property def is_active(self) -> bool: + """Flag if the package is currently active""" return self._is_active @is_active.setter @@ -321,6 +319,7 @@ class PackageModel(QObject): @pyqtProperty(str, notify = stateManageButtonChanged) def stateManageInstallButton(self) -> str: + """The state of the Manage Install package card""" if self._is_installing: return "busy" if self._is_recently_managed: @@ -335,6 +334,7 @@ class PackageModel(QObject): @property def is_recently_managed(self) -> bool: + """Flag if the package has been recently managed by the user, either un-/installed updated etc""" return self._is_recently_managed @is_recently_managed.setter @@ -345,6 +345,7 @@ class PackageModel(QObject): @property def is_installing(self) -> bool: + """Flag is we're currently installing""" return self._is_installing @is_installing.setter @@ -355,6 +356,7 @@ class PackageModel(QObject): @property def can_downgrade(self) -> bool: + """Flag if the installed package can be downgraded to a bundled version""" return self._can_downgrade @can_downgrade.setter @@ -367,6 +369,7 @@ class PackageModel(QObject): @pyqtProperty(str, notify = stateManageButtonChanged) def stateManageUpdateButton(self) -> str: + """The state of the manage Update button for this card """ if self._is_updating: return "busy" if self._can_update: @@ -375,6 +378,7 @@ class PackageModel(QObject): @property def is_updating(self) -> bool: + """Flag indicating if the package is being updated""" return self._is_updating @is_updating.setter @@ -385,6 +389,7 @@ class PackageModel(QObject): @property def can_update(self) -> bool: + """Flag indicating if the package can be updated""" return self._can_update @can_update.setter diff --git a/plugins/Marketplace/resources/qml/LicenseDialog.qml b/plugins/Marketplace/resources/qml/LicenseDialog.qml index d37fe38bd9..d44ef335eb 100644 --- a/plugins/Marketplace/resources/qml/LicenseDialog.qml +++ b/plugins/Marketplace/resources/qml/LicenseDialog.qml @@ -8,7 +8,7 @@ import QtQuick.Controls 2.3 import QtQuick.Layouts 1.3 import QtQuick.Controls.Styles 1.4 -import UM 1.1 as UM +import UM 1.6 as UM import Cura 1.6 as Cura UM.Dialog @@ -21,14 +21,15 @@ UM.Dialog height: minimumHeight backgroundColor: UM.Theme.getColor("main_background") + property variant catalog: UM.I18nCatalog { name: "cura" } + ColumnLayout { anchors.fill: parent spacing: UM.Theme.getSize("thick_margin").height - UM.I18nCatalog{id: catalog; name: "cura"} - - Row { + Row + { Layout.fillWidth: true height: childrenRect.height spacing: UM.Theme.getSize("default_margin").width @@ -36,6 +37,7 @@ UM.Dialog UM.RecolorImage { + id: icon width: UM.Theme.getSize("marketplace_large_icon").width height: UM.Theme.getSize("marketplace_large_icon").height color: UM.Theme.getColor("text") @@ -53,12 +55,10 @@ UM.Dialog wrapMode: Text.Wrap renderType: Text.NativeRendering } - } Cura.ScrollableTextArea { - Layout.fillWidth: true Layout.fillHeight: true anchors.topMargin: UM.Theme.getSize("default_margin").height @@ -73,7 +73,7 @@ UM.Dialog Cura.PrimaryButton { text: catalog.i18nc("@button", "Accept") - onClicked: { handler.onLicenseAccepted(packageId) } + onClicked: handler.onLicenseAccepted(packageId) } ] @@ -82,11 +82,11 @@ UM.Dialog Cura.SecondaryButton { text: catalog.i18nc("@button", "Decline") - onClicked: { handler.onLicenseDeclined(packageId) } + onClicked: handler.onLicenseDeclined(packageId) } ] - onAccepted: { handler.onLicenseAccepted(packageId) } - onRejected: { handler.onLicenseDeclined(packageId) } - onClosing: { handler.onLicenseDeclined(packageId) } + onAccepted: handler.onLicenseAccepted(packageId) + onRejected: handler.onLicenseDeclined(packageId) + onClosing: handler.onLicenseDeclined(packageId) } diff --git a/plugins/Toolbox/src/CloudSync/CloudApiClient.py b/plugins/Toolbox/src/CloudSync/CloudApiClient.py index 21eb1bdbd2..9543ec012e 100644 --- a/plugins/Toolbox/src/CloudSync/CloudApiClient.py +++ b/plugins/Toolbox/src/CloudSync/CloudApiClient.py @@ -38,7 +38,7 @@ class CloudApiClient: def _subscribe(self, package_id: str) -> None: """You probably don't want to use this directly. All installed packages will be automatically subscribed.""" - Logger.debug("Subscribing to {}", package_id) + Logger.debug("Subscribing to using the Old Toolbox {}", package_id) data = "{\"data\": {\"package_id\": \"%s\", \"sdk_version\": \"%s\"}}" % (package_id, CloudApiModel.sdk_version) HttpRequestManager.getInstance().put( url = CloudApiModel.api_url_user_packages, From 1c0e484069cfef1e6cd063684724d978506b95c6 Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Tue, 7 Dec 2021 11:25:16 +0100 Subject: [PATCH 103/195] Only show manage buttons in manage tab or detail view Contributes to: CURA-8587 --- plugins/Marketplace/resources/qml/ManagedPackages.qml | 1 + plugins/Marketplace/resources/qml/Materials.qml | 1 + plugins/Marketplace/resources/qml/PackageCard.qml | 7 +++++++ plugins/Marketplace/resources/qml/Packages.qml | 2 ++ plugins/Marketplace/resources/qml/Plugins.qml | 1 + 5 files changed, 12 insertions(+) diff --git a/plugins/Marketplace/resources/qml/ManagedPackages.qml b/plugins/Marketplace/resources/qml/ManagedPackages.qml index f44fbd0a9b..dbdc04bf52 100644 --- a/plugins/Marketplace/resources/qml/ManagedPackages.qml +++ b/plugins/Marketplace/resources/qml/ManagedPackages.qml @@ -20,6 +20,7 @@ Packages bannerVisible = false; } searchInBrowserUrl: "https://marketplace.ultimaker.com/app/cura/plugins?utm_source=cura&utm_medium=software&utm_campaign=marketplace-search-plugins-browser" + packagesManageableInListView: true model: Marketplace.LocalPackageList { diff --git a/plugins/Marketplace/resources/qml/Materials.qml b/plugins/Marketplace/resources/qml/Materials.qml index 39d283b0a5..d19f3a4b04 100644 --- a/plugins/Marketplace/resources/qml/Materials.qml +++ b/plugins/Marketplace/resources/qml/Materials.qml @@ -17,6 +17,7 @@ Packages bannerVisible = false; } searchInBrowserUrl: "https://marketplace.ultimaker.com/app/cura/materials?utm_source=cura&utm_medium=software&utm_campaign=marketplace-search-materials-browser" + packagesManageableInListView: false model: Marketplace.RemotePackageList { diff --git a/plugins/Marketplace/resources/qml/PackageCard.qml b/plugins/Marketplace/resources/qml/PackageCard.qml index 7bfee42457..326cf0583f 100644 --- a/plugins/Marketplace/resources/qml/PackageCard.qml +++ b/plugins/Marketplace/resources/qml/PackageCard.qml @@ -10,8 +10,10 @@ import Cura 1.6 as Cura Rectangle { + id: root property var packageData property bool expanded: false + property bool manageableInListView height: childrenRect.height color: UM.Theme.getColor("main_background") @@ -328,6 +330,7 @@ Rectangle secondaryText: catalog.i18nc("@button", "Disable") busySecondaryText: catalog.i18nc("@button", "Disabling...") enabled: !(installManageButton.busy || updateManageButton.busy) + visible: root.manageableInListView || root.expanded onClicked: { if (primary_action) @@ -351,6 +354,8 @@ Rectangle secondaryText: catalog.i18nc("@button", "Uninstall") busySecondaryText: catalog.i18nc("@button", "Uninstalling...") enabled: !(enableManageButton.busy || updateManageButton.busy) + visible: root.manageableInListView || root.expanded + onClicked: { if (primary_action) @@ -372,6 +377,8 @@ Rectangle primaryText: catalog.i18nc("@button", "Update") busyPrimaryText: catalog.i18nc("@button", "updating...") enabled: !(installManageButton.busy || enableManageButton.busy) + visible: root.manageableInListView || root.expanded + onClicked: packageData.updatePackageTriggered(packageData.packageId) } } diff --git a/plugins/Marketplace/resources/qml/Packages.qml b/plugins/Marketplace/resources/qml/Packages.qml index c240141a71..62c0f92149 100644 --- a/plugins/Marketplace/resources/qml/Packages.qml +++ b/plugins/Marketplace/resources/qml/Packages.qml @@ -19,6 +19,7 @@ ListView property string bannerText property string bannerReadMoreUrl property var onRemoveBanner + property bool packagesManageableInListView clip: true @@ -80,6 +81,7 @@ ListView PackageCard { + manageableInListView: packages.packagesManageableInListView packageData: model.package width: parent.width - UM.Theme.getSize("default_margin").width - UM.Theme.getSize("narrow_margin").width color: cardMouseArea.containsMouse ? UM.Theme.getColor("action_button_hovered") : UM.Theme.getColor("main_background") diff --git a/plugins/Marketplace/resources/qml/Plugins.qml b/plugins/Marketplace/resources/qml/Plugins.qml index 538afc827a..3cfa92d134 100644 --- a/plugins/Marketplace/resources/qml/Plugins.qml +++ b/plugins/Marketplace/resources/qml/Plugins.qml @@ -17,6 +17,7 @@ Packages bannerVisible = false; } searchInBrowserUrl: "https://marketplace.ultimaker.com/app/cura/plugins?utm_source=cura&utm_medium=software&utm_campaign=marketplace-search-plugins-browser" + packagesManageableInListView: false model: Marketplace.RemotePackageList { From ca76bcc29cc573b777c72ace624e384f78eff3d6 Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Tue, 7 Dec 2021 12:23:04 +0100 Subject: [PATCH 104/195] Show a conformation message after a successful manage action Contributes to: CURA-8587 --- plugins/Marketplace/PackageList.py | 2 - plugins/Marketplace/PackageModel.py | 32 +++++---- .../resources/qml/ManageButton.qml | 68 ++++++++++++++++++- .../Marketplace/resources/qml/PackageCard.qml | 11 ++- 4 files changed, 91 insertions(+), 22 deletions(-) diff --git a/plugins/Marketplace/PackageList.py b/plugins/Marketplace/PackageList.py index 346b7a2b67..47b5e8ff4b 100644 --- a/plugins/Marketplace/PackageList.py +++ b/plugins/Marketplace/PackageList.py @@ -187,7 +187,6 @@ class PackageList(ListModel): if update: package.is_updating = False else: - package.is_recently_managed = True package.is_installing = False self.subscribeUserToPackage(package_id, str(package.sdk_version)) @@ -293,7 +292,6 @@ class PackageList(ListModel): self._manager.removePackage(package_id) self.unsunscribeUserFromPackage(package_id) package.is_installing = False - package.is_recently_managed = True @pyqtSlot(str) def updatePackage(self, package_id: str) -> None: diff --git a/plugins/Marketplace/PackageModel.py b/plugins/Marketplace/PackageModel.py index 2231f6adbd..6b313eb9a5 100644 --- a/plugins/Marketplace/PackageModel.py +++ b/plugins/Marketplace/PackageModel.py @@ -61,7 +61,10 @@ class PackageModel(QObject): self._icon_url = author_data.get("icon_url", "") self._is_installing = False - self._is_recently_managed = False + self._is_recently_installed = False + self._is_recently_updated = False + self._is_recently_enabled = False + self._can_update = False self._is_updating = False self._is_enabling = False @@ -287,7 +290,9 @@ class PackageModel(QObject): """The state of the manage Enable Button of this package""" if self._is_enabling: return "busy" - if self._is_recently_managed or self._package_type == "material" or not self._is_installed: + if self._is_recently_enabled: + return "confirmed" + if self._package_type == "material" or not self._is_installed: return "hidden" if self._is_installed and self._is_active: return "secondary" @@ -301,6 +306,8 @@ class PackageModel(QObject): @is_enabling.setter def is_enabling(self, value: bool) -> None: if value != self._is_enabling: + if not value: + self._is_recently_enabled = True self._is_enabling = value self.stateManageButtonChanged.emit() @@ -322,8 +329,8 @@ class PackageModel(QObject): """The state of the Manage Install package card""" if self._is_installing: return "busy" - if self._is_recently_managed: - return "hidden" + if self._is_recently_installed: + return "confirmed" if self._is_installed: if self._is_bundled and not self._can_downgrade: return "hidden" @@ -332,17 +339,6 @@ class PackageModel(QObject): else: return "primary" - @property - def is_recently_managed(self) -> bool: - """Flag if the package has been recently managed by the user, either un-/installed updated etc""" - return self._is_recently_managed - - @is_recently_managed.setter - def is_recently_managed(self, value: bool) -> None: - if value != self._is_recently_managed: - self._is_recently_managed = value - self.stateManageButtonChanged.emit() - @property def is_installing(self) -> bool: """Flag is we're currently installing""" @@ -351,6 +347,8 @@ class PackageModel(QObject): @is_installing.setter def is_installing(self, value: bool) -> None: if value != self._is_installing: + if not value: + self._is_recently_installed = True self._is_installing = value self.stateManageButtonChanged.emit() @@ -372,6 +370,8 @@ class PackageModel(QObject): """The state of the manage Update button for this card """ if self._is_updating: return "busy" + if self._is_recently_updated: + return "confirmed" if self._can_update: return "primary" return "hidden" @@ -384,6 +384,8 @@ class PackageModel(QObject): @is_updating.setter def is_updating(self, value: bool) -> None: if value != self._is_updating: + if not value: + self._is_recently_updated = True self._is_updating = value self.stateManageButtonChanged.emit() diff --git a/plugins/Marketplace/resources/qml/ManageButton.qml b/plugins/Marketplace/resources/qml/ManageButton.qml index 39be04d386..67532bc9aa 100644 --- a/plugins/Marketplace/resources/qml/ManageButton.qml +++ b/plugins/Marketplace/resources/qml/ManageButton.qml @@ -15,6 +15,8 @@ RowLayout property alias secondaryText: secondaryButton.text property string busyPrimaryText: busyMessageText.text property string busySecondaryText: busyMessageText.text + property string confirmedPrimaryText: confirmedMessageText.text + property string confirmedSecondaryText: confirmedMessageText.text property bool enabled: true property bool busy: state == "busy" @@ -28,7 +30,8 @@ RowLayout onClicked: { - busyMessageText.text = manageButton.busyPrimaryText + busyMessage.text = manageButton.busyPrimaryText + confirmedMessage.text = manageButton.confirmedPrimaryText manageButton.clicked(true) } } @@ -41,7 +44,8 @@ RowLayout onClicked: { - busyMessageText.text = manageButton.busySecondaryText + busyMessage.text = manageButton.busySecondaryText + confirmedMessage.text = manageButton.confirmedSecondaryText manageButton.clicked(false) } } @@ -50,6 +54,7 @@ RowLayout { id: busyMessage visible: false + property alias text: busyMessageText.text height: UM.Theme.getSize("action_button").height width: childrenRect.width @@ -90,6 +95,26 @@ RowLayout } } + Item + { + id: confirmedMessage + property alias text: confirmedMessageText.text + + visible: false + height: UM.Theme.getSize("action_button").height + width: childrenRect.width + + Label + { + id: confirmedMessageText + visible: parent.visble + anchors.verticalCenter: parent.verticalCenter + + font: UM.Theme.getFont("medium_bold") + color: UM.Theme.getColor("primary") + } + } + states: [ State @@ -110,6 +135,11 @@ RowLayout target: busyMessage visible: false } + PropertyChanges + { + target: confirmedMessage + visible: false + } }, State { @@ -129,6 +159,11 @@ RowLayout target: busyMessage visible: false } + PropertyChanges + { + target: confirmedMessage + visible: false + } }, State { @@ -157,6 +192,35 @@ RowLayout target: busyMessage visible: manageButton.visible } + PropertyChanges + { + target: confirmedMessage + visible: false + } + }, + State + { + name: "confirmed" + PropertyChanges + { + target: primaryButton + visible: false + } + PropertyChanges + { + target: secondaryButton + visible: false + } + PropertyChanges + { + target: busyMessage + visible: false + } + PropertyChanges + { + target: confirmedMessage + visible: manageButton.visible + } } ] } diff --git a/plugins/Marketplace/resources/qml/PackageCard.qml b/plugins/Marketplace/resources/qml/PackageCard.qml index 326cf0583f..46e9214284 100644 --- a/plugins/Marketplace/resources/qml/PackageCard.qml +++ b/plugins/Marketplace/resources/qml/PackageCard.qml @@ -326,9 +326,11 @@ Rectangle state: packageData.stateManageEnableButton Layout.alignment: Qt.AlignTop primaryText: catalog.i18nc("@button", "Enable") - busyPrimaryText: catalog.i18nc("@button", "Inabling...") + busyPrimaryText: catalog.i18nc("@button", "Enabling...") + confirmedPrimaryText: catalog.i18nc("@button", "Enabled") secondaryText: catalog.i18nc("@button", "Disable") busySecondaryText: catalog.i18nc("@button", "Disabling...") + confirmedSecondaryText: catalog.i18nc("@button", "Disabled") enabled: !(installManageButton.busy || updateManageButton.busy) visible: root.manageableInListView || root.expanded @@ -351,10 +353,12 @@ Rectangle Layout.alignment: Qt.AlignTop primaryText: catalog.i18nc("@button", "Install") busyPrimaryText: catalog.i18nc("@button", "Installing...") + confirmedPrimaryText: catalog.i18nc("@button", "Installed") secondaryText: catalog.i18nc("@button", "Uninstall") busySecondaryText: catalog.i18nc("@button", "Uninstalling...") + confirmedSecondaryText: catalog.i18nc("@button", "Uninstalled") enabled: !(enableManageButton.busy || updateManageButton.busy) - visible: root.manageableInListView || root.expanded + visible: state == "confirmed" || root.manageableInListView || root.expanded onClicked: { @@ -375,7 +379,8 @@ Rectangle state: packageData.stateManageUpdateButton Layout.alignment: Qt.AlignTop primaryText: catalog.i18nc("@button", "Update") - busyPrimaryText: catalog.i18nc("@button", "updating...") + busyPrimaryText: catalog.i18nc("@button", "Updating...") + confirmedPrimaryText: catalog.i18nc("@button", "Updated") enabled: !(installManageButton.busy || enableManageButton.busy) visible: root.manageableInListView || root.expanded From 5b3e9079edfe7ef6722e7c3a17d9eb0fbf410958 Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Tue, 7 Dec 2021 12:46:45 +0100 Subject: [PATCH 105/195] Don't show en-/disable button when un-/installed Contributes to: CURA-8587 --- plugins/Marketplace/resources/qml/ManageButton.qml | 3 ++- plugins/Marketplace/resources/qml/PackageCard.qml | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/Marketplace/resources/qml/ManageButton.qml b/plugins/Marketplace/resources/qml/ManageButton.qml index 67532bc9aa..29b8ff22a5 100644 --- a/plugins/Marketplace/resources/qml/ManageButton.qml +++ b/plugins/Marketplace/resources/qml/ManageButton.qml @@ -19,6 +19,7 @@ RowLayout property string confirmedSecondaryText: confirmedMessageText.text property bool enabled: true property bool busy: state == "busy" + property bool confirmed: state == "confirmed" signal clicked(bool primary_action) @@ -107,7 +108,7 @@ RowLayout Label { id: confirmedMessageText - visible: parent.visble + visible: parent.visible anchors.verticalCenter: parent.verticalCenter font: UM.Theme.getFont("medium_bold") diff --git a/plugins/Marketplace/resources/qml/PackageCard.qml b/plugins/Marketplace/resources/qml/PackageCard.qml index 46e9214284..d782d43cbb 100644 --- a/plugins/Marketplace/resources/qml/PackageCard.qml +++ b/plugins/Marketplace/resources/qml/PackageCard.qml @@ -332,7 +332,7 @@ Rectangle busySecondaryText: catalog.i18nc("@button", "Disabling...") confirmedSecondaryText: catalog.i18nc("@button", "Disabled") enabled: !(installManageButton.busy || updateManageButton.busy) - visible: root.manageableInListView || root.expanded + visible: (root.manageableInListView || root.expanded) && !installManageButton.confirmed onClicked: { if (primary_action) From bb9696c39faf93197f1c4a6ca141437ea8789607 Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Tue, 7 Dec 2021 15:06:46 +0100 Subject: [PATCH 106/195] Reset button if user declines license Contributes to: CURA-8587 --- plugins/Marketplace/PackageList.py | 28 +++++------ plugins/Marketplace/PackageModel.py | 49 +++++++++++-------- .../resources/qml/ManageButton.qml | 34 +++++++++++-- .../Marketplace/resources/qml/PackageCard.qml | 2 +- 4 files changed, 73 insertions(+), 40 deletions(-) diff --git a/plugins/Marketplace/PackageList.py b/plugins/Marketplace/PackageList.py index 47b5e8ff4b..c83c5a7130 100644 --- a/plugins/Marketplace/PackageList.py +++ b/plugins/Marketplace/PackageList.py @@ -18,7 +18,7 @@ from cura.CuraApplication import CuraApplication from cura.CuraPackageManager import CuraPackageManager from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope # To make requests to the Ultimaker API with correct authorization. -from .PackageModel import PackageModel +from .PackageModel import PackageModel, ManageState from .Constants import USER_PACKAGES_URL if TYPE_CHECKING: @@ -161,7 +161,7 @@ class PackageList(ListModel): dialog.deleteLater() # reset package card package = self.getPackageModel(package_id) - package.is_installing = False + package.is_installing = ManageState.FAILED def _requestInstall(self, package_id: str, update: bool = False) -> None: Logger.debug(f"Request installing {package_id}") @@ -185,9 +185,9 @@ class PackageList(ListModel): if package.can_update and to_be_installed: package.can_update = False if update: - package.is_updating = False + package.is_updating = ManageState.HALTED else: - package.is_installing = False + package.is_installing = ManageState.HALTED self.subscribeUserToPackage(package_id, str(package.sdk_version)) def download(self, package_id: str, url: str, update: bool = False) -> None: @@ -232,9 +232,9 @@ class PackageList(ListModel): Logger.error(f"Failed to download package: {package_id} due to {reply_string}") package = self.getPackageModel(package_id) if update: - package.is_updating = False + package.is_updating = ManageState.FAILED else: - package.is_installing = False + package.is_installing = ManageState.FAILED def subscribeUserToPackage(self, package_id: str, sdk_version: str) -> None: """Subscribe the user (if logged in) to the package for a given SDK @@ -275,7 +275,7 @@ class PackageList(ListModel): :param package_id: the package identification string """ package = self.getPackageModel(package_id) - package.is_installing = True + package.is_installing = ManageState.PROCESSING url = package.download_url Logger.debug(f"Trying to download and install {package_id} from {url}") self.download(package_id, url, False) @@ -288,10 +288,10 @@ class PackageList(ListModel): """ Logger.debug(f"Uninstalling {package_id}") package = self.getPackageModel(package_id) - package.is_installing = True + package.is_installing = ManageState.PROCESSING self._manager.removePackage(package_id) self.unsunscribeUserFromPackage(package_id) - package.is_installing = False + package.is_installing = ManageState.HALTED @pyqtSlot(str) def updatePackage(self, package_id: str) -> None: @@ -300,7 +300,7 @@ class PackageList(ListModel): :param package_id: the package identification string """ package = self.getPackageModel(package_id) - package.is_updating = True + package.is_updating = ManageState.PROCESSING self._manager.removePackage(package_id, force_add = True) url = package.download_url Logger.debug(f"Trying to download and update {package_id} from {url}") @@ -313,11 +313,11 @@ class PackageList(ListModel): :param package_id: the package identification string """ package = self.getPackageModel(package_id) - package.is_enabling = True + package.is_enabling = ManageState.PROCESSING Logger.debug(f"Enabling {package_id}") self._plugin_registry.enablePlugin(package_id) package.is_active = True - package.is_enabling = False + package.is_enabling = ManageState.HALTED @pyqtSlot(str) def disablePackage(self, package_id: str) -> None: @@ -326,8 +326,8 @@ class PackageList(ListModel): :param package_id: the package identification string """ package = self.getPackageModel(package_id) - package.is_enabling = True + package.is_enabling = ManageState.PROCESSING Logger.debug(f"Disabling {package_id}") self._plugin_registry.disablePlugin(package_id) package.is_active = False - package.is_enabling = False + package.is_enabling = ManageState.HALTED diff --git a/plugins/Marketplace/PackageModel.py b/plugins/Marketplace/PackageModel.py index 6b313eb9a5..97e57cf951 100644 --- a/plugins/Marketplace/PackageModel.py +++ b/plugins/Marketplace/PackageModel.py @@ -1,16 +1,25 @@ # Copyright (c) 2021 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from PyQt5.QtCore import pyqtProperty, QObject, pyqtSignal import re +from enum import Enum from typing import Any, Dict, List, Optional +from PyQt5.QtCore import pyqtProperty, QObject, pyqtSignal + from cura.Settings.CuraContainerRegistry import CuraContainerRegistry # To get names of materials we're compatible with. from UM.i18n import i18nCatalog # To translate placeholder names if data is not present. +from UM.Logger import Logger catalog = i18nCatalog("cura") +class ManageState(Enum): + PROCESSING = 1 + HALTED = 0 + FAILED = -1 + + class PackageModel(QObject): """ Represents a package, containing all the relevant information to be displayed about a package. @@ -60,14 +69,14 @@ class PackageModel(QObject): if not self._icon_url or self._icon_url == "": self._icon_url = author_data.get("icon_url", "") - self._is_installing = False + self._is_installing: ManageState = ManageState.HALTED self._is_recently_installed = False self._is_recently_updated = False self._is_recently_enabled = False self._can_update = False - self._is_updating = False - self._is_enabling = False + self._is_updating: ManageState = ManageState.HALTED + self._is_enabling: ManageState = ManageState.HALTED self._can_downgrade = False self._section_title = section_title self.sdk_version = package_data.get("sdk_version_semver", "") @@ -288,7 +297,7 @@ class PackageModel(QObject): @pyqtProperty(str, notify = stateManageButtonChanged) def stateManageEnableButton(self) -> str: """The state of the manage Enable Button of this package""" - if self._is_enabling: + if self._is_enabling == ManageState.PROCESSING: return "busy" if self._is_recently_enabled: return "confirmed" @@ -299,16 +308,16 @@ class PackageModel(QObject): return "primary" @property - def is_enabling(self) -> bool: + def is_enabling(self) -> ManageState: """Flag if the package is being enabled/disabled""" return self._is_enabling @is_enabling.setter - def is_enabling(self, value: bool) -> None: + def is_enabling(self, value: ManageState) -> None: if value != self._is_enabling: - if not value: - self._is_recently_enabled = True self._is_enabling = value + if value == ManageState.HALTED: + self._is_recently_enabled = True self.stateManageButtonChanged.emit() @property @@ -327,7 +336,7 @@ class PackageModel(QObject): @pyqtProperty(str, notify = stateManageButtonChanged) def stateManageInstallButton(self) -> str: """The state of the Manage Install package card""" - if self._is_installing: + if self._is_installing == ManageState.PROCESSING: return "busy" if self._is_recently_installed: return "confirmed" @@ -340,16 +349,16 @@ class PackageModel(QObject): return "primary" @property - def is_installing(self) -> bool: - """Flag is we're currently installing""" + def is_installing(self) -> ManageState: + """Flag is we're currently installing, when setting this to ``None`` in indicates a failed installation""" return self._is_installing @is_installing.setter - def is_installing(self, value: bool) -> None: + def is_installing(self, value: ManageState) -> None: if value != self._is_installing: - if not value: - self._is_recently_installed = True self._is_installing = value + if value == ManageState.HALTED: + self._is_recently_installed = True self.stateManageButtonChanged.emit() @property @@ -368,7 +377,7 @@ class PackageModel(QObject): @pyqtProperty(str, notify = stateManageButtonChanged) def stateManageUpdateButton(self) -> str: """The state of the manage Update button for this card """ - if self._is_updating: + if self._is_updating == ManageState.PROCESSING: return "busy" if self._is_recently_updated: return "confirmed" @@ -377,16 +386,16 @@ class PackageModel(QObject): return "hidden" @property - def is_updating(self) -> bool: + def is_updating(self) -> ManageState: """Flag indicating if the package is being updated""" return self._is_updating @is_updating.setter - def is_updating(self, value: bool) -> None: + def is_updating(self, value: ManageState) -> None: if value != self._is_updating: - if not value: - self._is_recently_updated = True self._is_updating = value + if value == ManageState.HALTED: + self._is_recently_updated = True self.stateManageButtonChanged.emit() @property diff --git a/plugins/Marketplace/resources/qml/ManageButton.qml b/plugins/Marketplace/resources/qml/ManageButton.qml index 29b8ff22a5..0b3008461b 100644 --- a/plugins/Marketplace/resources/qml/ManageButton.qml +++ b/plugins/Marketplace/resources/qml/ManageButton.qml @@ -18,8 +18,8 @@ RowLayout property string confirmedPrimaryText: confirmedMessageText.text property string confirmedSecondaryText: confirmedMessageText.text property bool enabled: true - property bool busy: state == "busy" - property bool confirmed: state == "confirmed" + property bool busy: false + property bool confirmed: false signal clicked(bool primary_action) @@ -62,7 +62,7 @@ RowLayout UM.RecolorImage { id: busyIndicator - visible: parent.visible + visible: busyMessage.visible width: height anchors.left: parent.left anchors.top: parent.top @@ -76,7 +76,7 @@ RowLayout RotationAnimator { target: busyIndicator - running: busyIndicator.visible + running: busyMessage.visible from: 0 to: 360 loops: Animation.Infinite @@ -86,7 +86,7 @@ RowLayout Label { id: busyMessageText - visible: parent.visible + visible: busyMessage.visible anchors.left: busyIndicator.right anchors.leftMargin: UM.Theme.getSize("narrow_margin").width anchors.verticalCenter: parent.verticalCenter @@ -122,6 +122,12 @@ RowLayout { name: "primary" PropertyChanges + { + target: manageButton + busy: false + confirmed: false + } + PropertyChanges { target: primaryButton visible: true @@ -146,6 +152,12 @@ RowLayout { name: "secondary" PropertyChanges + { + target: manageButton + busy: false + confirmed: false + } + PropertyChanges { target: primaryButton visible: false @@ -179,6 +191,12 @@ RowLayout { name: "busy" PropertyChanges + { + target: manageButton + busy: true + confirmed: false + } + PropertyChanges { target: primaryButton visible: false @@ -203,6 +221,12 @@ RowLayout { name: "confirmed" PropertyChanges + { + target: manageButton + busy: false + confirmed: true + } + PropertyChanges { target: primaryButton visible: false diff --git a/plugins/Marketplace/resources/qml/PackageCard.qml b/plugins/Marketplace/resources/qml/PackageCard.qml index d782d43cbb..721a152b12 100644 --- a/plugins/Marketplace/resources/qml/PackageCard.qml +++ b/plugins/Marketplace/resources/qml/PackageCard.qml @@ -358,7 +358,7 @@ Rectangle busySecondaryText: catalog.i18nc("@button", "Uninstalling...") confirmedSecondaryText: catalog.i18nc("@button", "Uninstalled") enabled: !(enableManageButton.busy || updateManageButton.busy) - visible: state == "confirmed" || root.manageableInListView || root.expanded + visible: installManageButton.confirmed || root.manageableInListView || root.expanded onClicked: { From ea4ec5ca2714a426ae1e7b2ed31a69d329e10128 Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Tue, 7 Dec 2021 15:29:05 +0100 Subject: [PATCH 107/195] Removed old debug logger messages Contributes to: CURA-8587 --- plugins/Marketplace/PackageList.py | 14 -------------- plugins/Marketplace/PackageModel.py | 1 - 2 files changed, 15 deletions(-) diff --git a/plugins/Marketplace/PackageList.py b/plugins/Marketplace/PackageList.py index c83c5a7130..62e54e783c 100644 --- a/plugins/Marketplace/PackageList.py +++ b/plugins/Marketplace/PackageList.py @@ -125,8 +125,6 @@ class PackageList(ListModel): canInstallChanged = pyqtSignal(str, bool) def _openLicenseDialog(self, package_id: str, license_content: str) -> None: - Logger.debug(f"Prompting license for {package_id}") - plugin_path = self._plugin_registry.getPluginPath("Marketplace") if plugin_path is None: plugin_path = os.path.dirname(__file__) @@ -144,7 +142,6 @@ class PackageList(ListModel): @pyqtSlot(str) def onLicenseAccepted(self, package_id: str) -> None: - Logger.debug(f"Accepted license for {package_id}") # close dialog dialog = self._license_dialogs.pop(package_id) if dialog is not None: @@ -154,7 +151,6 @@ class PackageList(ListModel): @pyqtSlot(str) def onLicenseDeclined(self, package_id: str) -> None: - Logger.debug(f"Declined license for {package_id}") # close dialog dialog = self._license_dialogs.pop(package_id) if dialog is not None: @@ -164,8 +160,6 @@ class PackageList(ListModel): package.is_installing = ManageState.FAILED def _requestInstall(self, package_id: str, update: bool = False) -> None: - Logger.debug(f"Request installing {package_id}") - package_path = self._to_install[package_id] license_content = self._manager.getPackageLicense(package_path) @@ -178,7 +172,6 @@ class PackageList(ListModel): self._install(package_id, update) def _install(self, package_id: str, update: bool = False) -> None: - Logger.debug(f"Installing {package_id}") package_path = self._to_install.pop(package_id) to_be_installed = self._manager.installPackage(package_path) is not None package = self.getPackageModel(package_id) @@ -243,7 +236,6 @@ class PackageList(ListModel): :param sdk_version: the SDK version """ if self._account.isLoggedIn: - Logger.debug(f"Subscribing the user for package: {package_id}") HttpRequestManager.getInstance().put( url = USER_PACKAGES_URL, data = json.dumps({"data": {"package_id": package_id, "sdk_version": sdk_version}}).encode(), @@ -256,7 +248,6 @@ class PackageList(ListModel): :param package_id: the package identification string """ if self._account.isLoggedIn: - Logger.debug(f"Unsubscribing the user for package: {package_id}") HttpRequestManager.getInstance().delete(url = f"{USER_PACKAGES_URL}/{package_id}", scope = self._scope) # --- Handle the manage package buttons --- @@ -277,7 +268,6 @@ class PackageList(ListModel): package = self.getPackageModel(package_id) package.is_installing = ManageState.PROCESSING url = package.download_url - Logger.debug(f"Trying to download and install {package_id} from {url}") self.download(package_id, url, False) @pyqtSlot(str) @@ -286,7 +276,6 @@ class PackageList(ListModel): :param package_id: the package identification string """ - Logger.debug(f"Uninstalling {package_id}") package = self.getPackageModel(package_id) package.is_installing = ManageState.PROCESSING self._manager.removePackage(package_id) @@ -303,7 +292,6 @@ class PackageList(ListModel): package.is_updating = ManageState.PROCESSING self._manager.removePackage(package_id, force_add = True) url = package.download_url - Logger.debug(f"Trying to download and update {package_id} from {url}") self.download(package_id, url, True) @pyqtSlot(str) @@ -314,7 +302,6 @@ class PackageList(ListModel): """ package = self.getPackageModel(package_id) package.is_enabling = ManageState.PROCESSING - Logger.debug(f"Enabling {package_id}") self._plugin_registry.enablePlugin(package_id) package.is_active = True package.is_enabling = ManageState.HALTED @@ -327,7 +314,6 @@ class PackageList(ListModel): """ package = self.getPackageModel(package_id) package.is_enabling = ManageState.PROCESSING - Logger.debug(f"Disabling {package_id}") self._plugin_registry.disablePlugin(package_id) package.is_active = False package.is_enabling = ManageState.HALTED diff --git a/plugins/Marketplace/PackageModel.py b/plugins/Marketplace/PackageModel.py index 97e57cf951..4e7fce5ce4 100644 --- a/plugins/Marketplace/PackageModel.py +++ b/plugins/Marketplace/PackageModel.py @@ -9,7 +9,6 @@ from PyQt5.QtCore import pyqtProperty, QObject, pyqtSignal from cura.Settings.CuraContainerRegistry import CuraContainerRegistry # To get names of materials we're compatible with. from UM.i18n import i18nCatalog # To translate placeholder names if data is not present. -from UM.Logger import Logger catalog = i18nCatalog("cura") From dae92c354cb3dc0ef952a98c807411195347ed41 Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Tue, 7 Dec 2021 15:32:36 +0100 Subject: [PATCH 108/195] Marketplace doesn't need to inherit from QObject Review comment Contributes to: CURA-8587 --- plugins/Marketplace/Marketplace.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/plugins/Marketplace/Marketplace.py b/plugins/Marketplace/Marketplace.py index d55e538f6c..b47dae0a4a 100644 --- a/plugins/Marketplace/Marketplace.py +++ b/plugins/Marketplace/Marketplace.py @@ -2,7 +2,7 @@ # Cura is released under the terms of the LGPLv3 or higher. import os.path -from PyQt5.QtCore import pyqtSlot, QObject +from PyQt5.QtCore import pyqtSlot from PyQt5.QtQml import qmlRegisterType from typing import Optional, TYPE_CHECKING @@ -18,15 +18,14 @@ if TYPE_CHECKING: from PyQt5.QtCore import QObject -class Marketplace(Extension, QObject): +class Marketplace(Extension): """ The main managing object for the Marketplace plug-in. """ - def __init__(self, parent: Optional[QObject] = None) -> None: - QObject.__init__(self, parent = parent) - Extension.__init__(self) - self._window: Optional[QObject] = None # If the window has been loaded yet, it'll be cached in here. + def __init__(self) -> None: + super().__init__() + self._window: Optional["QObject"] = None # If the window has been loaded yet, it'll be cached in here. self.plugin_registry: Optional[PluginRegistry] = None qmlRegisterType(RemotePackageList, "Marketplace", 1, 0, "RemotePackageList") From 013e0b51e9b7266a4f570fdb9776b0093f4dc53d Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Tue, 7 Dec 2021 15:56:35 +0100 Subject: [PATCH 109/195] Storing multiple ongoing_requests A bit of defensive programming Contributes to: CURA-8587 --- plugins/Marketplace/LocalPackageList.py | 4 +++- plugins/Marketplace/PackageList.py | 8 ++++---- plugins/Marketplace/RemotePackageList.py | 7 ++++--- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/plugins/Marketplace/LocalPackageList.py b/plugins/Marketplace/LocalPackageList.py index 196f3f19c6..ebda51b4fe 100644 --- a/plugins/Marketplace/LocalPackageList.py +++ b/plugins/Marketplace/LocalPackageList.py @@ -38,6 +38,7 @@ class LocalPackageList(PackageList): def __init__(self, parent: Optional["QObject"] = None) -> None: super().__init__(parent) self._has_footer = False + self._ongoing_requests["check_updates"] = None @pyqtSlot() def updatePackages(self) -> None: @@ -74,7 +75,7 @@ class LocalPackageList(PackageList): installed_packages = "installed_packages=".join([f"{package['package_id']}:{package['package_version']}&" for package in packages]) request_url = f"{PACKAGE_UPDATES_URL}?installed_packages={installed_packages[:-1]}" - self._ongoing_request = HttpRequestManager.getInstance().get( + self._ongoing_requests["check_updates"] = HttpRequestManager.getInstance().get( request_url, scope = self._scope, callback = self._parseResponse @@ -100,3 +101,4 @@ class LocalPackageList(PackageList): package.can_update = True self.sort(attrgetter("sectionTitle", "can_update", "displayName"), key = "package", reverse = True) + self._ongoing_requests["check_updates"] = None diff --git a/plugins/Marketplace/PackageList.py b/plugins/Marketplace/PackageList.py index 62e54e783c..a2c67dc1ef 100644 --- a/plugins/Marketplace/PackageList.py +++ b/plugins/Marketplace/PackageList.py @@ -5,7 +5,7 @@ import json import os.path from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, Qt -from typing import cast, Dict, Optional, Set, TYPE_CHECKING +from typing import cast, Dict, List, Optional, Set, TYPE_CHECKING from UM.i18n import i18nCatalog from UM.Qt.ListModel import ListModel @@ -49,7 +49,7 @@ class PackageList(ListModel): self.canInstallChanged.connect(self._requestInstall) self._local_packages: Set[str] = {p["package_id"] for p in self._manager.local_packages} - self._ongoing_request: Optional[HttpRequestData] = None + self._ongoing_requests: Dict[str, Optional[HttpRequestData]] = {"download_package": None} self._scope = JsonDecoratorScope(UltimakerCloudScope(CuraApplication.getInstance())) self._license_dialogs: Dict[str, QObject] = {} @@ -197,7 +197,7 @@ class PackageList(ListModel): def downloadError(reply: "QNetworkReply", error: "QNetworkReply.NetworkError") -> None: self._downloadError(package_id, update, reply, error) - HttpRequestManager.getInstance().get( + self._ongoing_requests["download_package"] = HttpRequestManager.getInstance().get( url, scope = self._scope, callback = downloadFinished, @@ -211,8 +211,8 @@ class PackageList(ListModel): while bytes_read: temp_file.write(bytes_read) bytes_read = reply.read(self.DISK_WRITE_BUFFER_SIZE) - Logger.debug(f"Finished downloading {package_id} and stored it as {temp_file.name}") self._to_install[package_id] = temp_file.name + self._ongoing_requests["download_package"] = None self.canInstallChanged.emit(package_id, update) except IOError as e: Logger.error(f"Failed to write downloaded package to temp file {e}") diff --git a/plugins/Marketplace/RemotePackageList.py b/plugins/Marketplace/RemotePackageList.py index 88e1c28045..13f3d33d93 100644 --- a/plugins/Marketplace/RemotePackageList.py +++ b/plugins/Marketplace/RemotePackageList.py @@ -29,6 +29,7 @@ class RemotePackageList(PackageList): self._requested_search_string = "" self._current_search_string = "" self._request_url = self._initialRequestUrl() + self._ongoing_requests["get_packages"] = None self.isLoadingChanged.connect(self._onLoadingChanged) self.isLoadingChanged.emit() @@ -49,7 +50,7 @@ class RemotePackageList(PackageList): self.setErrorMessage("") # Clear any previous errors. self.setIsLoading(True) - self._ongoing_request = HttpRequestManager.getInstance().get( + self._ongoing_requests["get_packages"] = HttpRequestManager.getInstance().get( self._request_url, scope = self._scope, callback = self._parseResponse, @@ -58,8 +59,8 @@ class RemotePackageList(PackageList): @pyqtSlot() def abortUpdating(self) -> None: - HttpRequestManager.getInstance().abortRequest(self._ongoing_request) - self._ongoing_request = None + HttpRequestManager.getInstance().abortRequest(self._ongoing_requests["get_packages"]) + self._ongoing_requests["get_packages"] = None def reset(self) -> None: self.clear() From d2a9d7d94d1d4ffb0b880693fcb8bb160ef7195d Mon Sep 17 00:00:00 2001 From: Remco Burema Date: Tue, 7 Dec 2021 16:02:54 +0100 Subject: [PATCH 110/195] Get 'already going to be installed' status in constructor. Otherwise this isn't saved, and the state of 'installed, but needs restart' (as shown in the package card) won't be known to the package card (buttons), resulting in an 'Install' button when tabs are switched. part of CURA-8587 --- plugins/Marketplace/PackageModel.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/Marketplace/PackageModel.py b/plugins/Marketplace/PackageModel.py index 4e7fce5ce4..ed64a85c04 100644 --- a/plugins/Marketplace/PackageModel.py +++ b/plugins/Marketplace/PackageModel.py @@ -7,6 +7,7 @@ from typing import Any, Dict, List, Optional from PyQt5.QtCore import pyqtProperty, QObject, pyqtSignal +from cura.CuraApplication import CuraApplication from cura.Settings.CuraContainerRegistry import CuraContainerRegistry # To get names of materials we're compatible with. from UM.i18n import i18nCatalog # To translate placeholder names if data is not present. @@ -69,7 +70,7 @@ class PackageModel(QObject): self._icon_url = author_data.get("icon_url", "") self._is_installing: ManageState = ManageState.HALTED - self._is_recently_installed = False + self._is_recently_installed = self._package_id in CuraApplication.getInstance().getPackageManager().getPackagesToInstall() self._is_recently_updated = False self._is_recently_enabled = False From 14bc196154edd03802ab2ae84a6c9ac6bdf42115 Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Tue, 7 Dec 2021 16:08:51 +0100 Subject: [PATCH 111/195] Hidden other manage buttons when the other is confirmed Contributes to: CURA-8587 --- plugins/Marketplace/resources/qml/PackageCard.qml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/Marketplace/resources/qml/PackageCard.qml b/plugins/Marketplace/resources/qml/PackageCard.qml index 721a152b12..12f24f6f6f 100644 --- a/plugins/Marketplace/resources/qml/PackageCard.qml +++ b/plugins/Marketplace/resources/qml/PackageCard.qml @@ -332,7 +332,7 @@ Rectangle busySecondaryText: catalog.i18nc("@button", "Disabling...") confirmedSecondaryText: catalog.i18nc("@button", "Disabled") enabled: !(installManageButton.busy || updateManageButton.busy) - visible: (root.manageableInListView || root.expanded) && !installManageButton.confirmed + visible: (root.manageableInListView || root.expanded) && !(installManageButton.confirmed || updateManageButton.confirmed) onClicked: { if (primary_action) @@ -358,7 +358,7 @@ Rectangle busySecondaryText: catalog.i18nc("@button", "Uninstalling...") confirmedSecondaryText: catalog.i18nc("@button", "Uninstalled") enabled: !(enableManageButton.busy || updateManageButton.busy) - visible: installManageButton.confirmed || root.manageableInListView || root.expanded + visible: (installManageButton.confirmed || root.manageableInListView || root.expanded) && !(updateManageButton.confirmed || enableManageButton.confirmed) onClicked: { @@ -382,7 +382,7 @@ Rectangle busyPrimaryText: catalog.i18nc("@button", "Updating...") confirmedPrimaryText: catalog.i18nc("@button", "Updated") enabled: !(installManageButton.busy || enableManageButton.busy) - visible: root.manageableInListView || root.expanded + visible: (root.manageableInListView || root.expanded) && !installManageButton.confirmed onClicked: packageData.updatePackageTriggered(packageData.packageId) } From 9e4258ef8b3d625178f34496e06dcf6f7eff4275 Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Tue, 7 Dec 2021 16:22:31 +0100 Subject: [PATCH 112/195] Set the is_recently_installed flag Contributes to: CURA-8587 --- plugins/Marketplace/LocalPackageList.py | 6 +++--- plugins/Marketplace/PackageModel.py | 13 +++++++++++-- plugins/Marketplace/RemotePackageList.py | 5 ++++- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/plugins/Marketplace/LocalPackageList.py b/plugins/Marketplace/LocalPackageList.py index ebda51b4fe..c3491fc3af 100644 --- a/plugins/Marketplace/LocalPackageList.py +++ b/plugins/Marketplace/LocalPackageList.py @@ -65,10 +65,10 @@ class LocalPackageList(PackageList): package_type = package_info["package_type"] section_title = self.PACKAGE_CATEGORIES[bundled_or_installed][package_type] package = PackageModel(package_info, section_title = section_title, parent = self) - if package_id in self._manager.getPackagesToRemove() or package_id in self._manager.getPackagesToInstall(): - package.is_recently_managed = True - package.can_downgrade = self._manager.canDowngrade(package_id) self._connectManageButtonSignals(package) + package.can_downgrade = self._manager.canDowngrade(package_id) + if package_id in self._manager.getPackagesToRemove() or package_id in self._manager.getPackagesToInstall(): + package.is_recently_installed = True return package def checkForUpdates(self, packages: List[Dict[str, Any]]): diff --git a/plugins/Marketplace/PackageModel.py b/plugins/Marketplace/PackageModel.py index ed64a85c04..55e8aa3a75 100644 --- a/plugins/Marketplace/PackageModel.py +++ b/plugins/Marketplace/PackageModel.py @@ -7,7 +7,6 @@ from typing import Any, Dict, List, Optional from PyQt5.QtCore import pyqtProperty, QObject, pyqtSignal -from cura.CuraApplication import CuraApplication from cura.Settings.CuraContainerRegistry import CuraContainerRegistry # To get names of materials we're compatible with. from UM.i18n import i18nCatalog # To translate placeholder names if data is not present. @@ -70,7 +69,7 @@ class PackageModel(QObject): self._icon_url = author_data.get("icon_url", "") self._is_installing: ManageState = ManageState.HALTED - self._is_recently_installed = self._package_id in CuraApplication.getInstance().getPackageManager().getPackagesToInstall() + self._is_recently_installed = False self._is_recently_updated = False self._is_recently_enabled = False @@ -361,6 +360,16 @@ class PackageModel(QObject): self._is_recently_installed = True self.stateManageButtonChanged.emit() + @property + def is_recently_installed(self): + return self._is_recently_installed + + @is_recently_installed.setter + def is_recently_installed(self, value): + if value != self._is_recently_installed: + value = self._is_recently_installed + self.stateManageButtonChanged.emit() + @property def can_downgrade(self) -> bool: """Flag if the installed package can be downgraded to a bundled version""" diff --git a/plugins/Marketplace/RemotePackageList.py b/plugins/Marketplace/RemotePackageList.py index 13f3d33d93..583a427c44 100644 --- a/plugins/Marketplace/RemotePackageList.py +++ b/plugins/Marketplace/RemotePackageList.py @@ -129,11 +129,14 @@ class RemotePackageList(PackageList): return for package_data in response_data["data"]: - if package_data["package_id"] in self._local_packages: + package_id = package_data["package_id"] + if package_id in self._local_packages: continue # We should only show packages which are not already installed try: package = PackageModel(package_data, parent = self) self._connectManageButtonSignals(package) + if package_id in self._manager.getPackagesToRemove() or package_id in self._manager.getPackagesToInstall(): + package.is_recently_installed = True self.appendItem({"package": package}) # Add it to this list model. except RuntimeError: # Setting the ownership of this object to not qml can still result in a RuntimeError. Which can occur when quickly toggling From 3be6747e5dc9d0129878e9fe57556bb8be8df53a Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Tue, 7 Dec 2021 18:25:46 +0100 Subject: [PATCH 113/195] Trying to set a persistent install managebutton Contributes to: CURA-8587 --- plugins/Marketplace/PackageModel.py | 2 +- .../resources/qml/ManageButton.qml | 57 +++++++------------ .../Marketplace/resources/qml/PackageCard.qml | 20 ++++--- 3 files changed, 34 insertions(+), 45 deletions(-) diff --git a/plugins/Marketplace/PackageModel.py b/plugins/Marketplace/PackageModel.py index 55e8aa3a75..99d0039701 100644 --- a/plugins/Marketplace/PackageModel.py +++ b/plugins/Marketplace/PackageModel.py @@ -367,7 +367,7 @@ class PackageModel(QObject): @is_recently_installed.setter def is_recently_installed(self, value): if value != self._is_recently_installed: - value = self._is_recently_installed + self._is_recently_installed = value self.stateManageButtonChanged.emit() @property diff --git a/plugins/Marketplace/resources/qml/ManageButton.qml b/plugins/Marketplace/resources/qml/ManageButton.qml index 0b3008461b..a423e90ff6 100644 --- a/plugins/Marketplace/resources/qml/ManageButton.qml +++ b/plugins/Marketplace/resources/qml/ManageButton.qml @@ -17,16 +17,14 @@ RowLayout property string busySecondaryText: busyMessageText.text property string confirmedPrimaryText: confirmedMessageText.text property string confirmedSecondaryText: confirmedMessageText.text - property bool enabled: true - property bool busy: false - property bool confirmed: false + property bool busy + property bool confirmed signal clicked(bool primary_action) Cura.PrimaryButton { id: primaryButton - visible: false enabled: manageButton.enabled onClicked: @@ -40,7 +38,6 @@ RowLayout Cura.SecondaryButton { id: secondaryButton - visible: false enabled: manageButton.enabled onClicked: @@ -54,7 +51,6 @@ RowLayout Item { id: busyMessage - visible: false property alias text: busyMessageText.text height: UM.Theme.getSize("action_button").height width: childrenRect.width @@ -62,7 +58,6 @@ RowLayout UM.RecolorImage { id: busyIndicator - visible: busyMessage.visible width: height anchors.left: parent.left anchors.top: parent.top @@ -86,7 +81,6 @@ RowLayout Label { id: busyMessageText - visible: busyMessage.visible anchors.left: busyIndicator.right anchors.leftMargin: UM.Theme.getSize("narrow_margin").width anchors.verticalCenter: parent.verticalCenter @@ -101,14 +95,12 @@ RowLayout id: confirmedMessage property alias text: confirmedMessageText.text - visible: false height: UM.Theme.getSize("action_button").height width: childrenRect.width Label { id: confirmedMessageText - visible: parent.visible anchors.verticalCenter: parent.verticalCenter font: UM.Theme.getFont("medium_bold") @@ -122,12 +114,6 @@ RowLayout { name: "primary" PropertyChanges - { - target: manageButton - busy: false - confirmed: false - } - PropertyChanges { target: primaryButton visible: true @@ -152,12 +138,6 @@ RowLayout { name: "secondary" PropertyChanges - { - target: manageButton - busy: false - confirmed: false - } - PropertyChanges { target: primaryButton visible: false @@ -183,7 +163,22 @@ RowLayout name: "hidden" PropertyChanges { - target: manageButton + target: primaryButton + visible: false + } + PropertyChanges + { + target: secondaryButton + visible: false + } + PropertyChanges + { + target: busyMessage + visible: false + } + PropertyChanges + { + target: confirmedMessage visible: false } }, @@ -191,12 +186,6 @@ RowLayout { name: "busy" PropertyChanges - { - target: manageButton - busy: true - confirmed: false - } - PropertyChanges { target: primaryButton visible: false @@ -209,7 +198,7 @@ RowLayout PropertyChanges { target: busyMessage - visible: manageButton.visible + visible: true } PropertyChanges { @@ -221,12 +210,6 @@ RowLayout { name: "confirmed" PropertyChanges - { - target: manageButton - busy: false - confirmed: true - } - PropertyChanges { target: primaryButton visible: false @@ -244,7 +227,7 @@ RowLayout PropertyChanges { target: confirmedMessage - visible: manageButton.visible + visible: true } } ] diff --git a/plugins/Marketplace/resources/qml/PackageCard.qml b/plugins/Marketplace/resources/qml/PackageCard.qml index 12f24f6f6f..667c645c61 100644 --- a/plugins/Marketplace/resources/qml/PackageCard.qml +++ b/plugins/Marketplace/resources/qml/PackageCard.qml @@ -323,7 +323,9 @@ Rectangle ManageButton { id: enableManageButton - state: packageData.stateManageEnableButton + state: !(installManageButton.confirmed || updateManageButton.confirmed) || enableManageButton.confirmed ? packageData.stateManageEnableButton : "hidden" + busy: packageData.enableManageButton == "busy" + confirmed: packageData.enableManageButton == "confirmed" Layout.alignment: Qt.AlignTop primaryText: catalog.i18nc("@button", "Enable") busyPrimaryText: catalog.i18nc("@button", "Enabling...") @@ -332,9 +334,9 @@ Rectangle busySecondaryText: catalog.i18nc("@button", "Disabling...") confirmedSecondaryText: catalog.i18nc("@button", "Disabled") enabled: !(installManageButton.busy || updateManageButton.busy) - visible: (root.manageableInListView || root.expanded) && !(installManageButton.confirmed || updateManageButton.confirmed) - onClicked: { + onClicked: + { if (primary_action) { packageData.enablePackageTriggered(packageData.packageId) @@ -349,7 +351,9 @@ Rectangle ManageButton { id: installManageButton - state: packageData.stateManageInstallButton + state: (root.manageableInListView || root.expanded || installManageButton.confirmed) && !(enableManageButton.confirmed || updateManageButton.confirmed) ? packageData.stateManageInstallButton : "hidden" + busy: packageData.stateManageInstallButton == "busy" + confirmed: packageData.stateManageInstallButton == "confirmed" Layout.alignment: Qt.AlignTop primaryText: catalog.i18nc("@button", "Install") busyPrimaryText: catalog.i18nc("@button", "Installing...") @@ -358,7 +362,8 @@ Rectangle busySecondaryText: catalog.i18nc("@button", "Uninstalling...") confirmedSecondaryText: catalog.i18nc("@button", "Uninstalled") enabled: !(enableManageButton.busy || updateManageButton.busy) - visible: (installManageButton.confirmed || root.manageableInListView || root.expanded) && !(updateManageButton.confirmed || enableManageButton.confirmed) + + onStateChanged: print(packageData.displayName + " " + state) // TODO: Cleanup once you find out why this happens onClicked: { @@ -376,13 +381,14 @@ Rectangle ManageButton { id: updateManageButton - state: packageData.stateManageUpdateButton + state: (root.manageableInListView || root.expanded) && (!installManageButton.confirmed || updateManageButton.confirmed) ? packageData.stateManageUpdateButton : "hidden" + busy: packageData.stateManageUpdateButton == "busy" + confirmed: packageData.stateManageUpdateButton == "confirmed" Layout.alignment: Qt.AlignTop primaryText: catalog.i18nc("@button", "Update") busyPrimaryText: catalog.i18nc("@button", "Updating...") confirmedPrimaryText: catalog.i18nc("@button", "Updated") enabled: !(installManageButton.busy || enableManageButton.busy) - visible: (root.manageableInListView || root.expanded) && !installManageButton.confirmed onClicked: packageData.updatePackageTriggered(packageData.packageId) } From a61c3e9eff66eae187101f559e96aa264bef2c17 Mon Sep 17 00:00:00 2001 From: Remco Burema Date: Tue, 7 Dec 2021 21:23:54 +0100 Subject: [PATCH 114/195] Peristance of 'Installed' text. part of CURA-8587 --- plugins/Marketplace/PackageModel.py | 6 ++++++ plugins/Marketplace/resources/qml/ManageButton.qml | 2 ++ plugins/Marketplace/resources/qml/PackageCard.qml | 3 +-- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/plugins/Marketplace/PackageModel.py b/plugins/Marketplace/PackageModel.py index 99d0039701..382a9fc881 100644 --- a/plugins/Marketplace/PackageModel.py +++ b/plugins/Marketplace/PackageModel.py @@ -291,6 +291,8 @@ class PackageModel(QObject): disablePackageTriggered = pyqtSignal(str) + recentlyInstalledChanged = pyqtSignal(bool) + # --- enabling --- @pyqtProperty(str, notify = stateManageButtonChanged) @@ -370,6 +372,10 @@ class PackageModel(QObject): self._is_recently_installed = value self.stateManageButtonChanged.emit() + @pyqtProperty(bool, notify = stateManageButtonChanged) + def isRecentlyInstalled(self): + return self._is_recently_installed + @property def can_downgrade(self) -> bool: """Flag if the installed package can be downgraded to a bundled version""" diff --git a/plugins/Marketplace/resources/qml/ManageButton.qml b/plugins/Marketplace/resources/qml/ManageButton.qml index a423e90ff6..b2a3ec7f1b 100644 --- a/plugins/Marketplace/resources/qml/ManageButton.qml +++ b/plugins/Marketplace/resources/qml/ManageButton.qml @@ -19,6 +19,7 @@ RowLayout property string confirmedSecondaryText: confirmedMessageText.text property bool busy property bool confirmed + property bool confirmedTextChoice: true signal clicked(bool primary_action) @@ -228,6 +229,7 @@ RowLayout { target: confirmedMessage visible: true + text: manageButton.confirmedTextChoice ? manageButton.confirmedPrimaryText : manageButton.confirmedSecondaryText } } ] diff --git a/plugins/Marketplace/resources/qml/PackageCard.qml b/plugins/Marketplace/resources/qml/PackageCard.qml index 667c645c61..b43ea580ba 100644 --- a/plugins/Marketplace/resources/qml/PackageCard.qml +++ b/plugins/Marketplace/resources/qml/PackageCard.qml @@ -361,10 +361,9 @@ Rectangle secondaryText: catalog.i18nc("@button", "Uninstall") busySecondaryText: catalog.i18nc("@button", "Uninstalling...") confirmedSecondaryText: catalog.i18nc("@button", "Uninstalled") + confirmedTextChoice: packageData.isRecentlyInstalled enabled: !(enableManageButton.busy || updateManageButton.busy) - onStateChanged: print(packageData.displayName + " " + state) // TODO: Cleanup once you find out why this happens - onClicked: { if (primary_action) From 6c976bc9b0d989777be3ba9d65896fb92b8144dd Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Wed, 8 Dec 2021 08:06:56 +0100 Subject: [PATCH 115/195] Introduced a Manager to centralize plugin/package management Should have done this from the start. Will move other relevant scattered functions to this type. For now it checks if the restart banner needs to show. Taking into account that a user can toggle between enable and disable without an actual restart. Even with multiple plugins. Contributes to: CURA-8587 --- plugins/Marketplace/Manager.py | 32 +++++++++++++++++++ plugins/Marketplace/Marketplace.py | 4 ++- plugins/Marketplace/PackageModel.py | 2 -- .../Marketplace/resources/qml/Marketplace.qml | 4 ++- 4 files changed, 38 insertions(+), 4 deletions(-) create mode 100644 plugins/Marketplace/Manager.py diff --git a/plugins/Marketplace/Manager.py b/plugins/Marketplace/Manager.py new file mode 100644 index 0000000000..f367579079 --- /dev/null +++ b/plugins/Marketplace/Manager.py @@ -0,0 +1,32 @@ +# Copyright (c) 2021 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. +from typing import Optional + +from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal + +from cura.CuraApplication import CuraApplication +from UM.PluginRegistry import PluginRegistry + +class Manager(QObject): + def __init__(self, parent: Optional[QObject] = None): + super().__init__(parent = parent) + self._manager: "CuraPackageManager" = CuraApplication.getInstance().getPackageManager() + self._plugin_registry: PluginRegistry = CuraApplication.getInstance().getPluginRegistry() + + self._manager.installedPackagesChanged.connect(self.checkIfRestartNeeded) + self._plugin_registry.hasPluginsEnabledOrDisabledChanged.connect(self.checkIfRestartNeeded) + + self._restart_needed = False + + def checkIfRestartNeeded(self): + if self._manager.hasPackagesToRemoveOrInstall or len(self._plugin_registry.getCurrentSessionActivationChangedPlugins()) > 0: + self._restart_needed = True + else: + self._restart_needed = False + self.showRestartNotificationChanged.emit() + + showRestartNotificationChanged = pyqtSignal() + + @pyqtProperty(bool, notify = showRestartNotificationChanged) + def showRestartNotification(self) -> bool: + return self._restart_needed diff --git a/plugins/Marketplace/Marketplace.py b/plugins/Marketplace/Marketplace.py index b47dae0a4a..858ea867ee 100644 --- a/plugins/Marketplace/Marketplace.py +++ b/plugins/Marketplace/Marketplace.py @@ -13,6 +13,7 @@ from UM.PluginRegistry import PluginRegistry # To find out where we are stored from .RemotePackageList import RemotePackageList # To register this type with QML. from .LocalPackageList import LocalPackageList # To register this type with QML. +from .Manager import Manager # To register this type with QML. if TYPE_CHECKING: from PyQt5.QtCore import QObject @@ -30,6 +31,7 @@ class Marketplace(Extension): qmlRegisterType(RemotePackageList, "Marketplace", 1, 0, "RemotePackageList") qmlRegisterType(LocalPackageList, "Marketplace", 1, 0, "LocalPackageList") + qmlRegisterType(Manager, "Marketplace", 1, 0, "Manager") @pyqtSlot() def show(self) -> None: @@ -44,7 +46,7 @@ class Marketplace(Extension): if plugin_path is None: plugin_path = os.path.dirname(__file__) path = os.path.join(plugin_path, "resources", "qml", "Marketplace.qml") - self._window = CuraApplication.getInstance().createQmlComponent(path, {"plugin_registry": self.plugin_registry}) + self._window = CuraApplication.getInstance().createQmlComponent(path, {}) if self._window is None: # Still None? Failed to load the QML then. return self._window.show() diff --git a/plugins/Marketplace/PackageModel.py b/plugins/Marketplace/PackageModel.py index 382a9fc881..c17769d6ef 100644 --- a/plugins/Marketplace/PackageModel.py +++ b/plugins/Marketplace/PackageModel.py @@ -300,8 +300,6 @@ class PackageModel(QObject): """The state of the manage Enable Button of this package""" if self._is_enabling == ManageState.PROCESSING: return "busy" - if self._is_recently_enabled: - return "confirmed" if self._package_type == "material" or not self._is_installed: return "hidden" if self._is_installed and self._is_active: diff --git a/plugins/Marketplace/resources/qml/Marketplace.qml b/plugins/Marketplace/resources/qml/Marketplace.qml index 5c56b0c41d..c04ef5c027 100644 --- a/plugins/Marketplace/resources/qml/Marketplace.qml +++ b/plugins/Marketplace/resources/qml/Marketplace.qml @@ -8,11 +8,13 @@ import QtQuick.Window 2.2 import UM 1.2 as UM import Cura 1.6 as Cura +import Marketplace 1.0 as Marketplace Window { id: marketplaceDialog property variant catalog: UM.I18nCatalog { name: "cura" } + property variant manager: Marketplace.Manager { } signal searchStringChanged(string new_search) @@ -232,7 +234,7 @@ Window { height: quitButton.height + 2 * UM.Theme.getSize("default_margin").width color: UM.Theme.getColor("primary") - visible: CuraApplication.getPackageManager().hasPackagesToRemoveOrInstall || plugin_registry.hasPluginsEnabledOrDisabled + visible: manager.showRestartNotification anchors { left: parent.left From 453de95d12d93dcbf013dbaa2677590521064cc1 Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Wed, 8 Dec 2021 09:58:53 +0100 Subject: [PATCH 116/195] Defensive programming Long API calls might return after the Local or Remote PackageList has been deconstructed. Somehow setting the ownership in QML doesn't seem to work for this. So we guard against this with a try catch block. Contributes to: CURA-8587 --- plugins/Marketplace/LocalPackageList.py | 18 ++++++++++++------ plugins/Marketplace/PackageList.py | 5 +++++ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/plugins/Marketplace/LocalPackageList.py b/plugins/Marketplace/LocalPackageList.py index c3491fc3af..6fb9cfbf34 100644 --- a/plugins/Marketplace/LocalPackageList.py +++ b/plugins/Marketplace/LocalPackageList.py @@ -95,10 +95,16 @@ class LocalPackageList(PackageList): if len(response_data["data"]) == 0: return - for package_data in response_data["data"]: - package = self.getPackageModel(package_data["package_id"]) - package.download_url = package_data.get("download_url", "") - package.can_update = True + try: + for package_data in response_data["data"]: + package = self.getPackageModel(package_data["package_id"]) + package.download_url = package_data.get("download_url", "") + package.can_update = True - self.sort(attrgetter("sectionTitle", "can_update", "displayName"), key = "package", reverse = True) - self._ongoing_requests["check_updates"] = None + self.sort(attrgetter("sectionTitle", "can_update", "displayName"), key = "package", reverse = True) + self._ongoing_requests["check_updates"] = None + except RuntimeError: + # Setting the ownership of this object to not qml can still result in a RuntimeError. Which can occur when quickly toggling + # between de-/constructing RemotePackageLists. This try-except is here to prevent a hard crash when the wrapped C++ object + # was deleted when it was still parsing the response + return diff --git a/plugins/Marketplace/PackageList.py b/plugins/Marketplace/PackageList.py index a2c67dc1ef..7e64373e9c 100644 --- a/plugins/Marketplace/PackageList.py +++ b/plugins/Marketplace/PackageList.py @@ -218,6 +218,11 @@ class PackageList(ListModel): Logger.error(f"Failed to write downloaded package to temp file {e}") temp_file.close() self._downloadError(package_id, update) + except RuntimeError: + # Setting the ownership of this object to not qml can still result in a RuntimeError. Which can occur when quickly toggling + # between de-/constructing Remote or Local PackageLists. This try-except is here to prevent a hard crash when the wrapped C++ object + # was deleted when it was still parsing the response + return def _downloadError(self, package_id: str, update: bool = False, reply: Optional["QNetworkReply"] = None, error: Optional["QNetworkReply.NetworkError"] = None) -> None: if reply: From 7be2da587b9e37b0171d644543c77f3ab63a4e21 Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Wed, 8 Dec 2021 10:18:08 +0100 Subject: [PATCH 117/195] Automatic abortRequest for each API request Defensive programming Contributes to: CURA-8587 --- plugins/Marketplace/PackageList.py | 23 +++++++++++++------ plugins/Marketplace/RemotePackageList.py | 12 ---------- .../Marketplace/resources/qml/Packages.qml | 2 +- 3 files changed, 17 insertions(+), 20 deletions(-) diff --git a/plugins/Marketplace/PackageList.py b/plugins/Marketplace/PackageList.py index 7e64373e9c..e3af31d81a 100644 --- a/plugins/Marketplace/PackageList.py +++ b/plugins/Marketplace/PackageList.py @@ -5,7 +5,7 @@ import json import os.path from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, Qt -from typing import cast, Dict, List, Optional, Set, TYPE_CHECKING +from typing import cast, Dict, Optional, Set, TYPE_CHECKING from UM.i18n import i18nCatalog from UM.Qt.ListModel import ListModel @@ -53,17 +53,26 @@ class PackageList(ListModel): self._scope = JsonDecoratorScope(UltimakerCloudScope(CuraApplication.getInstance())) self._license_dialogs: Dict[str, QObject] = {} + def __del__(self) -> None: + """ When this object is deleted it will loop through all registered API requests and aborts them """ + self.cleanUpAPIRequest() + + def abortRequest(self, request_id: str) -> None: + """Aborts a single request""" + if request_id in self._ongoing_requests and self._ongoing_requests[request_id]: + HttpRequestManager.getInstance().abortRequest(self._ongoing_requests[request_id]) + self._ongoing_requests[request_id] = None + + @pyqtSlot() + def cleanUpAPIRequest(self) -> None: + for request_id in self._ongoing_requests: + self.abortRequest(request_id) + @pyqtSlot() def updatePackages(self) -> None: """ A Qt slot which will update the List from a source. Actual implementation should be done in the child class""" pass - @pyqtSlot() - def abortUpdating(self) -> None: - """ A Qt slot which allows the update process to be aborted. Override this for child classes with async/callback - updatePackges methods""" - pass - def reset(self) -> None: """ Resets and clears the list""" self.clear() diff --git a/plugins/Marketplace/RemotePackageList.py b/plugins/Marketplace/RemotePackageList.py index 583a427c44..d20cb5f4b0 100644 --- a/plugins/Marketplace/RemotePackageList.py +++ b/plugins/Marketplace/RemotePackageList.py @@ -33,13 +33,6 @@ class RemotePackageList(PackageList): self.isLoadingChanged.connect(self._onLoadingChanged) self.isLoadingChanged.emit() - def __del__(self) -> None: - """ - When deleting this object, abort the request so that we don't get a callback from it later on a deleted C++ - object. - """ - self.abortUpdating() - @pyqtSlot() def updatePackages(self) -> None: """ @@ -57,11 +50,6 @@ class RemotePackageList(PackageList): error_callback = self._onError ) - @pyqtSlot() - def abortUpdating(self) -> None: - HttpRequestManager.getInstance().abortRequest(self._ongoing_requests["get_packages"]) - self._ongoing_requests["get_packages"] = None - def reset(self) -> None: self.clear() self._request_url = self._initialRequestUrl() diff --git a/plugins/Marketplace/resources/qml/Packages.qml b/plugins/Marketplace/resources/qml/Packages.qml index 62c0f92149..194c90c248 100644 --- a/plugins/Marketplace/resources/qml/Packages.qml +++ b/plugins/Marketplace/resources/qml/Packages.qml @@ -24,7 +24,7 @@ ListView clip: true Component.onCompleted: model.updatePackages() - Component.onDestruction: model.abortUpdating() + Component.onDestruction: model.cleanUpAPIRequest() spacing: UM.Theme.getSize("default_margin").height From 5e35e19f219e9500403bd0bd5397e44cbbf91cd8 Mon Sep 17 00:00:00 2001 From: casper Date: Wed, 8 Dec 2021 10:53:51 +0100 Subject: [PATCH 118/195] Split PackageCard into PackageCard and PackagePage cura 8734 --- .../Marketplace/resources/qml/PackageCard.qml | 298 +--------- .../resources/qml/PackageDetails.qml | 7 +- .../Marketplace/resources/qml/PackagePage.qml | 511 ++++++++++++++++++ 3 files changed, 516 insertions(+), 300 deletions(-) create mode 100644 plugins/Marketplace/resources/qml/PackagePage.qml diff --git a/plugins/Marketplace/resources/qml/PackageCard.qml b/plugins/Marketplace/resources/qml/PackageCard.qml index b43ea580ba..19b6280ddf 100644 --- a/plugins/Marketplace/resources/qml/PackageCard.qml +++ b/plugins/Marketplace/resources/qml/PackageCard.qml @@ -12,57 +12,12 @@ Rectangle { id: root property var packageData - property bool expanded: false property bool manageableInListView height: childrenRect.height color: UM.Theme.getColor("main_background") radius: UM.Theme.getSize("default_radius").width - states: - [ - State - { - name: "Folded" - when: !expanded - PropertyChanges - { - target: shortDescription - visible: true - } - PropertyChanges - { - target: downloadCount - visible: false - } - PropertyChanges - { - target: extendedDescription - visible: false - } - }, - State - { - name: "Expanded" - when: expanded - PropertyChanges - { - target: shortDescription - visible: false - } - PropertyChanges - { - target: downloadCount - visible: true - } - PropertyChanges - { - target: extendedDescription - visible: true - } - } - ] - Column { width: parent.width @@ -258,32 +213,6 @@ Rectangle } } - Row - { - id: downloadCount - Layout.preferredWidth: parent.width - Layout.fillHeight: true - - UM.RecolorImage - { - id: downloadsIcon - width: UM.Theme.getSize("card_tiny_icon").width - height: UM.Theme.getSize("card_tiny_icon").height - - source: UM.Theme.getIcon("Download") - color: UM.Theme.getColor("text") - } - - Label - { - anchors.verticalCenter: downloadsIcon.verticalCenter - - color: UM.Theme.getColor("text") - font: UM.Theme.getFont("default") - text: packageData.downloadCount - } - } - // Author and action buttons. RowLayout { @@ -351,7 +280,7 @@ Rectangle ManageButton { id: installManageButton - state: (root.manageableInListView || root.expanded || installManageButton.confirmed) && !(enableManageButton.confirmed || updateManageButton.confirmed) ? packageData.stateManageInstallButton : "hidden" + state: (root.manageableInListView || installManageButton.confirmed) && !(enableManageButton.confirmed || updateManageButton.confirmed) ? packageData.stateManageInstallButton : "hidden" busy: packageData.stateManageInstallButton == "busy" confirmed: packageData.stateManageInstallButton == "confirmed" Layout.alignment: Qt.AlignTop @@ -380,7 +309,7 @@ Rectangle ManageButton { id: updateManageButton - state: (root.manageableInListView || root.expanded) && (!installManageButton.confirmed || updateManageButton.confirmed) ? packageData.stateManageUpdateButton : "hidden" + state: (root.manageableInListView) && (!installManageButton.confirmed || updateManageButton.confirmed) ? packageData.stateManageUpdateButton : "hidden" busy: packageData.stateManageUpdateButton == "busy" confirmed: packageData.stateManageUpdateButton == "confirmed" Layout.alignment: Qt.AlignTop @@ -394,229 +323,6 @@ Rectangle } } } - - Column - { - id: extendedDescription - width: parent.width - - padding: UM.Theme.getSize("default_margin").width - topPadding: 0 - spacing: UM.Theme.getSize("default_margin").height - - Label - { - width: parent.width - parent.padding * 2 - - text: catalog.i18nc("@header", "Description") - font: UM.Theme.getFont("medium_bold") - color: UM.Theme.getColor("text") - elide: Text.ElideRight - } - - Label - { - width: parent.width - parent.padding * 2 - - text: packageData.formattedDescription - font: UM.Theme.getFont("medium") - color: UM.Theme.getColor("text") - linkColor: UM.Theme.getColor("text_link") - wrapMode: Text.Wrap - textFormat: Text.RichText - - onLinkActivated: UM.UrlUtil.openUrl(link, ["http", "https"]) - } - - Column //Separate column to have no spacing between compatible printers. - { - id: compatiblePrinterColumn - width: parent.width - parent.padding * 2 - - visible: packageData.packageType === "material" - spacing: 0 - - Label - { - width: parent.width - - text: catalog.i18nc("@header", "Compatible printers") - font: UM.Theme.getFont("medium_bold") - color: UM.Theme.getColor("text") - elide: Text.ElideRight - } - - Repeater - { - model: packageData.compatiblePrinters - - Label - { - width: compatiblePrinterColumn.width - - text: modelData - font: UM.Theme.getFont("medium") - color: UM.Theme.getColor("text") - elide: Text.ElideRight - } - } - - Label - { - width: parent.width - - visible: packageData.compatiblePrinters.length == 0 - text: "(" + catalog.i18nc("@info", "No compatibility information") + ")" - font: UM.Theme.getFont("medium") - color: UM.Theme.getColor("text") - elide: Text.ElideRight - } - } - - Column - { - id: compatibleSupportMaterialColumn - width: parent.width - parent.padding * 2 - - visible: packageData.packageType === "material" - spacing: 0 - - Label - { - width: parent.width - - text: catalog.i18nc("@header", "Compatible support materials") - font: UM.Theme.getFont("medium_bold") - color: UM.Theme.getColor("text") - elide: Text.ElideRight - } - - Repeater - { - model: packageData.compatibleSupportMaterials - - Label - { - width: compatibleSupportMaterialColumn.width - - text: modelData - font: UM.Theme.getFont("medium") - color: UM.Theme.getColor("text") - elide: Text.ElideRight - } - } - - Label - { - width: parent.width - - visible: packageData.compatibleSupportMaterials.length == 0 - text: "(" + catalog.i18nc("@info No materials", "None") + ")" - font: UM.Theme.getFont("medium") - color: UM.Theme.getColor("text") - elide: Text.ElideRight - } - } - - Column - { - width: parent.width - parent.padding * 2 - - visible: packageData.packageType === "material" - spacing: 0 - - Label - { - width: parent.width - - text: catalog.i18nc("@header", "Compatible with Material Station") - font: UM.Theme.getFont("medium_bold") - color: UM.Theme.getColor("text") - elide: Text.ElideRight - } - - Label - { - width: parent.width - - text: packageData.isCompatibleMaterialStation ? catalog.i18nc("@info", "Yes") : catalog.i18nc("@info", "No") - font: UM.Theme.getFont("medium") - color: UM.Theme.getColor("text") - elide: Text.ElideRight - } - } - - Column - { - width: parent.width - parent.padding * 2 - - visible: packageData.packageType === "material" - spacing: 0 - - Label - { - width: parent.width - - text: catalog.i18nc("@header", "Optimized for Air Manager") - font: UM.Theme.getFont("medium_bold") - color: UM.Theme.getColor("text") - elide: Text.ElideRight - } - - Label - { - width: parent.width - - text: packageData.isCompatibleAirManager ? catalog.i18nc("@info", "Yes") : catalog.i18nc("@info", "No") - font: UM.Theme.getFont("medium") - color: UM.Theme.getColor("text") - elide: Text.ElideRight - } - } - - Row - { - id: externalButtonRow - anchors.horizontalCenter: parent.horizontalCenter - - spacing: UM.Theme.getSize("narrow_margin").width - - Cura.SecondaryButton - { - text: packageData.packageType === "plugin" ? catalog.i18nc("@button", "Visit plug-in website") : catalog.i18nc("@button", "Website") - iconSource: UM.Theme.getIcon("Globe") - outlineColor: "transparent" - onClicked: Qt.openUrlExternally(packageData.packageInfoUrl) - } - - Cura.SecondaryButton - { - visible: packageData.packageType === "material" - text: catalog.i18nc("@button", "Buy spool") - iconSource: UM.Theme.getIcon("ShoppingCart") - outlineColor: "transparent" - onClicked: Qt.openUrlExternally(packageData.whereToBuy) - } - - Cura.SecondaryButton - { - visible: packageData.packageType === "material" - text: catalog.i18nc("@button", "Safety datasheet") - iconSource: UM.Theme.getIcon("Warning") - outlineColor: "transparent" - onClicked: Qt.openUrlExternally(packageData.safetyDataSheet) - } - - Cura.SecondaryButton - { - visible: packageData.packageType === "material" - text: catalog.i18nc("@button", "Technical datasheet") - iconSource: UM.Theme.getIcon("DocumentFilled") - outlineColor: "transparent" - onClicked: Qt.openUrlExternally(packageData.technicalDataSheet) - } - } - } } FontMetrics diff --git a/plugins/Marketplace/resources/qml/PackageDetails.qml b/plugins/Marketplace/resources/qml/PackageDetails.qml index fdf1c8f92c..2599c7f28c 100644 --- a/plugins/Marketplace/resources/qml/PackageDetails.qml +++ b/plugins/Marketplace/resources/qml/PackageDetails.qml @@ -74,11 +74,11 @@ Item clip: true //Need to clip, not for the bottom (which is off the window) but for the top (which would overlap the header). ScrollBar.horizontal.policy: ScrollBar.AlwaysOff - contentHeight: expandedPackageCard.height + UM.Theme.getSize("default_margin").height * 2 + contentHeight: packagePage.height + UM.Theme.getSize("default_margin").height * 2 - PackageCard + PackagePage { - id: expandedPackageCard + id: packagePage anchors { left: parent.left @@ -90,7 +90,6 @@ Item } packageData: detailPage.packageData - expanded: true } } } diff --git a/plugins/Marketplace/resources/qml/PackagePage.qml b/plugins/Marketplace/resources/qml/PackagePage.qml new file mode 100644 index 0000000000..1669f8ffc7 --- /dev/null +++ b/plugins/Marketplace/resources/qml/PackagePage.qml @@ -0,0 +1,511 @@ +// Copyright (c) 2021 Ultimaker B.V. +// Cura is released under the terms of the LGPLv3 or higher. + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.1 + +import UM 1.6 as UM +import Cura 1.6 as Cura + +Rectangle +{ + id: root + property var packageData + property bool manageableInListView + + height: childrenRect.height + color: UM.Theme.getColor("main_background") + radius: UM.Theme.getSize("default_radius").width + + Column + { + width: parent.width + + spacing: 0 + + Item + { + width: parent.width + height: UM.Theme.getSize("card").height + + Image + { + id: packageItem + anchors + { + top: parent.top + left: parent.left + margins: UM.Theme.getSize("default_margin").width + } + width: UM.Theme.getSize("card_icon").width + height: width + + source: packageData.iconUrl != "" ? packageData.iconUrl : "../images/placeholder.svg" + } + + ColumnLayout + { + anchors + { + left: packageItem.right + leftMargin: UM.Theme.getSize("default_margin").width + right: parent.right + rightMargin: UM.Theme.getSize("default_margin").width + top: parent.top + topMargin: UM.Theme.getSize("narrow_margin").height + } + height: packageItem.height + packageItem.anchors.margins * 2 + + // Title row. + RowLayout + { + id: titleBar + Layout.preferredWidth: parent.width + Layout.preferredHeight: childrenRect.height + + Label + { + text: packageData.displayName + font: UM.Theme.getFont("medium_bold") + color: UM.Theme.getColor("text") + verticalAlignment: Text.AlignTop + } + VerifiedIcon + { + enabled: packageData.isCheckedByUltimaker + visible: packageData.isCheckedByUltimaker + } + + + Control + { + Layout.preferredWidth: UM.Theme.getSize("card_tiny_icon").width + Layout.preferredHeight: UM.Theme.getSize("card_tiny_icon").height + Layout.alignment: Qt.AlignCenter + enabled: false // remove! + visible: false // replace packageInfo.XXXXXX + // TODO: waiting for materials card implementation + + Cura.ToolTip + { + tooltipText: "" // TODO + visible: parent.hovered + } + + UM.RecolorImage + { + anchors.fill: parent + + color: UM.Theme.getColor("primary") + source: UM.Theme.getIcon("CheckCircle") // TODO + } + + // onClicked: Qt.openUrlExternally( XXXXXX ) // TODO + } + + Label + { + id: packageVersionLabel + text: packageData.packageVersion + font: UM.Theme.getFont("default") + color: UM.Theme.getColor("text") + Layout.fillWidth: true + } + + Button + { + id: externalLinkButton + + // For some reason if i set padding, they don't match up. If i set all of them explicitly, it does work? + leftPadding: UM.Theme.getSize("narrow_margin").width + rightPadding: UM.Theme.getSize("narrow_margin").width + topPadding: UM.Theme.getSize("narrow_margin").width + bottomPadding: UM.Theme.getSize("narrow_margin").width + + Layout.preferredWidth: UM.Theme.getSize("card_tiny_icon").width + 2 * padding + Layout.preferredHeight: UM.Theme.getSize("card_tiny_icon").width + 2 * padding + contentItem: UM.RecolorImage + { + source: UM.Theme.getIcon("LinkExternal") + color: UM.Theme.getColor("icon") + implicitWidth: UM.Theme.getSize("card_tiny_icon").width + implicitHeight: UM.Theme.getSize("card_tiny_icon").height + } + + background: Rectangle + { + color: externalLinkButton.hovered ? UM.Theme.getColor("action_button_hovered"): "transparent" + radius: externalLinkButton.width / 2 + } + onClicked: Qt.openUrlExternally(packageData.authorInfoUrl) + } + } + + Row + { + id: downloadCount + Layout.preferredWidth: parent.width + Layout.fillHeight: true + + UM.RecolorImage + { + id: downloadsIcon + width: UM.Theme.getSize("card_tiny_icon").width + height: UM.Theme.getSize("card_tiny_icon").height + + source: UM.Theme.getIcon("Download") + color: UM.Theme.getColor("text") + } + + Label + { + anchors.verticalCenter: downloadsIcon.verticalCenter + + color: UM.Theme.getColor("text") + font: UM.Theme.getFont("default") + text: packageData.downloadCount + } + } + + // Author and action buttons. + RowLayout + { + id: authorAndActionButton + Layout.preferredWidth: parent.width + Layout.preferredHeight: childrenRect.height + + spacing: UM.Theme.getSize("narrow_margin").width + + Label + { + id: authorBy + Layout.alignment: Qt.AlignCenter + + text: catalog.i18nc("@label", "By") + font: UM.Theme.getFont("default") + color: UM.Theme.getColor("text") + } + + Cura.TertiaryButton + { + Layout.fillWidth: true + Layout.preferredHeight: authorBy.height + Layout.alignment: Qt.AlignCenter + + text: packageData.authorName + textFont: UM.Theme.getFont("default_bold") + textColor: UM.Theme.getColor("text") // override normal link color + leftPadding: 0 + rightPadding: 0 + iconSource: UM.Theme.getIcon("LinkExternal") + isIconOnRightSide: true + + onClicked: Qt.openUrlExternally(packageData.authorInfoUrl) + } + + ManageButton + { + id: enableManageButton + state: !(installManageButton.confirmed || updateManageButton.confirmed) || enableManageButton.confirmed ? packageData.stateManageEnableButton : "hidden" + busy: packageData.enableManageButton == "busy" + confirmed: packageData.enableManageButton == "confirmed" + Layout.alignment: Qt.AlignTop + primaryText: catalog.i18nc("@button", "Enable") + busyPrimaryText: catalog.i18nc("@button", "Enabling...") + confirmedPrimaryText: catalog.i18nc("@button", "Enabled") + secondaryText: catalog.i18nc("@button", "Disable") + busySecondaryText: catalog.i18nc("@button", "Disabling...") + confirmedSecondaryText: catalog.i18nc("@button", "Disabled") + enabled: !(installManageButton.busy || updateManageButton.busy) + + onClicked: + { + if (primary_action) + { + packageData.enablePackageTriggered(packageData.packageId) + } + else + { + packageData.disablePackageTriggered(packageData.packageId) + } + } + } + + ManageButton + { + id: installManageButton + state: !(enableManageButton.confirmed || updateManageButton.confirmed) ? packageData.stateManageInstallButton : "hidden" + busy: packageData.stateManageInstallButton == "busy" + confirmed: packageData.stateManageInstallButton == "confirmed" + Layout.alignment: Qt.AlignTop + primaryText: catalog.i18nc("@button", "Install") + busyPrimaryText: catalog.i18nc("@button", "Installing...") + confirmedPrimaryText: catalog.i18nc("@button", "Installed") + secondaryText: catalog.i18nc("@button", "Uninstall") + busySecondaryText: catalog.i18nc("@button", "Uninstalling...") + confirmedSecondaryText: catalog.i18nc("@button", "Uninstalled") + confirmedTextChoice: packageData.isRecentlyInstalled + enabled: !(enableManageButton.busy || updateManageButton.busy) + + onClicked: + { + if (primary_action) + { + packageData.installPackageTriggered(packageData.packageId) + } + else + { + packageData.uninstallPackageTriggered(packageData.packageId) + } + } + } + + ManageButton + { + id: updateManageButton + state: !installManageButton.confirmed || updateManageButton.confirmed ? packageData.stateManageUpdateButton : "hidden" + busy: packageData.stateManageUpdateButton == "busy" + confirmed: packageData.stateManageUpdateButton == "confirmed" + Layout.alignment: Qt.AlignTop + primaryText: catalog.i18nc("@button", "Update") + busyPrimaryText: catalog.i18nc("@button", "Updating...") + confirmedPrimaryText: catalog.i18nc("@button", "Updated") + enabled: !(installManageButton.busy || enableManageButton.busy) + + onClicked: packageData.updatePackageTriggered(packageData.packageId) + } + } + } + } + + Column + { + id: extendedDescription + width: parent.width + + padding: UM.Theme.getSize("default_margin").width + topPadding: 0 + spacing: UM.Theme.getSize("default_margin").height + + Label + { + width: parent.width - parent.padding * 2 + + text: catalog.i18nc("@header", "Description") + font: UM.Theme.getFont("medium_bold") + color: UM.Theme.getColor("text") + elide: Text.ElideRight + } + + Label + { + width: parent.width - parent.padding * 2 + + text: packageData.formattedDescription + font: UM.Theme.getFont("medium") + color: UM.Theme.getColor("text") + linkColor: UM.Theme.getColor("text_link") + wrapMode: Text.Wrap + textFormat: Text.RichText + + onLinkActivated: UM.UrlUtil.openUrl(link, ["http", "https"]) + } + + Column //Separate column to have no spacing between compatible printers. + { + id: compatiblePrinterColumn + width: parent.width - parent.padding * 2 + + visible: packageData.packageType === "material" + spacing: 0 + + Label + { + width: parent.width + + text: catalog.i18nc("@header", "Compatible printers") + font: UM.Theme.getFont("medium_bold") + color: UM.Theme.getColor("text") + elide: Text.ElideRight + } + + Repeater + { + model: packageData.compatiblePrinters + + Label + { + width: compatiblePrinterColumn.width + + text: modelData + font: UM.Theme.getFont("medium") + color: UM.Theme.getColor("text") + elide: Text.ElideRight + } + } + + Label + { + width: parent.width + + visible: packageData.compatiblePrinters.length == 0 + text: "(" + catalog.i18nc("@info", "No compatibility information") + ")" + font: UM.Theme.getFont("medium") + color: UM.Theme.getColor("text") + elide: Text.ElideRight + } + } + + Column + { + id: compatibleSupportMaterialColumn + width: parent.width - parent.padding * 2 + + visible: packageData.packageType === "material" + spacing: 0 + + Label + { + width: parent.width + + text: catalog.i18nc("@header", "Compatible support materials") + font: UM.Theme.getFont("medium_bold") + color: UM.Theme.getColor("text") + elide: Text.ElideRight + } + + Repeater + { + model: packageData.compatibleSupportMaterials + + Label + { + width: compatibleSupportMaterialColumn.width + + text: modelData + font: UM.Theme.getFont("medium") + color: UM.Theme.getColor("text") + elide: Text.ElideRight + } + } + + Label + { + width: parent.width + + visible: packageData.compatibleSupportMaterials.length == 0 + text: "(" + catalog.i18nc("@info No materials", "None") + ")" + font: UM.Theme.getFont("medium") + color: UM.Theme.getColor("text") + elide: Text.ElideRight + } + } + + Column + { + width: parent.width - parent.padding * 2 + + visible: packageData.packageType === "material" + spacing: 0 + + Label + { + width: parent.width + + text: catalog.i18nc("@header", "Compatible with Material Station") + font: UM.Theme.getFont("medium_bold") + color: UM.Theme.getColor("text") + elide: Text.ElideRight + } + + Label + { + width: parent.width + + text: packageData.isCompatibleMaterialStation ? catalog.i18nc("@info", "Yes") : catalog.i18nc("@info", "No") + font: UM.Theme.getFont("medium") + color: UM.Theme.getColor("text") + elide: Text.ElideRight + } + } + + Column + { + width: parent.width - parent.padding * 2 + + visible: packageData.packageType === "material" + spacing: 0 + + Label + { + width: parent.width + + text: catalog.i18nc("@header", "Optimized for Air Manager") + font: UM.Theme.getFont("medium_bold") + color: UM.Theme.getColor("text") + elide: Text.ElideRight + } + + Label + { + width: parent.width + + text: packageData.isCompatibleAirManager ? catalog.i18nc("@info", "Yes") : catalog.i18nc("@info", "No") + font: UM.Theme.getFont("medium") + color: UM.Theme.getColor("text") + elide: Text.ElideRight + } + } + + Row + { + id: externalButtonRow + anchors.horizontalCenter: parent.horizontalCenter + + spacing: UM.Theme.getSize("narrow_margin").width + + Cura.SecondaryButton + { + text: packageData.packageType === "plugin" ? catalog.i18nc("@button", "Visit plug-in website") : catalog.i18nc("@button", "Website") + iconSource: UM.Theme.getIcon("Globe") + outlineColor: "transparent" + onClicked: Qt.openUrlExternally(packageData.packageInfoUrl) + } + + Cura.SecondaryButton + { + visible: packageData.packageType === "material" + text: catalog.i18nc("@button", "Buy spool") + iconSource: UM.Theme.getIcon("ShoppingCart") + outlineColor: "transparent" + onClicked: Qt.openUrlExternally(packageData.whereToBuy) + } + + Cura.SecondaryButton + { + visible: packageData.packageType === "material" + text: catalog.i18nc("@button", "Safety datasheet") + iconSource: UM.Theme.getIcon("Warning") + outlineColor: "transparent" + onClicked: Qt.openUrlExternally(packageData.safetyDataSheet) + } + + Cura.SecondaryButton + { + visible: packageData.packageType === "material" + text: catalog.i18nc("@button", "Technical datasheet") + iconSource: UM.Theme.getIcon("DocumentFilled") + outlineColor: "transparent" + onClicked: Qt.openUrlExternally(packageData.technicalDataSheet) + } + } + } + } + + FontMetrics + { + id: fontMetrics + font: UM.Theme.getFont("default") + } +} From db09954ac853f261390f8f1f58e0936281057852 Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Wed, 8 Dec 2021 10:59:28 +0100 Subject: [PATCH 119/195] Fixed returning an error after the package is destroyed Defensive programming Contributes to: CURA-8587 --- plugins/Marketplace/PackageList.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/plugins/Marketplace/PackageList.py b/plugins/Marketplace/PackageList.py index e3af31d81a..f7dcd29feb 100644 --- a/plugins/Marketplace/PackageList.py +++ b/plugins/Marketplace/PackageList.py @@ -237,11 +237,17 @@ class PackageList(ListModel): if reply: reply_string = bytes(reply.readAll()).decode() Logger.error(f"Failed to download package: {package_id} due to {reply_string}") - package = self.getPackageModel(package_id) - if update: - package.is_updating = ManageState.FAILED - else: - package.is_installing = ManageState.FAILED + try: + package = self.getPackageModel(package_id) + if update: + package.is_updating = ManageState.FAILED + else: + package.is_installing = ManageState.FAILED + except RuntimeError: + # Setting the ownership of this object to not qml can still result in a RuntimeError. Which can occur when quickly toggling + # between de-/constructing Remote or Local PackageLists. This try-except is here to prevent a hard crash when the wrapped C++ object + # was deleted when it was still parsing the response + return def subscribeUserToPackage(self, package_id: str, sdk_version: str) -> None: """Subscribe the user (if logged in) to the package for a given SDK From 27cb1d2d9f858f5c24d2363a5a1ca32baedabf33 Mon Sep 17 00:00:00 2001 From: Remco Burema Date: Wed, 8 Dec 2021 14:04:19 +0100 Subject: [PATCH 120/195] Also have/keep the uninstalled status displayed. 'isRecentlyInstalled' was both for installing _and_ uninstalling. Will add the rename to the refactor later on. part of CURA-8587 --- plugins/Marketplace/PackageModel.py | 5 +++-- plugins/Marketplace/resources/qml/PackageCard.qml | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/plugins/Marketplace/PackageModel.py b/plugins/Marketplace/PackageModel.py index c17769d6ef..b1b6db733d 100644 --- a/plugins/Marketplace/PackageModel.py +++ b/plugins/Marketplace/PackageModel.py @@ -7,6 +7,7 @@ from typing import Any, Dict, List, Optional from PyQt5.QtCore import pyqtProperty, QObject, pyqtSignal +from cura.CuraApplication import CuraApplication from cura.Settings.CuraContainerRegistry import CuraContainerRegistry # To get names of materials we're compatible with. from UM.i18n import i18nCatalog # To translate placeholder names if data is not present. @@ -371,8 +372,8 @@ class PackageModel(QObject): self.stateManageButtonChanged.emit() @pyqtProperty(bool, notify = stateManageButtonChanged) - def isRecentlyInstalled(self): - return self._is_recently_installed + def installationStatus(self): + return self._package_id in CuraApplication.getInstance().getPackageManager().getPackagesToInstall() @property def can_downgrade(self) -> bool: diff --git a/plugins/Marketplace/resources/qml/PackageCard.qml b/plugins/Marketplace/resources/qml/PackageCard.qml index b43ea580ba..f552bd8e69 100644 --- a/plugins/Marketplace/resources/qml/PackageCard.qml +++ b/plugins/Marketplace/resources/qml/PackageCard.qml @@ -361,7 +361,7 @@ Rectangle secondaryText: catalog.i18nc("@button", "Uninstall") busySecondaryText: catalog.i18nc("@button", "Uninstalling...") confirmedSecondaryText: catalog.i18nc("@button", "Uninstalled") - confirmedTextChoice: packageData.isRecentlyInstalled + confirmedTextChoice: packageData.installationStatus enabled: !(enableManageButton.busy || updateManageButton.busy) onClicked: From fd508342fe2b96669d22913942ba76616609db75 Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Wed, 8 Dec 2021 14:33:22 +0100 Subject: [PATCH 121/195] Renamed Manager to RestartManager Contributes to: CURA-8587 --- plugins/Marketplace/Marketplace.py | 4 ++-- .../Marketplace/{Manager.py => RestartManager.py} | 12 ++++++++---- plugins/Marketplace/resources/qml/Marketplace.qml | 4 ++-- 3 files changed, 12 insertions(+), 8 deletions(-) rename plugins/Marketplace/{Manager.py => RestartManager.py} (79%) diff --git a/plugins/Marketplace/Marketplace.py b/plugins/Marketplace/Marketplace.py index 858ea867ee..5ebc7830ed 100644 --- a/plugins/Marketplace/Marketplace.py +++ b/plugins/Marketplace/Marketplace.py @@ -13,7 +13,7 @@ from UM.PluginRegistry import PluginRegistry # To find out where we are stored from .RemotePackageList import RemotePackageList # To register this type with QML. from .LocalPackageList import LocalPackageList # To register this type with QML. -from .Manager import Manager # To register this type with QML. +from .RestartManager import RestartManager # To register this type with QML. if TYPE_CHECKING: from PyQt5.QtCore import QObject @@ -31,7 +31,7 @@ class Marketplace(Extension): qmlRegisterType(RemotePackageList, "Marketplace", 1, 0, "RemotePackageList") qmlRegisterType(LocalPackageList, "Marketplace", 1, 0, "LocalPackageList") - qmlRegisterType(Manager, "Marketplace", 1, 0, "Manager") + qmlRegisterType(RestartManager, "Marketplace", 1, 0, "RestartManager") @pyqtSlot() def show(self) -> None: diff --git a/plugins/Marketplace/Manager.py b/plugins/Marketplace/RestartManager.py similarity index 79% rename from plugins/Marketplace/Manager.py rename to plugins/Marketplace/RestartManager.py index f367579079..5b66ca0292 100644 --- a/plugins/Marketplace/Manager.py +++ b/plugins/Marketplace/RestartManager.py @@ -1,17 +1,21 @@ # Copyright (c) 2021 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from typing import Optional +from typing import Optional, TYPE_CHECKING from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal from cura.CuraApplication import CuraApplication -from UM.PluginRegistry import PluginRegistry -class Manager(QObject): +if TYPE_CHECKING: + from UM.PluginRegistry import PluginRegistry + from cura.CuraPackageManager import CuraPackageManager + + +class RestartManager(QObject): def __init__(self, parent: Optional[QObject] = None): super().__init__(parent = parent) self._manager: "CuraPackageManager" = CuraApplication.getInstance().getPackageManager() - self._plugin_registry: PluginRegistry = CuraApplication.getInstance().getPluginRegistry() + self._plugin_registry: "PluginRegistry" = CuraApplication.getInstance().getPluginRegistry() self._manager.installedPackagesChanged.connect(self.checkIfRestartNeeded) self._plugin_registry.hasPluginsEnabledOrDisabledChanged.connect(self.checkIfRestartNeeded) diff --git a/plugins/Marketplace/resources/qml/Marketplace.qml b/plugins/Marketplace/resources/qml/Marketplace.qml index c04ef5c027..017a9e3dde 100644 --- a/plugins/Marketplace/resources/qml/Marketplace.qml +++ b/plugins/Marketplace/resources/qml/Marketplace.qml @@ -14,7 +14,7 @@ Window { id: marketplaceDialog property variant catalog: UM.I18nCatalog { name: "cura" } - property variant manager: Marketplace.Manager { } + property variant restartManager: Marketplace.RestartManager { } signal searchStringChanged(string new_search) @@ -234,7 +234,7 @@ Window { height: quitButton.height + 2 * UM.Theme.getSize("default_margin").width color: UM.Theme.getColor("primary") - visible: manager.showRestartNotification + visible: restartManager.showRestartNotification anchors { left: parent.left From 4a436b5598f9df2dfdd0f192539ecc5943b0f737 Mon Sep 17 00:00:00 2001 From: casper Date: Wed, 8 Dec 2021 14:46:29 +0100 Subject: [PATCH 122/195] Display different types of manage buttons through Loader.sourceComponent cura 8734 --- .../resources/qml/ManageButton.qml | 291 ++++++------------ .../Marketplace/resources/qml/PackageCard.qml | 17 +- .../Marketplace/resources/qml/PackagePage.qml | 12 +- 3 files changed, 109 insertions(+), 211 deletions(-) diff --git a/plugins/Marketplace/resources/qml/ManageButton.qml b/plugins/Marketplace/resources/qml/ManageButton.qml index b2a3ec7f1b..0614e90c87 100644 --- a/plugins/Marketplace/resources/qml/ManageButton.qml +++ b/plugins/Marketplace/resources/qml/ManageButton.qml @@ -11,226 +11,131 @@ import Cura 1.6 as Cura RowLayout { id: manageButton - property alias primaryText: primaryButton.text - property alias secondaryText: secondaryButton.text - property string busyPrimaryText: busyMessageText.text - property string busySecondaryText: busyMessageText.text - property string confirmedPrimaryText: confirmedMessageText.text - property string confirmedSecondaryText: confirmedMessageText.text - property bool busy - property bool confirmed + property string button_style + property string primaryText + property string secondaryText + property string busyPrimaryText + property string busySecondaryText + property string confirmedPrimaryText + property string confirmedSecondaryText property bool confirmedTextChoice: true signal clicked(bool primary_action) - Cura.PrimaryButton + property Component primaryButton: Component { - id: primaryButton - enabled: manageButton.enabled - - onClicked: + Cura.PrimaryButton { - busyMessage.text = manageButton.busyPrimaryText - confirmedMessage.text = manageButton.confirmedPrimaryText - manageButton.clicked(true) + id: primaryButton + enabled: manageButton.enabled + text: manageButton.primaryText + + onClicked: + { + manageButton.confirmedTextChoice = true + manageButton.clicked(true) + } } } - Cura.SecondaryButton + property Component secondaryButton: Component { - id: secondaryButton - enabled: manageButton.enabled - - onClicked: + Cura.SecondaryButton { - busyMessage.text = manageButton.busySecondaryText - confirmedMessage.text = manageButton.confirmedSecondaryText - manageButton.clicked(false) + id: secondaryButton + enabled: manageButton.enabled + text: manageButton.secondaryText + + onClicked: + { + manageButton.confirmedTextChoice = false + manageButton.clicked(false) + } } } - Item + property Component busyButton: Component { - id: busyMessage - property alias text: busyMessageText.text - height: UM.Theme.getSize("action_button").height - width: childrenRect.width - - UM.RecolorImage + Item { - id: busyIndicator - width: height - anchors.left: parent.left - anchors.top: parent.top - anchors.topMargin: UM.Theme.getSize("narrow_margin").height - anchors.bottom: parent.bottom - anchors.bottomMargin: anchors.topMargin + id: busyMessage + height: UM.Theme.getSize("action_button").height + width: childrenRect.width - source: UM.Theme.getIcon("Spinner") - color: UM.Theme.getColor("primary") - - RotationAnimator + UM.RecolorImage { - target: busyIndicator - running: busyMessage.visible - from: 0 - to: 360 - loops: Animation.Infinite - duration: 2500 - } - } - Label - { - id: busyMessageText - anchors.left: busyIndicator.right - anchors.leftMargin: UM.Theme.getSize("narrow_margin").width - anchors.verticalCenter: parent.verticalCenter + id: busyIndicator + width: height + anchors.left: parent.left + anchors.top: parent.top + anchors.topMargin: UM.Theme.getSize("narrow_margin").height + anchors.bottom: parent.bottom + anchors.bottomMargin: anchors.topMargin - font: UM.Theme.getFont("medium_bold") - color: UM.Theme.getColor("primary") + source: UM.Theme.getIcon("Spinner") + color: UM.Theme.getColor("primary") + + RotationAnimator + { + target: busyIndicator + running: busyMessage.visible + from: 0 + to: 360 + loops: Animation.Infinite + duration: 2500 + } + } + Label + { + id: busyMessageText + anchors.left: busyIndicator.right + anchors.leftMargin: UM.Theme.getSize("narrow_margin").width + anchors.verticalCenter: parent.verticalCenter + text: manageButton.busyMessageText + + font: UM.Theme.getFont("medium_bold") + color: UM.Theme.getColor("primary") + } } } - Item + property Component confirmButton: Component { - id: confirmedMessage - property alias text: confirmedMessageText.text + Item + { - height: UM.Theme.getSize("action_button").height - width: childrenRect.width + height: UM.Theme.getSize("action_button").height + width: childrenRect.width - Label - { - id: confirmedMessageText - anchors.verticalCenter: parent.verticalCenter - - font: UM.Theme.getFont("medium_bold") - color: UM.Theme.getColor("primary") - } - } - - states: - [ - State - { - name: "primary" - PropertyChanges + Label { - target: primaryButton - visible: true - } - PropertyChanges - { - target: secondaryButton - visible: false - } - PropertyChanges - { - target: busyMessage - visible: false - } - PropertyChanges - { - target: confirmedMessage - visible: false - } - }, - State - { - name: "secondary" - PropertyChanges - { - target: primaryButton - visible: false - } - PropertyChanges - { - target: secondaryButton - visible: true - } - PropertyChanges - { - target: busyMessage - visible: false - } - PropertyChanges - { - target: confirmedMessage - visible: false - } - }, - State - { - name: "hidden" - PropertyChanges - { - target: primaryButton - visible: false - } - PropertyChanges - { - target: secondaryButton - visible: false - } - PropertyChanges - { - target: busyMessage - visible: false - } - PropertyChanges - { - target: confirmedMessage - visible: false - } - }, - State - { - name: "busy" - PropertyChanges - { - target: primaryButton - visible: false - } - PropertyChanges - { - target: secondaryButton - visible: false - } - PropertyChanges - { - target: busyMessage - visible: true - } - PropertyChanges - { - target: confirmedMessage - visible: false - } - }, - State - { - name: "confirmed" - PropertyChanges - { - target: primaryButton - visible: false - } - PropertyChanges - { - target: secondaryButton - visible: false - } - PropertyChanges - { - target: busyMessage - visible: false - } - PropertyChanges - { - target: confirmedMessage - visible: true + id: confirmedMessageText + anchors.verticalCenter: parent.verticalCenter text: manageButton.confirmedTextChoice ? manageButton.confirmedPrimaryText : manageButton.confirmedSecondaryText + + font: UM.Theme.getFont("medium_bold") + color: UM.Theme.getColor("primary") } } - ] + } + + Loader + { + sourceComponent: + { + switch (manageButton.button_style) + { + case "primary": + return manageButton.primaryButton; + case "secondary": + return manageButton.secondaryButton; + case "busy": + return manageButton.busyButton; + case "confirmed": + return manageButton.confirmButton; + default: + return; + } + } + } } diff --git a/plugins/Marketplace/resources/qml/PackageCard.qml b/plugins/Marketplace/resources/qml/PackageCard.qml index 1625f6cff3..6893b70e56 100644 --- a/plugins/Marketplace/resources/qml/PackageCard.qml +++ b/plugins/Marketplace/resources/qml/PackageCard.qml @@ -29,6 +29,7 @@ Rectangle width: parent.width height: UM.Theme.getSize("card").height + // card icon Image { id: packageItem @@ -44,6 +45,7 @@ Rectangle source: packageData.iconUrl != "" ? packageData.iconUrl : "../images/placeholder.svg" } + // ColumnLayout { anchors @@ -142,6 +144,7 @@ Rectangle } } + // description Item { id: shortDescription @@ -222,6 +225,7 @@ Rectangle spacing: UM.Theme.getSize("narrow_margin").width + // label "By" Label { id: authorBy @@ -232,6 +236,7 @@ Rectangle color: UM.Theme.getColor("text") } + // clickable author name Cura.TertiaryButton { Layout.fillWidth: true @@ -252,9 +257,7 @@ Rectangle ManageButton { id: enableManageButton - state: !(installManageButton.confirmed || updateManageButton.confirmed) || enableManageButton.confirmed ? packageData.stateManageEnableButton : "hidden" - busy: packageData.enableManageButton == "busy" - confirmed: packageData.enableManageButton == "confirmed" + button_style: !(installManageButton.confirmed || updateManageButton.confirmed) || enableManageButton.confirmed ? packageData.stateManageEnableButton : "hidden" Layout.alignment: Qt.AlignTop primaryText: catalog.i18nc("@button", "Enable") busyPrimaryText: catalog.i18nc("@button", "Enabling...") @@ -280,9 +283,7 @@ Rectangle ManageButton { id: installManageButton - state: (root.manageableInListView || installManageButton.confirmed) && !(enableManageButton.confirmed || updateManageButton.confirmed) ? packageData.stateManageInstallButton : "hidden" - busy: packageData.stateManageInstallButton == "busy" - confirmed: packageData.stateManageInstallButton == "confirmed" + button_style: (root.manageableInListView || installManageButton.confirmed) && !(enableManageButton.confirmed || updateManageButton.confirmed) ? packageData.stateManageInstallButton : "hidden" Layout.alignment: Qt.AlignTop primaryText: catalog.i18nc("@button", "Install") busyPrimaryText: catalog.i18nc("@button", "Installing...") @@ -309,9 +310,7 @@ Rectangle ManageButton { id: updateManageButton - state: (root.manageableInListView) && (!installManageButton.confirmed || updateManageButton.confirmed) ? packageData.stateManageUpdateButton : "hidden" - busy: packageData.stateManageUpdateButton == "busy" - confirmed: packageData.stateManageUpdateButton == "confirmed" + button_style: (root.manageableInListView) && (!installManageButton.confirmed || updateManageButton.confirmed) ? packageData.stateManageUpdateButton : "hidden" Layout.alignment: Qt.AlignTop primaryText: catalog.i18nc("@button", "Update") busyPrimaryText: catalog.i18nc("@button", "Updating...") diff --git a/plugins/Marketplace/resources/qml/PackagePage.qml b/plugins/Marketplace/resources/qml/PackagePage.qml index 1669f8ffc7..5529373ba3 100644 --- a/plugins/Marketplace/resources/qml/PackagePage.qml +++ b/plugins/Marketplace/resources/qml/PackagePage.qml @@ -207,9 +207,7 @@ Rectangle ManageButton { id: enableManageButton - state: !(installManageButton.confirmed || updateManageButton.confirmed) || enableManageButton.confirmed ? packageData.stateManageEnableButton : "hidden" - busy: packageData.enableManageButton == "busy" - confirmed: packageData.enableManageButton == "confirmed" + button_style: !(installManageButton.confirmed || updateManageButton.confirmed) || enableManageButton.confirmed ? packageData.stateManageEnableButton : "hidden" Layout.alignment: Qt.AlignTop primaryText: catalog.i18nc("@button", "Enable") busyPrimaryText: catalog.i18nc("@button", "Enabling...") @@ -235,9 +233,7 @@ Rectangle ManageButton { id: installManageButton - state: !(enableManageButton.confirmed || updateManageButton.confirmed) ? packageData.stateManageInstallButton : "hidden" - busy: packageData.stateManageInstallButton == "busy" - confirmed: packageData.stateManageInstallButton == "confirmed" + button_style: !(enableManageButton.confirmed || updateManageButton.confirmed) ? packageData.stateManageInstallButton : "hidden" Layout.alignment: Qt.AlignTop primaryText: catalog.i18nc("@button", "Install") busyPrimaryText: catalog.i18nc("@button", "Installing...") @@ -264,9 +260,7 @@ Rectangle ManageButton { id: updateManageButton - state: !installManageButton.confirmed || updateManageButton.confirmed ? packageData.stateManageUpdateButton : "hidden" - busy: packageData.stateManageUpdateButton == "busy" - confirmed: packageData.stateManageUpdateButton == "confirmed" + button_style: !installManageButton.confirmed || updateManageButton.confirmed ? packageData.stateManageUpdateButton : "hidden" Layout.alignment: Qt.AlignTop primaryText: catalog.i18nc("@button", "Update") busyPrimaryText: catalog.i18nc("@button", "Updating...") From e0ca0d5446ace8495487bc98fd46af05b229c98d Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Wed, 8 Dec 2021 14:46:34 +0100 Subject: [PATCH 123/195] Renamed recently_install to better depict the usage This flag is an indication if the package. was recently un-/installed Contributes to: CURA-8587 --- plugins/Marketplace/LocalPackageList.py | 2 +- plugins/Marketplace/PackageModel.py | 18 +++++++++--------- plugins/Marketplace/RemotePackageList.py | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/plugins/Marketplace/LocalPackageList.py b/plugins/Marketplace/LocalPackageList.py index 6fb9cfbf34..b9f3253b85 100644 --- a/plugins/Marketplace/LocalPackageList.py +++ b/plugins/Marketplace/LocalPackageList.py @@ -68,7 +68,7 @@ class LocalPackageList(PackageList): self._connectManageButtonSignals(package) package.can_downgrade = self._manager.canDowngrade(package_id) if package_id in self._manager.getPackagesToRemove() or package_id in self._manager.getPackagesToInstall(): - package.is_recently_installed = True + package.installation_status_changed = True return package def checkForUpdates(self, packages: List[Dict[str, Any]]): diff --git a/plugins/Marketplace/PackageModel.py b/plugins/Marketplace/PackageModel.py index b1b6db733d..fbc998b5c9 100644 --- a/plugins/Marketplace/PackageModel.py +++ b/plugins/Marketplace/PackageModel.py @@ -70,7 +70,7 @@ class PackageModel(QObject): self._icon_url = author_data.get("icon_url", "") self._is_installing: ManageState = ManageState.HALTED - self._is_recently_installed = False + self._installation_status_changed = False self._is_recently_updated = False self._is_recently_enabled = False @@ -338,7 +338,7 @@ class PackageModel(QObject): """The state of the Manage Install package card""" if self._is_installing == ManageState.PROCESSING: return "busy" - if self._is_recently_installed: + if self._installation_status_changed: return "confirmed" if self._is_installed: if self._is_bundled and not self._can_downgrade: @@ -358,17 +358,17 @@ class PackageModel(QObject): if value != self._is_installing: self._is_installing = value if value == ManageState.HALTED: - self._is_recently_installed = True + self._installation_status_changed = True self.stateManageButtonChanged.emit() @property - def is_recently_installed(self): - return self._is_recently_installed + def installation_status_changed(self): + return self._installation_status_changed - @is_recently_installed.setter - def is_recently_installed(self, value): - if value != self._is_recently_installed: - self._is_recently_installed = value + @installation_status_changed.setter + def installation_status_changed(self, value): + if value != self._installation_status_changed: + self._installation_status_changed = value self.stateManageButtonChanged.emit() @pyqtProperty(bool, notify = stateManageButtonChanged) diff --git a/plugins/Marketplace/RemotePackageList.py b/plugins/Marketplace/RemotePackageList.py index d20cb5f4b0..7228507bf5 100644 --- a/plugins/Marketplace/RemotePackageList.py +++ b/plugins/Marketplace/RemotePackageList.py @@ -124,7 +124,7 @@ class RemotePackageList(PackageList): package = PackageModel(package_data, parent = self) self._connectManageButtonSignals(package) if package_id in self._manager.getPackagesToRemove() or package_id in self._manager.getPackagesToInstall(): - package.is_recently_installed = True + package.installation_status_changed = True self.appendItem({"package": package}) # Add it to this list model. except RuntimeError: # Setting the ownership of this object to not qml can still result in a RuntimeError. Which can occur when quickly toggling From df0c502961320bb4ed38935824473b511fe2e803 Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Wed, 8 Dec 2021 14:48:47 +0100 Subject: [PATCH 124/195] Removed redundant pyQtSlots Contributes to: CURA-8587 --- plugins/Marketplace/PackageList.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/plugins/Marketplace/PackageList.py b/plugins/Marketplace/PackageList.py index f7dcd29feb..21c1da004b 100644 --- a/plugins/Marketplace/PackageList.py +++ b/plugins/Marketplace/PackageList.py @@ -279,7 +279,6 @@ class PackageList(ListModel): package.enablePackageTriggered.connect(self.enablePackage) package.disablePackageTriggered.connect(self.disablePackage) - @pyqtSlot(str) def installPackage(self, package_id: str) -> None: """Install a package from the Marketplace @@ -290,7 +289,6 @@ class PackageList(ListModel): url = package.download_url self.download(package_id, url, False) - @pyqtSlot(str) def uninstallPackage(self, package_id: str) -> None: """Uninstall a package from the Marketplace @@ -302,7 +300,6 @@ class PackageList(ListModel): self.unsunscribeUserFromPackage(package_id) package.is_installing = ManageState.HALTED - @pyqtSlot(str) def updatePackage(self, package_id: str) -> None: """Update a package from the Marketplace @@ -314,7 +311,6 @@ class PackageList(ListModel): url = package.download_url self.download(package_id, url, True) - @pyqtSlot(str) def enablePackage(self, package_id: str) -> None: """Enable a package in the plugin registry @@ -326,7 +322,6 @@ class PackageList(ListModel): package.is_active = True package.is_enabling = ManageState.HALTED - @pyqtSlot(str) def disablePackage(self, package_id: str) -> None: """Disable a package in the plugin registry From c72fd12ea2b44b1380b3af372e9c96a0bfb4ea88 Mon Sep 17 00:00:00 2001 From: casper Date: Wed, 8 Dec 2021 15:50:00 +0100 Subject: [PATCH 125/195] Make ManageButton a reusable Component cura 8734 --- .../resources/qml/ManageButton.qml | 30 +++--- .../Marketplace/resources/qml/PackageCard.qml | 95 ++++++++++++++----- .../Marketplace/resources/qml/PackagePage.qml | 95 ++++++++++++++----- 3 files changed, 158 insertions(+), 62 deletions(-) diff --git a/plugins/Marketplace/resources/qml/ManageButton.qml b/plugins/Marketplace/resources/qml/ManageButton.qml index 0614e90c87..7843805e26 100644 --- a/plugins/Marketplace/resources/qml/ManageButton.qml +++ b/plugins/Marketplace/resources/qml/ManageButton.qml @@ -8,17 +8,13 @@ import QtQuick.Layouts 1.1 import UM 1.6 as UM import Cura 1.6 as Cura -RowLayout +Item { id: manageButton property string button_style - property string primaryText - property string secondaryText - property string busyPrimaryText - property string busySecondaryText - property string confirmedPrimaryText - property string confirmedSecondaryText - property bool confirmedTextChoice: true + property string text + property bool busy + property bool confirmed signal clicked(bool primary_action) @@ -27,12 +23,10 @@ RowLayout Cura.PrimaryButton { id: primaryButton - enabled: manageButton.enabled - text: manageButton.primaryText + text: manageButton.text onClicked: { - manageButton.confirmedTextChoice = true manageButton.clicked(true) } } @@ -43,12 +37,10 @@ RowLayout Cura.SecondaryButton { id: secondaryButton - enabled: manageButton.enabled - text: manageButton.secondaryText + text: manageButton.text onClicked: { - manageButton.confirmedTextChoice = false manageButton.clicked(false) } } @@ -59,8 +51,6 @@ RowLayout Item { id: busyMessage - height: UM.Theme.getSize("action_button").height - width: childrenRect.width UM.RecolorImage { @@ -91,7 +81,7 @@ RowLayout anchors.left: busyIndicator.right anchors.leftMargin: UM.Theme.getSize("narrow_margin").width anchors.verticalCenter: parent.verticalCenter - text: manageButton.busyMessageText + text: manageButton.text font: UM.Theme.getFont("medium_bold") color: UM.Theme.getColor("primary") @@ -111,7 +101,7 @@ RowLayout { id: confirmedMessageText anchors.verticalCenter: parent.verticalCenter - text: manageButton.confirmedTextChoice ? manageButton.confirmedPrimaryText : manageButton.confirmedSecondaryText + text: manageButton.text font: UM.Theme.getFont("medium_bold") color: UM.Theme.getColor("primary") @@ -119,8 +109,12 @@ RowLayout } } + height: UM.Theme.getSize("action_button").height + width: childrenRect.width + Loader { + sourceComponent: { switch (manageButton.button_style) diff --git a/plugins/Marketplace/resources/qml/PackageCard.qml b/plugins/Marketplace/resources/qml/PackageCard.qml index 6893b70e56..5c986c2e6e 100644 --- a/plugins/Marketplace/resources/qml/PackageCard.qml +++ b/plugins/Marketplace/resources/qml/PackageCard.qml @@ -257,15 +257,34 @@ Rectangle ManageButton { id: enableManageButton - button_style: !(installManageButton.confirmed || updateManageButton.confirmed) || enableManageButton.confirmed ? packageData.stateManageEnableButton : "hidden" + visible: !(installManageButton.confirmed || updateManageButton.confirmed) || enableManageButton.confirmed + button_style: packageData.stateManageEnableButton Layout.alignment: Qt.AlignTop - primaryText: catalog.i18nc("@button", "Enable") - busyPrimaryText: catalog.i18nc("@button", "Enabling...") - confirmedPrimaryText: catalog.i18nc("@button", "Enabled") - secondaryText: catalog.i18nc("@button", "Disable") - busySecondaryText: catalog.i18nc("@button", "Disabling...") - confirmedSecondaryText: catalog.i18nc("@button", "Disabled") - enabled: !(installManageButton.busy || updateManageButton.busy) + busy: packageData.enableManageButton == "busy" + confirmed: packageData.enableManageButton == "confirmed" + text: { + switch (packageData.stateManageEnableButton) { + case "primary": + return catalog.i18nc("@button", "Enable"); + case "secondary": + return catalog.i18nc("@button", "Disable"); + case "busy": + if (packageData.installationStatus) { + return catalog.i18nc("@button", "Enabling..."); + } else { + return catalog.i18nc("@button", "Disabling..."); + } + case "confirmed": + if (packageData.installationStatus) { + return catalog.i18nc("@button", "Enabled"); + } else { + return catalog.i18nc("@button", "Disabled"); + } + default: + return ""; + } + } + enabled: !installManageButton.busy && !updateManageButton.busy onClicked: { @@ -283,16 +302,34 @@ Rectangle ManageButton { id: installManageButton - button_style: (root.manageableInListView || installManageButton.confirmed) && !(enableManageButton.confirmed || updateManageButton.confirmed) ? packageData.stateManageInstallButton : "hidden" + visible: (root.manageableInListView || installManageButton.confirmed) && !(enableManageButton.confirmed || updateManageButton.confirmed) + button_style: packageData.stateManageInstallButton + busy: packageData.stateManageInstallButton == "busy" + confirmed: packageData.stateManageInstallButton == "confirmed" Layout.alignment: Qt.AlignTop - primaryText: catalog.i18nc("@button", "Install") - busyPrimaryText: catalog.i18nc("@button", "Installing...") - confirmedPrimaryText: catalog.i18nc("@button", "Installed") - secondaryText: catalog.i18nc("@button", "Uninstall") - busySecondaryText: catalog.i18nc("@button", "Uninstalling...") - confirmedSecondaryText: catalog.i18nc("@button", "Uninstalled") - confirmedTextChoice: packageData.installationStatus - enabled: !(enableManageButton.busy || updateManageButton.busy) + text: { + switch (packageData.stateManageInstallButton) { + case "primary": + return catalog.i18nc("@button", "Install"); + case "secondary": + return catalog.i18nc("@button", "Uninstall"); + case "busy": + if (packageData.installationStatus) { + return catalog.i18nc("@button", "Installing..."); + } else { + return catalog.i18nc("@button", "Uninstalling..."); + } + case "confirmed": + if (packageData.installationStatus) { + return catalog.i18nc("@button", "Installed"); + } else { + return catalog.i18nc("@button", "Uninstalled"); + } + default: + return ""; + } + } + enabled: !enableManageButton.busy && !updateManageButton.busy onClicked: { @@ -310,12 +347,26 @@ Rectangle ManageButton { id: updateManageButton - button_style: (root.manageableInListView) && (!installManageButton.confirmed || updateManageButton.confirmed) ? packageData.stateManageUpdateButton : "hidden" + visible: (root.manageableInListView) && (!installManageButton.confirmed || updateManageButton.confirmed) + + button_style: packageData.stateManageUpdateButton + busy: packageData.stateManageUpdateButton == "busy" + confirmed: packageData.stateManageUpdateButton == "confirmed" Layout.alignment: Qt.AlignTop - primaryText: catalog.i18nc("@button", "Update") - busyPrimaryText: catalog.i18nc("@button", "Updating...") - confirmedPrimaryText: catalog.i18nc("@button", "Updated") - enabled: !(installManageButton.busy || enableManageButton.busy) + enabled: !installManageButton.busy && !enableManageButton.busy + + text: { + switch (packageData.stateManageInstallButton) { + case "primary": + return catalog.i18nc("@button", "Update"); + case "busy": + return catalog.i18nc("@button", "Updating..."); + case "confirmed": + return catalog.i18nc("@button", "Updated"); + default: + return ""; + } + } onClicked: packageData.updatePackageTriggered(packageData.packageId) } diff --git a/plugins/Marketplace/resources/qml/PackagePage.qml b/plugins/Marketplace/resources/qml/PackagePage.qml index 5529373ba3..fca25c4022 100644 --- a/plugins/Marketplace/resources/qml/PackagePage.qml +++ b/plugins/Marketplace/resources/qml/PackagePage.qml @@ -207,15 +207,34 @@ Rectangle ManageButton { id: enableManageButton - button_style: !(installManageButton.confirmed || updateManageButton.confirmed) || enableManageButton.confirmed ? packageData.stateManageEnableButton : "hidden" + visible: !(installManageButton.confirmed || updateManageButton.confirmed) || enableManageButton.confirmed + button_style: packageData.stateManageEnableButton Layout.alignment: Qt.AlignTop - primaryText: catalog.i18nc("@button", "Enable") - busyPrimaryText: catalog.i18nc("@button", "Enabling...") - confirmedPrimaryText: catalog.i18nc("@button", "Enabled") - secondaryText: catalog.i18nc("@button", "Disable") - busySecondaryText: catalog.i18nc("@button", "Disabling...") - confirmedSecondaryText: catalog.i18nc("@button", "Disabled") - enabled: !(installManageButton.busy || updateManageButton.busy) + busy: packageData.enableManageButton == "busy" + confirmed: packageData.enableManageButton == "confirmed" + text: { + switch (packageData.stateManageEnableButton) { + case "primary": + return catalog.i18nc("@button", "Enable"); + case "secondary": + return catalog.i18nc("@button", "Disable"); + case "busy": + if (packageData.installationStatus) { + return catalog.i18nc("@button", "Enabling..."); + } else { + return catalog.i18nc("@button", "Disabling..."); + } + case "confirmed": + if (packageData.installationStatus) { + return catalog.i18nc("@button", "Enabled"); + } else { + return catalog.i18nc("@button", "Disabled"); + } + default: + return ""; + } + } + enabled: !installManageButton.busy && !updateManageButton.busy onClicked: { @@ -233,16 +252,34 @@ Rectangle ManageButton { id: installManageButton - button_style: !(enableManageButton.confirmed || updateManageButton.confirmed) ? packageData.stateManageInstallButton : "hidden" + visible: !(enableManageButton.confirmed || updateManageButton.confirmed) + button_style: packageData.stateManageInstallButton + busy: packageData.stateManageInstallButton == "busy" + confirmed: packageData.stateManageInstallButton == "confirmed" Layout.alignment: Qt.AlignTop - primaryText: catalog.i18nc("@button", "Install") - busyPrimaryText: catalog.i18nc("@button", "Installing...") - confirmedPrimaryText: catalog.i18nc("@button", "Installed") - secondaryText: catalog.i18nc("@button", "Uninstall") - busySecondaryText: catalog.i18nc("@button", "Uninstalling...") - confirmedSecondaryText: catalog.i18nc("@button", "Uninstalled") - confirmedTextChoice: packageData.isRecentlyInstalled - enabled: !(enableManageButton.busy || updateManageButton.busy) + text: { + switch (packageData.stateManageInstallButton) { + case "primary": + return catalog.i18nc("@button", "Install"); + case "secondary": + return catalog.i18nc("@button", "Uninstall"); + case "busy": + if (packageData.installationStatus) { + return catalog.i18nc("@button", "Installing..."); + } else { + return catalog.i18nc("@button", "Uninstalling..."); + } + case "confirmed": + if (packageData.installationStatus) { + return catalog.i18nc("@button", "Installed"); + } else { + return catalog.i18nc("@button", "Uninstalled"); + } + default: + return ""; + } + } + enabled: !enableManageButton.busy && !updateManageButton.busy onClicked: { @@ -260,12 +297,26 @@ Rectangle ManageButton { id: updateManageButton - button_style: !installManageButton.confirmed || updateManageButton.confirmed ? packageData.stateManageUpdateButton : "hidden" + visible: !installManageButton.confirmed || updateManageButton.confirmed + + button_style: packageData.stateManageUpdateButton + busy: packageData.stateManageUpdateButton == "busy" + confirmed: packageData.stateManageUpdateButton == "confirmed" Layout.alignment: Qt.AlignTop - primaryText: catalog.i18nc("@button", "Update") - busyPrimaryText: catalog.i18nc("@button", "Updating...") - confirmedPrimaryText: catalog.i18nc("@button", "Updated") - enabled: !(installManageButton.busy || enableManageButton.busy) + enabled: !installManageButton.busy && !enableManageButton.busy + + text: { + switch (packageData.stateManageInstallButton) { + case "primary": + return catalog.i18nc("@button", "Update"); + case "busy": + return catalog.i18nc("@button", "Updating..."); + case "confirmed": + return catalog.i18nc("@button", "Updated"); + default: + return ""; + } + } onClicked: packageData.updatePackageTriggered(packageData.packageId) } From e4d469b6a1404d7e575995605ae171aa56fd307d Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Wed, 8 Dec 2021 16:13:32 +0100 Subject: [PATCH 126/195] Added some typing annotation Contributes to: CURA-8587 --- plugins/Marketplace/RestartManager.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/plugins/Marketplace/RestartManager.py b/plugins/Marketplace/RestartManager.py index 5b66ca0292..3fa6ada797 100644 --- a/plugins/Marketplace/RestartManager.py +++ b/plugins/Marketplace/RestartManager.py @@ -2,17 +2,18 @@ # Cura is released under the terms of the LGPLv3 or higher. from typing import Optional, TYPE_CHECKING -from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal +from PyQt5.QtCore import pyqtProperty, pyqtSignal from cura.CuraApplication import CuraApplication if TYPE_CHECKING: + from PyQt5.QtCore import QObject from UM.PluginRegistry import PluginRegistry from cura.CuraPackageManager import CuraPackageManager class RestartManager(QObject): - def __init__(self, parent: Optional[QObject] = None): + def __init__(self, parent: Optional[QObject] = None) -> None: super().__init__(parent = parent) self._manager: "CuraPackageManager" = CuraApplication.getInstance().getPackageManager() self._plugin_registry: "PluginRegistry" = CuraApplication.getInstance().getPluginRegistry() @@ -22,7 +23,7 @@ class RestartManager(QObject): self._restart_needed = False - def checkIfRestartNeeded(self): + def checkIfRestartNeeded(self) -> None: if self._manager.hasPackagesToRemoveOrInstall or len(self._plugin_registry.getCurrentSessionActivationChangedPlugins()) > 0: self._restart_needed = True else: From 4c516e8cec2e2f24f4da10491fee2e819ef3e3cc Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Wed, 8 Dec 2021 16:14:59 +0100 Subject: [PATCH 127/195] Moved QObject out if TYPE_CHECKING statement Contributes to: CURA-8587 --- plugins/Marketplace/RestartManager.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/plugins/Marketplace/RestartManager.py b/plugins/Marketplace/RestartManager.py index 3fa6ada797..19650dd64e 100644 --- a/plugins/Marketplace/RestartManager.py +++ b/plugins/Marketplace/RestartManager.py @@ -2,12 +2,11 @@ # Cura is released under the terms of the LGPLv3 or higher. from typing import Optional, TYPE_CHECKING -from PyQt5.QtCore import pyqtProperty, pyqtSignal +from PyQt5.QtCore import pyqtProperty, pyqtSignal, QObject from cura.CuraApplication import CuraApplication if TYPE_CHECKING: - from PyQt5.QtCore import QObject from UM.PluginRegistry import PluginRegistry from cura.CuraPackageManager import CuraPackageManager From 9f41115bc144f31636b90a2ea5d5d1ce9061baa2 Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Wed, 8 Dec 2021 18:47:56 +0100 Subject: [PATCH 128/195] Rework of the ManageButton Now uses the internal signal Contributes to: CURA-8587 --- plugins/Marketplace/LocalPackageList.py | 4 +- plugins/Marketplace/PackageList.py | 25 +-- plugins/Marketplace/PackageModel.py | 168 ++++++++---------- plugins/Marketplace/RemotePackageList.py | 2 - .../resources/qml/ManageButton.qml | 19 +- .../Marketplace/resources/qml/PackageCard.qml | 106 +++++------ .../Marketplace/resources/qml/PackagePage.qml | 106 +++++------ 7 files changed, 168 insertions(+), 262 deletions(-) diff --git a/plugins/Marketplace/LocalPackageList.py b/plugins/Marketplace/LocalPackageList.py index b9f3253b85..32e60b2518 100644 --- a/plugins/Marketplace/LocalPackageList.py +++ b/plugins/Marketplace/LocalPackageList.py @@ -66,9 +66,7 @@ class LocalPackageList(PackageList): section_title = self.PACKAGE_CATEGORIES[bundled_or_installed][package_type] package = PackageModel(package_info, section_title = section_title, parent = self) self._connectManageButtonSignals(package) - package.can_downgrade = self._manager.canDowngrade(package_id) - if package_id in self._manager.getPackagesToRemove() or package_id in self._manager.getPackagesToInstall(): - package.installation_status_changed = True + package.setCanDowngrade(self._manager.canDowngrade(package_id)) return package def checkForUpdates(self, packages: List[Dict[str, Any]]): diff --git a/plugins/Marketplace/PackageList.py b/plugins/Marketplace/PackageList.py index 21c1da004b..e6e5e78ba9 100644 --- a/plugins/Marketplace/PackageList.py +++ b/plugins/Marketplace/PackageList.py @@ -18,7 +18,7 @@ from cura.CuraApplication import CuraApplication from cura.CuraPackageManager import CuraPackageManager from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope # To make requests to the Ultimaker API with correct authorization. -from .PackageModel import PackageModel, ManageState +from .PackageModel import PackageModel from .Constants import USER_PACKAGES_URL if TYPE_CHECKING: @@ -166,7 +166,6 @@ class PackageList(ListModel): dialog.deleteLater() # reset package card package = self.getPackageModel(package_id) - package.is_installing = ManageState.FAILED def _requestInstall(self, package_id: str, update: bool = False) -> None: package_path = self._to_install[package_id] @@ -184,12 +183,8 @@ class PackageList(ListModel): package_path = self._to_install.pop(package_id) to_be_installed = self._manager.installPackage(package_path) is not None package = self.getPackageModel(package_id) - if package.can_update and to_be_installed: - package.can_update = False - if update: - package.is_updating = ManageState.HALTED - else: - package.is_installing = ManageState.HALTED + # TODO handle failure + package.isRecentlyInstalledChanged.emit(update) self.subscribeUserToPackage(package_id, str(package.sdk_version)) def download(self, package_id: str, url: str, update: bool = False) -> None: @@ -239,10 +234,7 @@ class PackageList(ListModel): Logger.error(f"Failed to download package: {package_id} due to {reply_string}") try: package = self.getPackageModel(package_id) - if update: - package.is_updating = ManageState.FAILED - else: - package.is_installing = ManageState.FAILED + # TODO: handle error except RuntimeError: # Setting the ownership of this object to not qml can still result in a RuntimeError. Which can occur when quickly toggling # between de-/constructing Remote or Local PackageLists. This try-except is here to prevent a hard crash when the wrapped C++ object @@ -285,7 +277,6 @@ class PackageList(ListModel): :param package_id: the package identification string """ package = self.getPackageModel(package_id) - package.is_installing = ManageState.PROCESSING url = package.download_url self.download(package_id, url, False) @@ -295,10 +286,9 @@ class PackageList(ListModel): :param package_id: the package identification string """ package = self.getPackageModel(package_id) - package.is_installing = ManageState.PROCESSING self._manager.removePackage(package_id) self.unsunscribeUserFromPackage(package_id) - package.is_installing = ManageState.HALTED + package.isRecentlyInstalledChanged.emit(False) def updatePackage(self, package_id: str) -> None: """Update a package from the Marketplace @@ -306,7 +296,6 @@ class PackageList(ListModel): :param package_id: the package identification string """ package = self.getPackageModel(package_id) - package.is_updating = ManageState.PROCESSING self._manager.removePackage(package_id, force_add = True) url = package.download_url self.download(package_id, url, True) @@ -317,10 +306,8 @@ class PackageList(ListModel): :param package_id: the package identification string """ package = self.getPackageModel(package_id) - package.is_enabling = ManageState.PROCESSING self._plugin_registry.enablePlugin(package_id) package.is_active = True - package.is_enabling = ManageState.HALTED def disablePackage(self, package_id: str) -> None: """Disable a package in the plugin registry @@ -328,7 +315,5 @@ class PackageList(ListModel): :param package_id: the package identification string """ package = self.getPackageModel(package_id) - package.is_enabling = ManageState.PROCESSING self._plugin_registry.disablePlugin(package_id) package.is_active = False - package.is_enabling = ManageState.HALTED diff --git a/plugins/Marketplace/PackageModel.py b/plugins/Marketplace/PackageModel.py index fbc998b5c9..93d41187e1 100644 --- a/plugins/Marketplace/PackageModel.py +++ b/plugins/Marketplace/PackageModel.py @@ -13,13 +13,6 @@ from UM.i18n import i18nCatalog # To translate placeholder names if data is not catalog = i18nCatalog("cura") - -class ManageState(Enum): - PROCESSING = 1 - HALTED = 0 - FAILED = -1 - - class PackageModel(QObject): """ Represents a package, containing all the relevant information to be displayed about a package. @@ -69,19 +62,45 @@ class PackageModel(QObject): if not self._icon_url or self._icon_url == "": self._icon_url = author_data.get("icon_url", "") - self._is_installing: ManageState = ManageState.HALTED - self._installation_status_changed = False + self._is_installing = False + self._install_status_changing = False + self._is_recently_installed = False self._is_recently_updated = False - self._is_recently_enabled = False self._can_update = False - self._is_updating: ManageState = ManageState.HALTED - self._is_enabling: ManageState = ManageState.HALTED + self._is_updating = False self._can_downgrade = False self._section_title = section_title self.sdk_version = package_data.get("sdk_version_semver", "") # Note that there's a lot more info in the package_data than just these specified here. + def install_clicked(package_id): + self._install_status_changing = True + self.setIsInstalling(True) + + self.installPackageTriggered.connect(install_clicked) + + def uninstall_clicked(package_id): + self._install_status_changing = False + self.setIsInstalling(True) + + self.uninstallPackageTriggered.connect(uninstall_clicked) + + def update_clicked(package_id): + self.setIsUpdating(True) + + self.updatePackageTriggered.connect(update_clicked) + + def finished_installed(is_updating): + if is_updating: + self._is_recently_installed = True + self.setIsUpdating(False) + else: + self._is_recently_updated + self.setIsInstalling(False) + + self.isRecentlyInstalledChanged.connect(finished_installed) + def __eq__(self, other: object): if isinstance(other, PackageModel): return other == self @@ -278,6 +297,10 @@ class PackageModel(QObject): def isCompatibleAirManager(self) -> bool: return self._is_compatible_air_manager + @pyqtProperty(bool, constant = True) + def isBundled(self) -> bool: + return self._is_bundled + # --- manage buttons signals --- stateManageButtonChanged = pyqtSignal() @@ -292,33 +315,14 @@ class PackageModel(QObject): disablePackageTriggered = pyqtSignal(str) - recentlyInstalledChanged = pyqtSignal(bool) + isRecentlyInstalledChanged = pyqtSignal(bool) # --- enabling --- - @pyqtProperty(str, notify = stateManageButtonChanged) - def stateManageEnableButton(self) -> str: + @pyqtProperty(bool, notify = stateManageButtonChanged) + def stateManageEnableButton(self) -> bool: """The state of the manage Enable Button of this package""" - if self._is_enabling == ManageState.PROCESSING: - return "busy" - if self._package_type == "material" or not self._is_installed: - return "hidden" - if self._is_installed and self._is_active: - return "secondary" - return "primary" - - @property - def is_enabling(self) -> ManageState: - """Flag if the package is being enabled/disabled""" - return self._is_enabling - - @is_enabling.setter - def is_enabling(self, value: ManageState) -> None: - if value != self._is_enabling: - self._is_enabling = value - if value == ManageState.HALTED: - self._is_recently_enabled = True - self.stateManageButtonChanged.emit() + return not (self._is_installed and self._is_active) @property def is_active(self) -> bool: @@ -333,85 +337,67 @@ class PackageModel(QObject): # --- Installing --- - @pyqtProperty(str, notify = stateManageButtonChanged) - def stateManageInstallButton(self) -> str: + @pyqtProperty(bool, notify = stateManageButtonChanged) + def stateManageInstallButton(self) -> bool: """The state of the Manage Install package card""" - if self._is_installing == ManageState.PROCESSING: - return "busy" - if self._installation_status_changed: - return "confirmed" - if self._is_installed: - if self._is_bundled and not self._can_downgrade: - return "hidden" - else: - return "secondary" - else: - return "primary" + return not self._is_installed - @property - def is_installing(self) -> ManageState: - """Flag is we're currently installing, when setting this to ``None`` in indicates a failed installation""" - return self._is_installing - - @is_installing.setter - def is_installing(self, value: ManageState) -> None: + def setIsInstalling(self, value: bool) -> None: if value != self._is_installing: self._is_installing = value - if value == ManageState.HALTED: - self._installation_status_changed = True self.stateManageButtonChanged.emit() - @property - def installation_status_changed(self): - return self._installation_status_changed + @pyqtProperty(bool, fset = setIsInstalling, notify = stateManageButtonChanged) + def isInstalling(self) -> bool: + return self._is_installing - @installation_status_changed.setter - def installation_status_changed(self, value): - if value != self._installation_status_changed: - self._installation_status_changed = value + def setInstallStatusChanging(self, value: bool) -> None: + if value != self._install_status_changing: + self._install_status_changing = value self.stateManageButtonChanged.emit() + @pyqtProperty(bool, fset = setInstallStatusChanging, notify = stateManageButtonChanged) + def installStatusChanging(self) -> bool: + return self._install_status_changing + @pyqtProperty(bool, notify = stateManageButtonChanged) - def installationStatus(self): + def isInstalled(self) -> bool: return self._package_id in CuraApplication.getInstance().getPackageManager().getPackagesToInstall() - @property - def can_downgrade(self) -> bool: - """Flag if the installed package can be downgraded to a bundled version""" - return self._can_downgrade + @pyqtProperty(bool, notify = stateManageButtonChanged) + def isUninstalled(self) -> bool: + return self._package_id in CuraApplication.getInstance().getPackageManager().getPackagesToRemove() - @can_downgrade.setter - def can_downgrade(self, value: bool) -> None: + def setCanDowngrade(self, value: bool) -> None: if value != self._can_downgrade: self._can_downgrade = value self.stateManageButtonChanged.emit() + @pyqtProperty(bool, fset = setCanDowngrade, notify = stateManageButtonChanged) + def canDowngrade(self) -> bool: + """Flag if the installed package can be downgraded to a bundled version""" + return self._can_downgrade + # --- Updating --- - @pyqtProperty(str, notify = stateManageButtonChanged) - def stateManageUpdateButton(self) -> str: - """The state of the manage Update button for this card """ - if self._is_updating == ManageState.PROCESSING: - return "busy" - if self._is_recently_updated: - return "confirmed" - if self._can_update: - return "primary" - return "hidden" - - @property - def is_updating(self) -> ManageState: - """Flag indicating if the package is being updated""" - return self._is_updating - - @is_updating.setter - def is_updating(self, value: ManageState) -> None: + def setIsUpdating(self, value): if value != self._is_updating: self._is_updating = value - if value == ManageState.HALTED: - self._is_recently_updated = True self.stateManageButtonChanged.emit() + @pyqtProperty(bool, fset = setIsUpdating, notify = stateManageButtonChanged) + def isUpdating(self): + return self._is_updating + + def setIsUpdated(self, value): + if value != self._is_recently_updated: + self._is_recently_updated = value + self.stateManageButtonChanged.emit() + + @pyqtProperty(bool, fset = setIsUpdated, notify = stateManageButtonChanged) + def isUpdated(self): + return self._is_recently_updated + @property def can_update(self) -> bool: """Flag indicating if the package can be updated""" diff --git a/plugins/Marketplace/RemotePackageList.py b/plugins/Marketplace/RemotePackageList.py index 7228507bf5..5325fc8640 100644 --- a/plugins/Marketplace/RemotePackageList.py +++ b/plugins/Marketplace/RemotePackageList.py @@ -123,8 +123,6 @@ class RemotePackageList(PackageList): try: package = PackageModel(package_data, parent = self) self._connectManageButtonSignals(package) - if package_id in self._manager.getPackagesToRemove() or package_id in self._manager.getPackagesToInstall(): - package.installation_status_changed = True self.appendItem({"package": package}) # Add it to this list model. except RuntimeError: # Setting the ownership of this object to not qml can still result in a RuntimeError. Which can occur when quickly toggling diff --git a/plugins/Marketplace/resources/qml/ManageButton.qml b/plugins/Marketplace/resources/qml/ManageButton.qml index 7843805e26..2e2ef294d1 100644 --- a/plugins/Marketplace/resources/qml/ManageButton.qml +++ b/plugins/Marketplace/resources/qml/ManageButton.qml @@ -11,7 +11,7 @@ import Cura 1.6 as Cura Item { id: manageButton - property string button_style + property bool button_style property string text property bool busy property bool confirmed @@ -117,19 +117,10 @@ Item sourceComponent: { - switch (manageButton.button_style) - { - case "primary": - return manageButton.primaryButton; - case "secondary": - return manageButton.secondaryButton; - case "busy": - return manageButton.busyButton; - case "confirmed": - return manageButton.confirmButton; - default: - return; - } + if (busy) { return manageButton.busyButton; } + else if (confirmed) { return manageButton.confirmButton; } + else if (manageButton.button_style) { return manageButton.primaryButton; } + else { return manageButton.secondaryButton; } } } } diff --git a/plugins/Marketplace/resources/qml/PackageCard.qml b/plugins/Marketplace/resources/qml/PackageCard.qml index 5c986c2e6e..897f5abca7 100644 --- a/plugins/Marketplace/resources/qml/PackageCard.qml +++ b/plugins/Marketplace/resources/qml/PackageCard.qml @@ -257,34 +257,16 @@ Rectangle ManageButton { id: enableManageButton - visible: !(installManageButton.confirmed || updateManageButton.confirmed) || enableManageButton.confirmed + visible: root.manageableInListView && !(installManageButton.confirmed || updateManageButton.confirmed) + enabled: !(installManageButton.busy || updateManageButton.busy) + + busy: false + confirmed: false + button_style: packageData.stateManageEnableButton Layout.alignment: Qt.AlignTop - busy: packageData.enableManageButton == "busy" - confirmed: packageData.enableManageButton == "confirmed" - text: { - switch (packageData.stateManageEnableButton) { - case "primary": - return catalog.i18nc("@button", "Enable"); - case "secondary": - return catalog.i18nc("@button", "Disable"); - case "busy": - if (packageData.installationStatus) { - return catalog.i18nc("@button", "Enabling..."); - } else { - return catalog.i18nc("@button", "Disabling..."); - } - case "confirmed": - if (packageData.installationStatus) { - return catalog.i18nc("@button", "Enabled"); - } else { - return catalog.i18nc("@button", "Disabled"); - } - default: - return ""; - } - } - enabled: !installManageButton.busy && !updateManageButton.busy + + text: packageData.stateManageEnableButton ? catalog.i18nc("@button", "Enable") : catalog.i18nc("@button", "Disable") onClicked: { @@ -302,34 +284,31 @@ Rectangle ManageButton { id: installManageButton - visible: (root.manageableInListView || installManageButton.confirmed) && !(enableManageButton.confirmed || updateManageButton.confirmed) + visible: (root.manageableInListView || confirmed) && ((packageData.isBundled && packageData.canDowngrade) || !packageData.isBundled || !updateManageButton.confirmed) + + enabled: !packageData.isUpdating + + busy: packageData.isInstalling + confirmed: packageData.isInstalled || packageData.isUninstalled + button_style: packageData.stateManageInstallButton - busy: packageData.stateManageInstallButton == "busy" - confirmed: packageData.stateManageInstallButton == "confirmed" Layout.alignment: Qt.AlignTop - text: { - switch (packageData.stateManageInstallButton) { - case "primary": - return catalog.i18nc("@button", "Install"); - case "secondary": - return catalog.i18nc("@button", "Uninstall"); - case "busy": - if (packageData.installationStatus) { - return catalog.i18nc("@button", "Installing..."); - } else { - return catalog.i18nc("@button", "Uninstalling..."); - } - case "confirmed": - if (packageData.installationStatus) { - return catalog.i18nc("@button", "Installed"); - } else { - return catalog.i18nc("@button", "Uninstalled"); - } - default: - return ""; + + text: + { + if (packageData.stateManageInstallButton) + { + if (packageData.isInstalling) { return catalog.i18nc("@button", "Installing..."); } + else if (packageData.isInstalled) { return catalog.i18nc("@button", "Installed"); } + else { return catalog.i18nc("@button", "Install"); } + } + else + { + if (packageData.isInstalling) { return catalog.i18nc("@button", "Uninstalling..."); } + else if (packageData.isUninstalled) { return catalog.i18nc("@button", "Uninstalled"); } + else { return catalog.i18nc("@button", "Uninstall"); } } } - enabled: !enableManageButton.busy && !updateManageButton.busy onClicked: { @@ -347,25 +326,20 @@ Rectangle ManageButton { id: updateManageButton - visible: (root.manageableInListView) && (!installManageButton.confirmed || updateManageButton.confirmed) + visible: (root.manageableInListView && confirmed) && !installManageButton.confirmed + enabled: !installManageButton.busy - button_style: packageData.stateManageUpdateButton - busy: packageData.stateManageUpdateButton == "busy" - confirmed: packageData.stateManageUpdateButton == "confirmed" + busy: packageData.isUpdating + confirmed: packageData.isUpdated + + button_style: true Layout.alignment: Qt.AlignTop - enabled: !installManageButton.busy && !enableManageButton.busy - text: { - switch (packageData.stateManageInstallButton) { - case "primary": - return catalog.i18nc("@button", "Update"); - case "busy": - return catalog.i18nc("@button", "Updating..."); - case "confirmed": - return catalog.i18nc("@button", "Updated"); - default: - return ""; - } + text: + { + if (packageData.isUpdating) { return catalog.i18nc("@button", "Updating..."); } + else if (packageData.isUpdated) { return catalog.i18nc("@button", "Updated"); } + else { return catalog.i18nc("@button", "Update"); } } onClicked: packageData.updatePackageTriggered(packageData.packageId) diff --git a/plugins/Marketplace/resources/qml/PackagePage.qml b/plugins/Marketplace/resources/qml/PackagePage.qml index fca25c4022..12d24fb812 100644 --- a/plugins/Marketplace/resources/qml/PackagePage.qml +++ b/plugins/Marketplace/resources/qml/PackagePage.qml @@ -207,34 +207,16 @@ Rectangle ManageButton { id: enableManageButton - visible: !(installManageButton.confirmed || updateManageButton.confirmed) || enableManageButton.confirmed + visible: !(installManageButton.confirmed || updateManageButton.confirmed) + + enabled: !(installManageButton.busy || updateManageButton.busy) + busy: false + confirmed: false + button_style: packageData.stateManageEnableButton Layout.alignment: Qt.AlignTop - busy: packageData.enableManageButton == "busy" - confirmed: packageData.enableManageButton == "confirmed" - text: { - switch (packageData.stateManageEnableButton) { - case "primary": - return catalog.i18nc("@button", "Enable"); - case "secondary": - return catalog.i18nc("@button", "Disable"); - case "busy": - if (packageData.installationStatus) { - return catalog.i18nc("@button", "Enabling..."); - } else { - return catalog.i18nc("@button", "Disabling..."); - } - case "confirmed": - if (packageData.installationStatus) { - return catalog.i18nc("@button", "Enabled"); - } else { - return catalog.i18nc("@button", "Disabled"); - } - default: - return ""; - } - } - enabled: !installManageButton.busy && !updateManageButton.busy + + text: packageData.stateManageEnableButton ? catalog.i18nc("@button", "Enable") : catalog.i18nc("@button", "Disable") onClicked: { @@ -252,34 +234,31 @@ Rectangle ManageButton { id: installManageButton - visible: !(enableManageButton.confirmed || updateManageButton.confirmed) + visible: confirmed && ((packageData.isBundled && packageData.canDowngrade) || !packageData.isBundled || !updateManageButton.confirmed) + + enabled: !packageData.isUpdating + + busy: packageData.isInstalling + confirmed: packageData.isInstalled || packageData.isUninstalled + button_style: packageData.stateManageInstallButton - busy: packageData.stateManageInstallButton == "busy" - confirmed: packageData.stateManageInstallButton == "confirmed" Layout.alignment: Qt.AlignTop - text: { - switch (packageData.stateManageInstallButton) { - case "primary": - return catalog.i18nc("@button", "Install"); - case "secondary": - return catalog.i18nc("@button", "Uninstall"); - case "busy": - if (packageData.installationStatus) { - return catalog.i18nc("@button", "Installing..."); - } else { - return catalog.i18nc("@button", "Uninstalling..."); - } - case "confirmed": - if (packageData.installationStatus) { - return catalog.i18nc("@button", "Installed"); - } else { - return catalog.i18nc("@button", "Uninstalled"); - } - default: - return ""; + + text: + { + if (packageData.stateManageInstallButton) + { + if (packageData.isInstalling) { return catalog.i18nc("@button", "Installing..."); } + else if (packageData.isInstalled) { return catalog.i18nc("@button", "Installed"); } + else { return catalog.i18nc("@button", "Install"); } + } + else + { + if (packageData.isInstalling) { return catalog.i18nc("@button", "Uninstalling..."); } + else if (packageData.isUninstalled) { return catalog.i18nc("@button", "Uninstalled"); } + else { return catalog.i18nc("@button", "Uninstall"); } } } - enabled: !enableManageButton.busy && !updateManageButton.busy onClicked: { @@ -297,25 +276,20 @@ Rectangle ManageButton { id: updateManageButton - visible: !installManageButton.confirmed || updateManageButton.confirmed + visible: confirmed && !installManageButton.confirmed + enabled: !installManageButton.busy - button_style: packageData.stateManageUpdateButton - busy: packageData.stateManageUpdateButton == "busy" - confirmed: packageData.stateManageUpdateButton == "confirmed" + busy: packageData.isUpdating + confirmed: packageData.isUpdated + + button_style: true Layout.alignment: Qt.AlignTop - enabled: !installManageButton.busy && !enableManageButton.busy - text: { - switch (packageData.stateManageInstallButton) { - case "primary": - return catalog.i18nc("@button", "Update"); - case "busy": - return catalog.i18nc("@button", "Updating..."); - case "confirmed": - return catalog.i18nc("@button", "Updated"); - default: - return ""; - } + text: + { + if (packageData.isUpdating) { return catalog.i18nc("@button", "Updating..."); } + else if (packageData.isUpdated) { return catalog.i18nc("@button", "Updated"); } + else { return catalog.i18nc("@button", "Update"); } } onClicked: packageData.updatePackageTriggered(packageData.packageId) From d405652db7f511f59657c131e17995c988663db2 Mon Sep 17 00:00:00 2001 From: casper Date: Wed, 8 Dec 2021 19:41:50 +0100 Subject: [PATCH 129/195] Combine dublicated elements from `PackageCard` and `PackagePage` into reusable component `PackageCardHeader` cura 8734 --- .../Marketplace/resources/qml/PackageCard.qml | 390 +++--------------- .../resources/qml/PackageCardHeader.qml | 299 ++++++++++++++ .../Marketplace/resources/qml/PackagePage.qml | 271 +----------- 3 files changed, 362 insertions(+), 598 deletions(-) create mode 100644 plugins/Marketplace/resources/qml/PackageCardHeader.qml diff --git a/plugins/Marketplace/resources/qml/PackageCard.qml b/plugins/Marketplace/resources/qml/PackageCard.qml index 5c986c2e6e..9097417c80 100644 --- a/plugins/Marketplace/resources/qml/PackageCard.qml +++ b/plugins/Marketplace/resources/qml/PackageCard.qml @@ -10,9 +10,8 @@ import Cura 1.6 as Cura Rectangle { - id: root - property var packageData - property bool manageableInListView + property alias packageData: packageCardHeader.packageData + property alias manageableInListView: packageCardHeader.showManageButtons height: childrenRect.height color: UM.Theme.getColor("main_background") @@ -24,352 +23,79 @@ Rectangle spacing: 0 - Item + PackageCardHeader { - width: parent.width - height: UM.Theme.getSize("card").height - - // card icon - Image - { - id: packageItem - anchors - { - top: parent.top - left: parent.left - margins: UM.Theme.getSize("default_margin").width - } - width: UM.Theme.getSize("card_icon").width - height: width - - source: packageData.iconUrl != "" ? packageData.iconUrl : "../images/placeholder.svg" - } - - // - ColumnLayout - { - anchors - { - left: packageItem.right - leftMargin: UM.Theme.getSize("default_margin").width - right: parent.right - rightMargin: UM.Theme.getSize("default_margin").width - top: parent.top - topMargin: UM.Theme.getSize("narrow_margin").height - } - height: packageItem.height + packageItem.anchors.margins * 2 - - // Title row. - RowLayout - { - id: titleBar - Layout.preferredWidth: parent.width - Layout.preferredHeight: childrenRect.height - - Label - { - text: packageData.displayName - font: UM.Theme.getFont("medium_bold") - color: UM.Theme.getColor("text") - verticalAlignment: Text.AlignTop - } - VerifiedIcon - { - enabled: packageData.isCheckedByUltimaker - visible: packageData.isCheckedByUltimaker - } - - - Control - { - Layout.preferredWidth: UM.Theme.getSize("card_tiny_icon").width - Layout.preferredHeight: UM.Theme.getSize("card_tiny_icon").height - Layout.alignment: Qt.AlignCenter - enabled: false // remove! - visible: false // replace packageInfo.XXXXXX - // TODO: waiting for materials card implementation - - Cura.ToolTip - { - tooltipText: "" // TODO - visible: parent.hovered - } - - UM.RecolorImage - { - anchors.fill: parent - - color: UM.Theme.getColor("primary") - source: UM.Theme.getIcon("CheckCircle") // TODO - } - - // onClicked: Qt.openUrlExternally( XXXXXX ) // TODO - } - - Label - { - id: packageVersionLabel - text: packageData.packageVersion - font: UM.Theme.getFont("default") - color: UM.Theme.getColor("text") - Layout.fillWidth: true - } - - Button - { - id: externalLinkButton - - // For some reason if i set padding, they don't match up. If i set all of them explicitly, it does work? - leftPadding: UM.Theme.getSize("narrow_margin").width - rightPadding: UM.Theme.getSize("narrow_margin").width - topPadding: UM.Theme.getSize("narrow_margin").width - bottomPadding: UM.Theme.getSize("narrow_margin").width - - Layout.preferredWidth: UM.Theme.getSize("card_tiny_icon").width + 2 * padding - Layout.preferredHeight: UM.Theme.getSize("card_tiny_icon").width + 2 * padding - contentItem: UM.RecolorImage - { - source: UM.Theme.getIcon("LinkExternal") - color: UM.Theme.getColor("icon") - implicitWidth: UM.Theme.getSize("card_tiny_icon").width - implicitHeight: UM.Theme.getSize("card_tiny_icon").height - } - - background: Rectangle - { - color: externalLinkButton.hovered ? UM.Theme.getColor("action_button_hovered"): "transparent" - radius: externalLinkButton.width / 2 - } - onClicked: Qt.openUrlExternally(packageData.authorInfoUrl) - } - } + id: packageCardHeader // description - Item + Item + { + id: shortDescription + + anchors.fill: parent + + Label { - id: shortDescription - Layout.preferredWidth: parent.width - Layout.fillHeight: true + id: descriptionLabel + width: parent.width + property real lastLineWidth: 0; //Store the width of the last line, to properly position the elision. - Label + text: packageData.description + textFormat: Text.PlainText //Must be plain text, or we won't get onLineLaidOut signals. Don't auto-detect! + font: UM.Theme.getFont("default") + color: UM.Theme.getColor("text") + maximumLineCount: 2 + wrapMode: Text.Wrap + elide: Text.ElideRight + visible: text !== "" + + onLineLaidOut: { - id: descriptionLabel - width: parent.width - property real lastLineWidth: 0; //Store the width of the last line, to properly position the elision. - - text: packageData.description - textFormat: Text.PlainText //Must be plain text, or we won't get onLineLaidOut signals. Don't auto-detect! - font: UM.Theme.getFont("default") - color: UM.Theme.getColor("text") - maximumLineCount: 2 - wrapMode: Text.Wrap - elide: Text.ElideRight - visible: text !== "" - - onLineLaidOut: + if(truncated && line.isLast) { - if(truncated && line.isLast) + let max_line_width = parent.width - readMoreButton.width - fontMetrics.advanceWidth("… ") - 2 * UM.Theme.getSize("default_margin").width; + if(line.implicitWidth > max_line_width) { - let max_line_width = parent.width - readMoreButton.width - fontMetrics.advanceWidth("… ") - 2 * UM.Theme.getSize("default_margin").width; - if(line.implicitWidth > max_line_width) - { - line.width = max_line_width; - } - else - { - line.width = line.implicitWidth - fontMetrics.advanceWidth("…"); //Truncate the ellipsis. We're adding this ourselves. - } - descriptionLabel.lastLineWidth = line.implicitWidth; + line.width = max_line_width; } + else + { + line.width = line.implicitWidth - fontMetrics.advanceWidth("…"); //Truncate the ellipsis. We're adding this ourselves. + } + descriptionLabel.lastLineWidth = line.implicitWidth; } } - Label - { - id: tripleDotLabel - anchors.left: parent.left - anchors.leftMargin: descriptionLabel.lastLineWidth - anchors.bottom: descriptionLabel.bottom - - text: "… " - font: descriptionLabel.font - color: descriptionLabel.color - visible: descriptionLabel.truncated && descriptionLabel.text !== "" - } - Cura.TertiaryButton - { - id: readMoreButton - anchors.right: parent.right - anchors.bottom: descriptionLabel.bottom - height: fontMetrics.height //Height of a single line. - - text: catalog.i18nc("@info", "Read more") - iconSource: UM.Theme.getIcon("LinkExternal") - - visible: descriptionLabel.truncated && descriptionLabel.text !== "" - enabled: visible - leftPadding: UM.Theme.getSize("default_margin").width - rightPadding: UM.Theme.getSize("wide_margin").width - textFont: descriptionLabel.font - isIconOnRightSide: true - - onClicked: Qt.openUrlExternally(packageData.packageInfoUrl) - } } - - // Author and action buttons. - RowLayout + Label { - id: authorAndActionButton - Layout.preferredWidth: parent.width - Layout.preferredHeight: childrenRect.height + id: tripleDotLabel + anchors.left: parent.left + anchors.leftMargin: descriptionLabel.lastLineWidth + anchors.bottom: descriptionLabel.bottom - spacing: UM.Theme.getSize("narrow_margin").width + text: "… " + font: descriptionLabel.font + color: descriptionLabel.color + visible: descriptionLabel.truncated && descriptionLabel.text !== "" + } + Cura.TertiaryButton + { + id: readMoreButton + anchors.right: parent.right + anchors.bottom: descriptionLabel.bottom + height: fontMetrics.height //Height of a single line. - // label "By" - Label - { - id: authorBy - Layout.alignment: Qt.AlignCenter + text: catalog.i18nc("@info", "Read more") + iconSource: UM.Theme.getIcon("LinkExternal") - text: catalog.i18nc("@label", "By") - font: UM.Theme.getFont("default") - color: UM.Theme.getColor("text") - } + visible: descriptionLabel.truncated && descriptionLabel.text !== "" + enabled: visible + leftPadding: UM.Theme.getSize("default_margin").width + rightPadding: UM.Theme.getSize("wide_margin").width + textFont: descriptionLabel.font + isIconOnRightSide: true - // clickable author name - Cura.TertiaryButton - { - Layout.fillWidth: true - Layout.preferredHeight: authorBy.height - Layout.alignment: Qt.AlignCenter - - text: packageData.authorName - textFont: UM.Theme.getFont("default_bold") - textColor: UM.Theme.getColor("text") // override normal link color - leftPadding: 0 - rightPadding: 0 - iconSource: UM.Theme.getIcon("LinkExternal") - isIconOnRightSide: true - - onClicked: Qt.openUrlExternally(packageData.authorInfoUrl) - } - - ManageButton - { - id: enableManageButton - visible: !(installManageButton.confirmed || updateManageButton.confirmed) || enableManageButton.confirmed - button_style: packageData.stateManageEnableButton - Layout.alignment: Qt.AlignTop - busy: packageData.enableManageButton == "busy" - confirmed: packageData.enableManageButton == "confirmed" - text: { - switch (packageData.stateManageEnableButton) { - case "primary": - return catalog.i18nc("@button", "Enable"); - case "secondary": - return catalog.i18nc("@button", "Disable"); - case "busy": - if (packageData.installationStatus) { - return catalog.i18nc("@button", "Enabling..."); - } else { - return catalog.i18nc("@button", "Disabling..."); - } - case "confirmed": - if (packageData.installationStatus) { - return catalog.i18nc("@button", "Enabled"); - } else { - return catalog.i18nc("@button", "Disabled"); - } - default: - return ""; - } - } - enabled: !installManageButton.busy && !updateManageButton.busy - - onClicked: - { - if (primary_action) - { - packageData.enablePackageTriggered(packageData.packageId) - } - else - { - packageData.disablePackageTriggered(packageData.packageId) - } - } - } - - ManageButton - { - id: installManageButton - visible: (root.manageableInListView || installManageButton.confirmed) && !(enableManageButton.confirmed || updateManageButton.confirmed) - button_style: packageData.stateManageInstallButton - busy: packageData.stateManageInstallButton == "busy" - confirmed: packageData.stateManageInstallButton == "confirmed" - Layout.alignment: Qt.AlignTop - text: { - switch (packageData.stateManageInstallButton) { - case "primary": - return catalog.i18nc("@button", "Install"); - case "secondary": - return catalog.i18nc("@button", "Uninstall"); - case "busy": - if (packageData.installationStatus) { - return catalog.i18nc("@button", "Installing..."); - } else { - return catalog.i18nc("@button", "Uninstalling..."); - } - case "confirmed": - if (packageData.installationStatus) { - return catalog.i18nc("@button", "Installed"); - } else { - return catalog.i18nc("@button", "Uninstalled"); - } - default: - return ""; - } - } - enabled: !enableManageButton.busy && !updateManageButton.busy - - onClicked: - { - if (primary_action) - { - packageData.installPackageTriggered(packageData.packageId) - } - else - { - packageData.uninstallPackageTriggered(packageData.packageId) - } - } - } - - ManageButton - { - id: updateManageButton - visible: (root.manageableInListView) && (!installManageButton.confirmed || updateManageButton.confirmed) - - button_style: packageData.stateManageUpdateButton - busy: packageData.stateManageUpdateButton == "busy" - confirmed: packageData.stateManageUpdateButton == "confirmed" - Layout.alignment: Qt.AlignTop - enabled: !installManageButton.busy && !enableManageButton.busy - - text: { - switch (packageData.stateManageInstallButton) { - case "primary": - return catalog.i18nc("@button", "Update"); - case "busy": - return catalog.i18nc("@button", "Updating..."); - case "confirmed": - return catalog.i18nc("@button", "Updated"); - default: - return ""; - } - } - - onClicked: packageData.updatePackageTriggered(packageData.packageId) - } + onClicked: Qt.openUrlExternally(packageData.packageInfoUrl) } } } diff --git a/plugins/Marketplace/resources/qml/PackageCardHeader.qml b/plugins/Marketplace/resources/qml/PackageCardHeader.qml new file mode 100644 index 0000000000..4719fcfe13 --- /dev/null +++ b/plugins/Marketplace/resources/qml/PackageCardHeader.qml @@ -0,0 +1,299 @@ +// Copyright (c) 2021 Ultimaker B.V. +// Cura is released under the terms of the LGPLv3 or higher. + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.1 + +import UM 1.6 as UM +import Cura 1.6 as Cura + +// As both the PackageCard and Package contain similar components; a package icon, title, author bar. These components +// are combined into the reusable "PackageCardHeader" component +Item +{ + default property alias contents: contentItem.children; + + property var packageData + property bool showManageButtons + + width: parent.width + height: UM.Theme.getSize("card").height + + // card icon + Image + { + id: packageItem + anchors + { + top: parent.top + left: parent.left + margins: UM.Theme.getSize("default_margin").width + } + width: UM.Theme.getSize("card_icon").width + height: width + + source: packageData.iconUrl != "" ? packageData.iconUrl : "../images/placeholder.svg" + } + + ColumnLayout + { + anchors + { + left: packageItem.right + leftMargin: UM.Theme.getSize("default_margin").width + right: parent.right + rightMargin: UM.Theme.getSize("default_margin").width + top: parent.top + topMargin: UM.Theme.getSize("narrow_margin").height + } + height: packageItem.height + packageItem.anchors.margins * 2 + + // Title row. + RowLayout + { + id: titleBar + Layout.preferredWidth: parent.width + Layout.preferredHeight: childrenRect.height + + Label + { + text: packageData.displayName + font: UM.Theme.getFont("medium_bold") + color: UM.Theme.getColor("text") + verticalAlignment: Text.AlignTop + } + VerifiedIcon + { + enabled: packageData.isCheckedByUltimaker + visible: packageData.isCheckedByUltimaker + } + + Control + { + Layout.preferredWidth: UM.Theme.getSize("card_tiny_icon").width + Layout.preferredHeight: UM.Theme.getSize("card_tiny_icon").height + Layout.alignment: Qt.AlignCenter + enabled: false // remove! + visible: false // replace packageInfo.XXXXXX + // TODO: waiting for materials card implementation + + Cura.ToolTip + { + tooltipText: "" // TODO + visible: parent.hovered + } + + UM.RecolorImage + { + anchors.fill: parent + + color: UM.Theme.getColor("primary") + source: UM.Theme.getIcon("CheckCircle") // TODO + } + + // onClicked: Qt.openUrlExternally( XXXXXX ) // TODO + } + + Label + { + id: packageVersionLabel + text: packageData.packageVersion + font: UM.Theme.getFont("default") + color: UM.Theme.getColor("text") + Layout.fillWidth: true + } + + Button + { + id: externalLinkButton + + // For some reason if i set padding, they don't match up. If i set all of them explicitly, it does work? + leftPadding: UM.Theme.getSize("narrow_margin").width + rightPadding: UM.Theme.getSize("narrow_margin").width + topPadding: UM.Theme.getSize("narrow_margin").width + bottomPadding: UM.Theme.getSize("narrow_margin").width + + Layout.preferredWidth: UM.Theme.getSize("card_tiny_icon").width + 2 * padding + Layout.preferredHeight: UM.Theme.getSize("card_tiny_icon").width + 2 * padding + contentItem: UM.RecolorImage + { + source: UM.Theme.getIcon("LinkExternal") + color: UM.Theme.getColor("icon") + implicitWidth: UM.Theme.getSize("card_tiny_icon").width + implicitHeight: UM.Theme.getSize("card_tiny_icon").height + } + + background: Rectangle + { + color: externalLinkButton.hovered ? UM.Theme.getColor("action_button_hovered"): "transparent" + radius: externalLinkButton.width / 2 + } + onClicked: Qt.openUrlExternally(packageData.authorInfoUrl) + } + } + + // When a package Card companent is created and children are provided to it they are rendered here + Item { + id: contentItem + Layout.fillHeight: true + Layout.preferredWidth: parent.width + } + + // Author and action buttons. + RowLayout + { + id: authorAndActionButton + Layout.preferredWidth: parent.width + Layout.preferredHeight: childrenRect.height + + spacing: UM.Theme.getSize("narrow_margin").width + + // label "By" + Label + { + id: authorBy + Layout.alignment: Qt.AlignCenter + + text: catalog.i18nc("@label", "By") + font: UM.Theme.getFont("default") + color: UM.Theme.getColor("text") + } + + // clickable author name + Cura.TertiaryButton + { + Layout.fillWidth: true + Layout.preferredHeight: authorBy.height + Layout.alignment: Qt.AlignCenter + + text: packageData.authorName + textFont: UM.Theme.getFont("default_bold") + textColor: UM.Theme.getColor("text") // override normal link color + leftPadding: 0 + rightPadding: 0 + iconSource: UM.Theme.getIcon("LinkExternal") + isIconOnRightSide: true + + onClicked: Qt.openUrlExternally(packageData.authorInfoUrl) + } + + ManageButton + { + id: enableManageButton + visible: !(installManageButton.confirmed || updateManageButton.confirmed) || enableManageButton.confirmed + button_style: packageData.stateManageEnableButton + Layout.alignment: Qt.AlignTop + busy: packageData.enableManageButton == "busy" + confirmed: packageData.enableManageButton == "confirmed" + text: { + switch (packageData.stateManageEnableButton) { + case "primary": + return catalog.i18nc("@button", "Enable"); + case "secondary": + return catalog.i18nc("@button", "Disable"); + case "busy": + if (packageData.installationStatus) { + return catalog.i18nc("@button", "Enabling..."); + } else { + return catalog.i18nc("@button", "Disabling..."); + } + case "confirmed": + if (packageData.installationStatus) { + return catalog.i18nc("@button", "Enabled"); + } else { + return catalog.i18nc("@button", "Disabled"); + } + default: + return ""; + } + } + enabled: !installManageButton.busy && !updateManageButton.busy + + onClicked: + { + if (primary_action) + { + packageData.enablePackageTriggered(packageData.packageId) + } + else + { + packageData.disablePackageTriggered(packageData.packageId) + } + } + } + + ManageButton + { + id: installManageButton + visible: (showManageButtons || installManageButton.confirmed) && !(enableManageButton.confirmed || updateManageButton.confirmed) + button_style: packageData.stateManageInstallButton + busy: packageData.stateManageInstallButton == "busy" + confirmed: packageData.stateManageInstallButton == "confirmed" + Layout.alignment: Qt.AlignTop + text: { + switch (packageData.stateManageInstallButton) { + case "primary": + return catalog.i18nc("@button", "Install"); + case "secondary": + return catalog.i18nc("@button", "Uninstall"); + case "busy": + if (packageData.installationStatus) { + return catalog.i18nc("@button", "Installing..."); + } else { + return catalog.i18nc("@button", "Uninstalling..."); + } + case "confirmed": + if (packageData.installationStatus) { + return catalog.i18nc("@button", "Installed"); + } else { + return catalog.i18nc("@button", "Uninstalled"); + } + default: + return ""; + } + } + enabled: !enableManageButton.busy && !updateManageButton.busy + + onClicked: + { + if (primary_action) + { + packageData.installPackageTriggered(packageData.packageId) + } + else + { + packageData.uninstallPackageTriggered(packageData.packageId) + } + } + } + + ManageButton + { + id: updateManageButton + visible: showManageButtons && (!installManageButton.confirmed || updateManageButton.confirmed) + + button_style: packageData.stateManageUpdateButton + busy: packageData.stateManageUpdateButton == "busy" + confirmed: packageData.stateManageUpdateButton == "confirmed" + Layout.alignment: Qt.AlignTop + enabled: !installManageButton.busy && !enableManageButton.busy + + text: { + switch (packageData.stateManageInstallButton) { + case "primary": + return catalog.i18nc("@button", "Update"); + case "busy": + return catalog.i18nc("@button", "Updating..."); + case "confirmed": + return catalog.i18nc("@button", "Updated"); + default: + return ""; + } + } + + onClicked: packageData.updatePackageTriggered(packageData.packageId) + } + } + } +} diff --git a/plugins/Marketplace/resources/qml/PackagePage.qml b/plugins/Marketplace/resources/qml/PackagePage.qml index fca25c4022..21c400fff2 100644 --- a/plugins/Marketplace/resources/qml/PackagePage.qml +++ b/plugins/Marketplace/resources/qml/PackagePage.qml @@ -11,8 +11,7 @@ import Cura 1.6 as Cura Rectangle { id: root - property var packageData - property bool manageableInListView + property alias packageData: packageCardHeader.packageData height: childrenRect.height color: UM.Theme.getColor("main_background") @@ -29,118 +28,12 @@ Rectangle width: parent.width height: UM.Theme.getSize("card").height - Image + PackageCardHeader { - id: packageItem - anchors - { - top: parent.top - left: parent.left - margins: UM.Theme.getSize("default_margin").width - } - width: UM.Theme.getSize("card_icon").width - height: width + id: packageCardHeader + showManageButtons: true - source: packageData.iconUrl != "" ? packageData.iconUrl : "../images/placeholder.svg" - } - - ColumnLayout - { - anchors - { - left: packageItem.right - leftMargin: UM.Theme.getSize("default_margin").width - right: parent.right - rightMargin: UM.Theme.getSize("default_margin").width - top: parent.top - topMargin: UM.Theme.getSize("narrow_margin").height - } - height: packageItem.height + packageItem.anchors.margins * 2 - - // Title row. - RowLayout - { - id: titleBar - Layout.preferredWidth: parent.width - Layout.preferredHeight: childrenRect.height - - Label - { - text: packageData.displayName - font: UM.Theme.getFont("medium_bold") - color: UM.Theme.getColor("text") - verticalAlignment: Text.AlignTop - } - VerifiedIcon - { - enabled: packageData.isCheckedByUltimaker - visible: packageData.isCheckedByUltimaker - } - - - Control - { - Layout.preferredWidth: UM.Theme.getSize("card_tiny_icon").width - Layout.preferredHeight: UM.Theme.getSize("card_tiny_icon").height - Layout.alignment: Qt.AlignCenter - enabled: false // remove! - visible: false // replace packageInfo.XXXXXX - // TODO: waiting for materials card implementation - - Cura.ToolTip - { - tooltipText: "" // TODO - visible: parent.hovered - } - - UM.RecolorImage - { - anchors.fill: parent - - color: UM.Theme.getColor("primary") - source: UM.Theme.getIcon("CheckCircle") // TODO - } - - // onClicked: Qt.openUrlExternally( XXXXXX ) // TODO - } - - Label - { - id: packageVersionLabel - text: packageData.packageVersion - font: UM.Theme.getFont("default") - color: UM.Theme.getColor("text") - Layout.fillWidth: true - } - - Button - { - id: externalLinkButton - - // For some reason if i set padding, they don't match up. If i set all of them explicitly, it does work? - leftPadding: UM.Theme.getSize("narrow_margin").width - rightPadding: UM.Theme.getSize("narrow_margin").width - topPadding: UM.Theme.getSize("narrow_margin").width - bottomPadding: UM.Theme.getSize("narrow_margin").width - - Layout.preferredWidth: UM.Theme.getSize("card_tiny_icon").width + 2 * padding - Layout.preferredHeight: UM.Theme.getSize("card_tiny_icon").width + 2 * padding - contentItem: UM.RecolorImage - { - source: UM.Theme.getIcon("LinkExternal") - color: UM.Theme.getColor("icon") - implicitWidth: UM.Theme.getSize("card_tiny_icon").width - implicitHeight: UM.Theme.getSize("card_tiny_icon").height - } - - background: Rectangle - { - color: externalLinkButton.hovered ? UM.Theme.getColor("action_button_hovered"): "transparent" - radius: externalLinkButton.width / 2 - } - onClicked: Qt.openUrlExternally(packageData.authorInfoUrl) - } - } + anchors.fill: parent Row { @@ -167,160 +60,6 @@ Rectangle text: packageData.downloadCount } } - - // Author and action buttons. - RowLayout - { - id: authorAndActionButton - Layout.preferredWidth: parent.width - Layout.preferredHeight: childrenRect.height - - spacing: UM.Theme.getSize("narrow_margin").width - - Label - { - id: authorBy - Layout.alignment: Qt.AlignCenter - - text: catalog.i18nc("@label", "By") - font: UM.Theme.getFont("default") - color: UM.Theme.getColor("text") - } - - Cura.TertiaryButton - { - Layout.fillWidth: true - Layout.preferredHeight: authorBy.height - Layout.alignment: Qt.AlignCenter - - text: packageData.authorName - textFont: UM.Theme.getFont("default_bold") - textColor: UM.Theme.getColor("text") // override normal link color - leftPadding: 0 - rightPadding: 0 - iconSource: UM.Theme.getIcon("LinkExternal") - isIconOnRightSide: true - - onClicked: Qt.openUrlExternally(packageData.authorInfoUrl) - } - - ManageButton - { - id: enableManageButton - visible: !(installManageButton.confirmed || updateManageButton.confirmed) || enableManageButton.confirmed - button_style: packageData.stateManageEnableButton - Layout.alignment: Qt.AlignTop - busy: packageData.enableManageButton == "busy" - confirmed: packageData.enableManageButton == "confirmed" - text: { - switch (packageData.stateManageEnableButton) { - case "primary": - return catalog.i18nc("@button", "Enable"); - case "secondary": - return catalog.i18nc("@button", "Disable"); - case "busy": - if (packageData.installationStatus) { - return catalog.i18nc("@button", "Enabling..."); - } else { - return catalog.i18nc("@button", "Disabling..."); - } - case "confirmed": - if (packageData.installationStatus) { - return catalog.i18nc("@button", "Enabled"); - } else { - return catalog.i18nc("@button", "Disabled"); - } - default: - return ""; - } - } - enabled: !installManageButton.busy && !updateManageButton.busy - - onClicked: - { - if (primary_action) - { - packageData.enablePackageTriggered(packageData.packageId) - } - else - { - packageData.disablePackageTriggered(packageData.packageId) - } - } - } - - ManageButton - { - id: installManageButton - visible: !(enableManageButton.confirmed || updateManageButton.confirmed) - button_style: packageData.stateManageInstallButton - busy: packageData.stateManageInstallButton == "busy" - confirmed: packageData.stateManageInstallButton == "confirmed" - Layout.alignment: Qt.AlignTop - text: { - switch (packageData.stateManageInstallButton) { - case "primary": - return catalog.i18nc("@button", "Install"); - case "secondary": - return catalog.i18nc("@button", "Uninstall"); - case "busy": - if (packageData.installationStatus) { - return catalog.i18nc("@button", "Installing..."); - } else { - return catalog.i18nc("@button", "Uninstalling..."); - } - case "confirmed": - if (packageData.installationStatus) { - return catalog.i18nc("@button", "Installed"); - } else { - return catalog.i18nc("@button", "Uninstalled"); - } - default: - return ""; - } - } - enabled: !enableManageButton.busy && !updateManageButton.busy - - onClicked: - { - if (primary_action) - { - packageData.installPackageTriggered(packageData.packageId) - } - else - { - packageData.uninstallPackageTriggered(packageData.packageId) - } - } - } - - ManageButton - { - id: updateManageButton - visible: !installManageButton.confirmed || updateManageButton.confirmed - - button_style: packageData.stateManageUpdateButton - busy: packageData.stateManageUpdateButton == "busy" - confirmed: packageData.stateManageUpdateButton == "confirmed" - Layout.alignment: Qt.AlignTop - enabled: !installManageButton.busy && !enableManageButton.busy - - text: { - switch (packageData.stateManageInstallButton) { - case "primary": - return catalog.i18nc("@button", "Update"); - case "busy": - return catalog.i18nc("@button", "Updating..."); - case "confirmed": - return catalog.i18nc("@button", "Updated"); - default: - return ""; - } - } - - onClicked: packageData.updatePackageTriggered(packageData.packageId) - } - } } } From 7734bf5169fc70852c4035bc482ec54ee51bb8f5 Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Thu, 9 Dec 2021 07:58:14 +0100 Subject: [PATCH 130/195] Show Install and Update buttons in the correct scenario's Contributes to: CURA-8587 --- plugins/Marketplace/PackageModel.py | 21 +++++++------------ .../resources/qml/PackageCardHeader.qml | 12 +++++------ 2 files changed, 14 insertions(+), 19 deletions(-) diff --git a/plugins/Marketplace/PackageModel.py b/plugins/Marketplace/PackageModel.py index 93d41187e1..9996a8e1df 100644 --- a/plugins/Marketplace/PackageModel.py +++ b/plugins/Marketplace/PackageModel.py @@ -64,8 +64,6 @@ class PackageModel(QObject): self._is_installing = False self._install_status_changing = False - self._is_recently_installed = False - self._is_recently_updated = False self._can_update = False self._is_updating = False @@ -93,10 +91,8 @@ class PackageModel(QObject): def finished_installed(is_updating): if is_updating: - self._is_recently_installed = True self.setIsUpdating(False) else: - self._is_recently_updated self.setIsInstalling(False) self.isRecentlyInstalledChanged.connect(finished_installed) @@ -362,7 +358,11 @@ class PackageModel(QObject): @pyqtProperty(bool, notify = stateManageButtonChanged) def isInstalled(self) -> bool: - return self._package_id in CuraApplication.getInstance().getPackageManager().getPackagesToInstall() + return self._package_id in CuraApplication.getInstance().getPackageManager().local_packages + + @pyqtProperty(bool, notify = stateManageButtonChanged) + def isRecentlyInstalled(self) -> bool: + return self._package_id in CuraApplication.getInstance().getPackageManager().getPackagesToInstall() or self._package_id in CuraApplication.getInstance().getPackageManager().getPackagesToRemove() @pyqtProperty(bool, notify = stateManageButtonChanged) def isUninstalled(self) -> bool: @@ -389,14 +389,9 @@ class PackageModel(QObject): def isUpdating(self): return self._is_updating - def setIsUpdated(self, value): - if value != self._is_recently_updated: - self._is_recently_updated = value - self.stateManageButtonChanged.emit() - - @pyqtProperty(bool, fset = setIsUpdated, notify = stateManageButtonChanged) - def isUpdated(self): - return self._is_recently_updated + @pyqtProperty(bool, notify = stateManageButtonChanged) + def isRecentlyUpdated(self): + return self._package_id in CuraApplication.getInstance().getPackageManager().getPackagesToInstall() and self._package_id in CuraApplication.getInstance().getPackageManager().getPackagesToRemove() @property def can_update(self) -> bool: diff --git a/plugins/Marketplace/resources/qml/PackageCardHeader.qml b/plugins/Marketplace/resources/qml/PackageCardHeader.qml index ca74d9f77d..5859fbcd8e 100644 --- a/plugins/Marketplace/resources/qml/PackageCardHeader.qml +++ b/plugins/Marketplace/resources/qml/PackageCardHeader.qml @@ -181,7 +181,7 @@ Item ManageButton { id: enableManageButton - visible: showManageButtons && !(installManageButton.confirmed || updateManageButton.confirmed) + visible: showManageButtons && packageData.isInstalled && !(installManageButton.confirmed || updateManageButton.confirmed) enabled: !(installManageButton.busy || updateManageButton.busy) busy: false @@ -208,12 +208,12 @@ Item ManageButton { id: installManageButton - visible: (showManageButtons || confirmed) && ((packageData.isBundled && packageData.canDowngrade) || !packageData.isBundled || !updateManageButton.confirmed) + visible: (showManageButtons || confirmed) && ((packageData.isBundled && packageData.canDowngrade) || !packageData.isBundled) && !updateManageButton.confirmed enabled: !packageData.isUpdating busy: packageData.isInstalling - confirmed: packageData.isInstalled || packageData.isUninstalled + confirmed: packageData.isRecentlyInstalled button_style: packageData.stateManageInstallButton Layout.alignment: Qt.AlignTop @@ -223,7 +223,7 @@ Item if (packageData.stateManageInstallButton) { if (packageData.isInstalling) { return catalog.i18nc("@button", "Installing..."); } - else if (packageData.isInstalled) { return catalog.i18nc("@button", "Installed"); } + else if (packageData.isRecentlyInstalled) { return catalog.i18nc("@button", "Installed"); } else { return catalog.i18nc("@button", "Install"); } } else @@ -254,7 +254,7 @@ Item enabled: !installManageButton.busy busy: packageData.isUpdating - confirmed: packageData.isUpdated + confirmed: packageData.isRecentlyUpdated button_style: true Layout.alignment: Qt.AlignTop @@ -262,7 +262,7 @@ Item text: { if (packageData.isUpdating) { return catalog.i18nc("@button", "Updating..."); } - else if (packageData.isUpdated) { return catalog.i18nc("@button", "Updated"); } + else if (packageData.isRecentlyUpdated) { return catalog.i18nc("@button", "Updated"); } else { return catalog.i18nc("@button", "Update"); } } From 59470814e2be076e10afa717190a4e39fe267a6e Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Thu, 9 Dec 2021 08:30:22 +0100 Subject: [PATCH 131/195] Show spinner again Contributes to: CURA-8587 --- plugins/Marketplace/resources/qml/ManageButton.qml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/Marketplace/resources/qml/ManageButton.qml b/plugins/Marketplace/resources/qml/ManageButton.qml index 2e2ef294d1..fdeb37a87a 100644 --- a/plugins/Marketplace/resources/qml/ManageButton.qml +++ b/plugins/Marketplace/resources/qml/ManageButton.qml @@ -55,12 +55,11 @@ Item UM.RecolorImage { id: busyIndicator + visible: parent.visible + height: UM.Theme.getSize("action_button").height - 2 * UM.Theme.getSize("narrow_margin").height width: height anchors.left: parent.left - anchors.top: parent.top - anchors.topMargin: UM.Theme.getSize("narrow_margin").height - anchors.bottom: parent.bottom - anchors.bottomMargin: anchors.topMargin + anchors.verticalCenter: parent.verticalCenter source: UM.Theme.getIcon("Spinner") color: UM.Theme.getColor("primary") @@ -78,6 +77,7 @@ Item Label { id: busyMessageText + visible: parent.visible anchors.left: busyIndicator.right anchors.leftMargin: UM.Theme.getSize("narrow_margin").width anchors.verticalCenter: parent.verticalCenter From 51a77f683dc0b227089825af9c3235d557a56ba4 Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Thu, 9 Dec 2021 09:56:41 +0100 Subject: [PATCH 132/195] Moved stateManageButton logic out of the packageModel Contributes to: CURA-8587 --- cura/CuraPackageManager.py | 13 +++- plugins/Marketplace/PackageList.py | 24 +------- plugins/Marketplace/PackageModel.py | 59 ++++++------------- .../resources/qml/PackageCardHeader.qml | 12 ++-- 4 files changed, 37 insertions(+), 71 deletions(-) diff --git a/cura/CuraPackageManager.py b/cura/CuraPackageManager.py index bca6494f37..386d249925 100644 --- a/cura/CuraPackageManager.py +++ b/cura/CuraPackageManager.py @@ -1,7 +1,7 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from typing import Any, cast, Dict, List, Tuple, TYPE_CHECKING, Optional +from typing import Any, cast, Dict, List, Set, Tuple, TYPE_CHECKING, Optional from cura.CuraApplication import CuraApplication # To find some resource types. from cura.Settings.GlobalStack import GlobalStack @@ -20,10 +20,12 @@ class CuraPackageManager(PackageManager): def __init__(self, application: "QtApplication", parent: Optional["QObject"] = None) -> None: super().__init__(application, parent) self._local_packages: Optional[List[Dict[str, Any]]] = None + self._local_packages_id: Optional[Set[str]] = None self.installedPackagesChanged.connect(self._updateLocalPackages) def _updateLocalPackages(self) -> None: self._local_packages = self.getAllLocalPackages() + self._local_packages_id = set(pkg["package_id"] for pkg in self._local_packages) @property def local_packages(self) -> List[Dict[str, Any]]: @@ -34,6 +36,15 @@ class CuraPackageManager(PackageManager): # It's guaranteed to be a list now. return cast(List[Dict[str, Any]], self._local_packages) + @property + def local_packages_id(self) -> Set[str]: + """locally installed packages, lazy execution""" + if self._local_packages_id is None: + self._updateLocalPackages() + # _updateLocalPackages always results in a list of packages, not None. + # It's guaranteed to be a list now. + return cast(Set[str], self._local_packages_id) + def initialize(self) -> None: self._installation_dirs_dict["materials"] = Resources.getStoragePath(CuraApplication.ResourceTypes.MaterialInstanceContainer) self._installation_dirs_dict["qualities"] = Resources.getStoragePath(CuraApplication.ResourceTypes.QualityInstanceContainer) diff --git a/plugins/Marketplace/PackageList.py b/plugins/Marketplace/PackageList.py index e6e5e78ba9..4271166318 100644 --- a/plugins/Marketplace/PackageList.py +++ b/plugins/Marketplace/PackageList.py @@ -184,7 +184,6 @@ class PackageList(ListModel): to_be_installed = self._manager.installPackage(package_path) is not None package = self.getPackageModel(package_id) # TODO handle failure - package.isRecentlyInstalledChanged.emit(update) self.subscribeUserToPackage(package_id, str(package.sdk_version)) def download(self, package_id: str, url: str, update: bool = False) -> None: @@ -268,8 +267,8 @@ class PackageList(ListModel): package.installPackageTriggered.connect(self.installPackage) package.uninstallPackageTriggered.connect(self.uninstallPackage) package.updatePackageTriggered.connect(self.updatePackage) - package.enablePackageTriggered.connect(self.enablePackage) - package.disablePackageTriggered.connect(self.disablePackage) + package.enablePackageTriggered.connect(self._plugin_registry.enablePlugin) + package.disablePackageTriggered.connect(self._plugin_registry.disablePlugin) def installPackage(self, package_id: str) -> None: """Install a package from the Marketplace @@ -288,7 +287,6 @@ class PackageList(ListModel): package = self.getPackageModel(package_id) self._manager.removePackage(package_id) self.unsunscribeUserFromPackage(package_id) - package.isRecentlyInstalledChanged.emit(False) def updatePackage(self, package_id: str) -> None: """Update a package from the Marketplace @@ -299,21 +297,3 @@ class PackageList(ListModel): self._manager.removePackage(package_id, force_add = True) url = package.download_url self.download(package_id, url, True) - - def enablePackage(self, package_id: str) -> None: - """Enable a package in the plugin registry - - :param package_id: the package identification string - """ - package = self.getPackageModel(package_id) - self._plugin_registry.enablePlugin(package_id) - package.is_active = True - - def disablePackage(self, package_id: str) -> None: - """Disable a package in the plugin registry - - :param package_id: the package identification string - """ - package = self.getPackageModel(package_id) - self._plugin_registry.disablePlugin(package_id) - package.is_active = False diff --git a/plugins/Marketplace/PackageModel.py b/plugins/Marketplace/PackageModel.py index 9996a8e1df..6d20cd7f91 100644 --- a/plugins/Marketplace/PackageModel.py +++ b/plugins/Marketplace/PackageModel.py @@ -3,22 +3,21 @@ import re from enum import Enum -from typing import Any, Dict, List, Optional +from typing import Any, cast, Dict, List, Optional from PyQt5.QtCore import pyqtProperty, QObject, pyqtSignal from cura.CuraApplication import CuraApplication +from cura.CuraPackageManager import CuraPackageManager from cura.Settings.CuraContainerRegistry import CuraContainerRegistry # To get names of materials we're compatible with. from UM.i18n import i18nCatalog # To translate placeholder names if data is not present. +from UM.PluginRegistry import PluginRegistry catalog = i18nCatalog("cura") class PackageModel(QObject): """ Represents a package, containing all the relevant information to be displayed about a package. - - Effectively this behaves like a glorified named tuple, but as a QObject so that its properties can be obtained from - QML. The model can also be constructed directly from a response received by the API. """ def __init__(self, package_data: Dict[str, Any], section_title: Optional[str] = None, parent: Optional[QObject] = None) -> None: @@ -29,10 +28,11 @@ class PackageModel(QObject): :param parent: The parent QML object that controls the lifetime of this model (normally a PackageList). """ super().__init__(parent) + self._package_manager: CuraPackageManager = cast(CuraPackageManager, CuraApplication.getInstance().getPackageManager()) + self._plugin_registry: PluginRegistry = CuraApplication.getInstance().getPluginRegistry() + self._package_id = package_data.get("package_id", "UnknownPackageId") self._package_type = package_data.get("package_type", "") - self._is_installed = package_data.get("is_installed", False) - self._is_active = package_data.get("is_active", False) self._is_bundled = package_data.get("is_bundled", False) self._icon_url = package_data.get("icon_url", "") self._display_name = package_data.get("display_name", catalog.i18nc("@label:property", "Unknown Package")) @@ -89,13 +89,11 @@ class PackageModel(QObject): self.updatePackageTriggered.connect(update_clicked) - def finished_installed(is_updating): - if is_updating: - self.setIsUpdating(False) - else: - self.setIsInstalling(False) + def finished_installed(): + self.setIsUpdating(False) + self.setIsInstalling(False) - self.isRecentlyInstalledChanged.connect(finished_installed) + self._package_manager.installedPackagesChanged.connect(finished_installed) def __eq__(self, other: object): if isinstance(other, PackageModel): @@ -313,30 +311,9 @@ class PackageModel(QObject): isRecentlyInstalledChanged = pyqtSignal(bool) - # --- enabling --- - @pyqtProperty(bool, notify = stateManageButtonChanged) - def stateManageEnableButton(self) -> bool: - """The state of the manage Enable Button of this package""" - return not (self._is_installed and self._is_active) - - @property - def is_active(self) -> bool: - """Flag if the package is currently active""" - return self._is_active - - @is_active.setter - def is_active(self, value: bool) -> None: - if value != self._is_active: - self._is_active = value - self.stateManageButtonChanged.emit() - - # --- Installing --- - - @pyqtProperty(bool, notify = stateManageButtonChanged) - def stateManageInstallButton(self) -> bool: - """The state of the Manage Install package card""" - return not self._is_installed + def isActive(self): + return not self._package_id in self._plugin_registry.getDisabledPlugins() def setIsInstalling(self, value: bool) -> None: if value != self._is_installing: @@ -358,15 +335,15 @@ class PackageModel(QObject): @pyqtProperty(bool, notify = stateManageButtonChanged) def isInstalled(self) -> bool: - return self._package_id in CuraApplication.getInstance().getPackageManager().local_packages + return self._package_id in self._package_manager.local_packages_id @pyqtProperty(bool, notify = stateManageButtonChanged) def isRecentlyInstalled(self) -> bool: - return self._package_id in CuraApplication.getInstance().getPackageManager().getPackagesToInstall() or self._package_id in CuraApplication.getInstance().getPackageManager().getPackagesToRemove() + return self._package_id in self._package_manager.getPackagesToInstall() @pyqtProperty(bool, notify = stateManageButtonChanged) - def isUninstalled(self) -> bool: - return self._package_id in CuraApplication.getInstance().getPackageManager().getPackagesToRemove() + def isRecentlyUninstalled(self) -> bool: + return self._package_id in self._package_manager.getPackagesToRemove() def setCanDowngrade(self, value: bool) -> None: if value != self._can_downgrade: @@ -378,8 +355,6 @@ class PackageModel(QObject): """Flag if the installed package can be downgraded to a bundled version""" return self._can_downgrade - # --- Updating --- - def setIsUpdating(self, value): if value != self._is_updating: self._is_updating = value @@ -391,7 +366,7 @@ class PackageModel(QObject): @pyqtProperty(bool, notify = stateManageButtonChanged) def isRecentlyUpdated(self): - return self._package_id in CuraApplication.getInstance().getPackageManager().getPackagesToInstall() and self._package_id in CuraApplication.getInstance().getPackageManager().getPackagesToRemove() + return self._package_id in self._package_manager.getPackagesToInstall() and self._package_id in self._package_manager.getPackagesToRemove() @property def can_update(self) -> bool: diff --git a/plugins/Marketplace/resources/qml/PackageCardHeader.qml b/plugins/Marketplace/resources/qml/PackageCardHeader.qml index 5859fbcd8e..4964338ab2 100644 --- a/plugins/Marketplace/resources/qml/PackageCardHeader.qml +++ b/plugins/Marketplace/resources/qml/PackageCardHeader.qml @@ -187,7 +187,7 @@ Item busy: false confirmed: false - button_style: packageData.stateManageEnableButton + button_style: packageData.isInstalled && packageData.isActive Layout.alignment: Qt.AlignTop text: packageData.stateManageEnableButton ? catalog.i18nc("@button", "Enable") : catalog.i18nc("@button", "Disable") @@ -213,23 +213,23 @@ Item enabled: !packageData.isUpdating busy: packageData.isInstalling - confirmed: packageData.isRecentlyInstalled + confirmed: packageData.isRecentlyInstalled || packageData.isRecentlyUninstalled - button_style: packageData.stateManageInstallButton + button_style: !packageData.isInstalled Layout.alignment: Qt.AlignTop text: { - if (packageData.stateManageInstallButton) + if (packageData.isRecentlyInstalled) { return catalog.i18nc("@button", "Installed"); } + if (packageData.isRecentlyUninstalled) { return catalog.i18nc("@button", "Uninstalled"); } + if (button_style) { if (packageData.isInstalling) { return catalog.i18nc("@button", "Installing..."); } - else if (packageData.isRecentlyInstalled) { return catalog.i18nc("@button", "Installed"); } else { return catalog.i18nc("@button", "Install"); } } else { if (packageData.isInstalling) { return catalog.i18nc("@button", "Uninstalling..."); } - else if (packageData.isUninstalled) { return catalog.i18nc("@button", "Uninstalled"); } else { return catalog.i18nc("@button", "Uninstall"); } } } From 8dc88e52c2d8321ce0e0607ff3fcfadf2a392506 Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Thu, 9 Dec 2021 10:03:56 +0100 Subject: [PATCH 133/195] Don't show the Enable buttons for materials Contributes to: CURA-8587 --- plugins/Marketplace/resources/qml/PackageCardHeader.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/Marketplace/resources/qml/PackageCardHeader.qml b/plugins/Marketplace/resources/qml/PackageCardHeader.qml index 4964338ab2..ee7745235f 100644 --- a/plugins/Marketplace/resources/qml/PackageCardHeader.qml +++ b/plugins/Marketplace/resources/qml/PackageCardHeader.qml @@ -181,7 +181,7 @@ Item ManageButton { id: enableManageButton - visible: showManageButtons && packageData.isInstalled && !(installManageButton.confirmed || updateManageButton.confirmed) + visible: showManageButtons && packageData.isInstalled && !(installManageButton.confirmed || updateManageButton.confirmed || packageData.packageType == "material") enabled: !(installManageButton.busy || updateManageButton.busy) busy: false From 0299bb1694f7f5c21a1d1a61101d5e83252b5145 Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Thu, 9 Dec 2021 10:24:26 +0100 Subject: [PATCH 134/195] Fixed update buttons Contributes to: CURA-8587 --- plugins/Marketplace/LocalPackageList.py | 6 +++--- plugins/Marketplace/PackageModel.py | 13 ++++++------- .../Marketplace/resources/qml/PackageCardHeader.qml | 2 +- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/plugins/Marketplace/LocalPackageList.py b/plugins/Marketplace/LocalPackageList.py index 32e60b2518..72199bd0e4 100644 --- a/plugins/Marketplace/LocalPackageList.py +++ b/plugins/Marketplace/LocalPackageList.py @@ -51,7 +51,7 @@ class LocalPackageList(PackageList): # Obtain and sort the local packages self.setItems([{"package": p} for p in [self._makePackageModel(p) for p in self._manager.local_packages]]) - self.sort(attrgetter("sectionTitle", "can_update", "displayName"), key = "package", reverse = True) + self.sort(attrgetter("sectionTitle", "_can_update", "displayName"), key = "package", reverse = True) self.checkForUpdates(self._manager.local_packages) self.setIsLoading(False) @@ -97,9 +97,9 @@ class LocalPackageList(PackageList): for package_data in response_data["data"]: package = self.getPackageModel(package_data["package_id"]) package.download_url = package_data.get("download_url", "") - package.can_update = True + package.setCanUpdate(True) - self.sort(attrgetter("sectionTitle", "can_update", "displayName"), key = "package", reverse = True) + self.sort(attrgetter("sectionTitle", "_can_update", "displayName"), key = "package", reverse = True) self._ongoing_requests["check_updates"] = None except RuntimeError: # Setting the ownership of this object to not qml can still result in a RuntimeError. Which can occur when quickly toggling diff --git a/plugins/Marketplace/PackageModel.py b/plugins/Marketplace/PackageModel.py index 6d20cd7f91..f4afaf0a68 100644 --- a/plugins/Marketplace/PackageModel.py +++ b/plugins/Marketplace/PackageModel.py @@ -368,13 +368,12 @@ class PackageModel(QObject): def isRecentlyUpdated(self): return self._package_id in self._package_manager.getPackagesToInstall() and self._package_id in self._package_manager.getPackagesToRemove() - @property - def can_update(self) -> bool: - """Flag indicating if the package can be updated""" - return self._can_update - - @can_update.setter - def can_update(self, value: bool) -> None: + def setCanUpdate(self, value: bool) -> None: if value != self._can_update: self._can_update = value self.stateManageButtonChanged.emit() + + @pyqtProperty(bool, fset = setCanUpdate, notify = stateManageButtonChanged) + def canUpdate(self) -> bool: + """Flag indicating if the package can be updated""" + return self._can_update diff --git a/plugins/Marketplace/resources/qml/PackageCardHeader.qml b/plugins/Marketplace/resources/qml/PackageCardHeader.qml index ee7745235f..59a4408ce7 100644 --- a/plugins/Marketplace/resources/qml/PackageCardHeader.qml +++ b/plugins/Marketplace/resources/qml/PackageCardHeader.qml @@ -250,7 +250,7 @@ Item ManageButton { id: updateManageButton - visible: (showManageButtons && confirmed) && !installManageButton.confirmed + visible: (showManageButtons || confirmed) && packageData.canUpdate && !installManageButton.confirmed enabled: !installManageButton.busy busy: packageData.isUpdating From 8a583a43238e9af5d4dcca554a83c88c1b227b09 Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Thu, 9 Dec 2021 10:27:27 +0100 Subject: [PATCH 135/195] Fixed Enable button text and style Contributes to: CURA-8587 --- plugins/Marketplace/resources/qml/PackageCardHeader.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/Marketplace/resources/qml/PackageCardHeader.qml b/plugins/Marketplace/resources/qml/PackageCardHeader.qml index 59a4408ce7..be0a52f52a 100644 --- a/plugins/Marketplace/resources/qml/PackageCardHeader.qml +++ b/plugins/Marketplace/resources/qml/PackageCardHeader.qml @@ -187,10 +187,10 @@ Item busy: false confirmed: false - button_style: packageData.isInstalled && packageData.isActive + button_style: packageData.isInstalled && !packageData.isActive Layout.alignment: Qt.AlignTop - text: packageData.stateManageEnableButton ? catalog.i18nc("@button", "Enable") : catalog.i18nc("@button", "Disable") + text: button_style ? catalog.i18nc("@button", "Enable") : catalog.i18nc("@button", "Disable") onClicked: { From 4c5ca22b24c57a5c2ad6ce9493573dc0f17a955c Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Thu, 9 Dec 2021 12:04:47 +0100 Subject: [PATCH 136/195] Handle bundled packages which can be Downgraded Contributes to: CURA-8587 --- plugins/Marketplace/LocalPackageList.py | 1 - plugins/Marketplace/PackageModel.py | 12 ++---------- .../Marketplace/resources/qml/PackageCardHeader.qml | 6 +++++- 3 files changed, 7 insertions(+), 12 deletions(-) diff --git a/plugins/Marketplace/LocalPackageList.py b/plugins/Marketplace/LocalPackageList.py index 72199bd0e4..7bd2005fd2 100644 --- a/plugins/Marketplace/LocalPackageList.py +++ b/plugins/Marketplace/LocalPackageList.py @@ -66,7 +66,6 @@ class LocalPackageList(PackageList): section_title = self.PACKAGE_CATEGORIES[bundled_or_installed][package_type] package = PackageModel(package_info, section_title = section_title, parent = self) self._connectManageButtonSignals(package) - package.setCanDowngrade(self._manager.canDowngrade(package_id)) return package def checkForUpdates(self, packages: List[Dict[str, Any]]): diff --git a/plugins/Marketplace/PackageModel.py b/plugins/Marketplace/PackageModel.py index f4afaf0a68..14a71206d9 100644 --- a/plugins/Marketplace/PackageModel.py +++ b/plugins/Marketplace/PackageModel.py @@ -67,7 +67,6 @@ class PackageModel(QObject): self._can_update = False self._is_updating = False - self._can_downgrade = False self._section_title = section_title self.sdk_version = package_data.get("sdk_version_semver", "") # Note that there's a lot more info in the package_data than just these specified here. @@ -309,8 +308,6 @@ class PackageModel(QObject): disablePackageTriggered = pyqtSignal(str) - isRecentlyInstalledChanged = pyqtSignal(bool) - @pyqtProperty(bool, notify = stateManageButtonChanged) def isActive(self): return not self._package_id in self._plugin_registry.getDisabledPlugins() @@ -345,15 +342,10 @@ class PackageModel(QObject): def isRecentlyUninstalled(self) -> bool: return self._package_id in self._package_manager.getPackagesToRemove() - def setCanDowngrade(self, value: bool) -> None: - if value != self._can_downgrade: - self._can_downgrade = value - self.stateManageButtonChanged.emit() - - @pyqtProperty(bool, fset = setCanDowngrade, notify = stateManageButtonChanged) + @pyqtProperty(bool, notify = stateManageButtonChanged) def canDowngrade(self) -> bool: """Flag if the installed package can be downgraded to a bundled version""" - return self._can_downgrade + return self._package_manager.canDowngrade(self._package_id) def setIsUpdating(self, value): if value != self._is_updating: diff --git a/plugins/Marketplace/resources/qml/PackageCardHeader.qml b/plugins/Marketplace/resources/qml/PackageCardHeader.qml index be0a52f52a..ff999c1ee6 100644 --- a/plugins/Marketplace/resources/qml/PackageCardHeader.qml +++ b/plugins/Marketplace/resources/qml/PackageCardHeader.qml @@ -220,8 +220,12 @@ Item text: { + if (packageData.canDowngrade) { return catalog.i18nc("@button", "Downgrade"); } if (packageData.isRecentlyInstalled) { return catalog.i18nc("@button", "Installed"); } - if (packageData.isRecentlyUninstalled) { return catalog.i18nc("@button", "Uninstalled"); } + if (packageData.isRecentlyUninstalled) + { + if (packageData.canDowngrade) { return catalog.i18nc("@button", "Downgraded") } + else { return catalog.i18nc("@button", "Uninstalled"); } } if (button_style) { if (packageData.isInstalling) { return catalog.i18nc("@button", "Installing..."); } From 9874b0c8ba9d72136bd5be18361fc7bc61ca1a32 Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Thu, 9 Dec 2021 15:14:20 +0100 Subject: [PATCH 137/195] removed redundant dunders from helper class Contributes to: CURA-8587 --- cura/CuraPackageManager.py | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/cura/CuraPackageManager.py b/cura/CuraPackageManager.py index 386d249925..fcbf5f6ea6 100644 --- a/cura/CuraPackageManager.py +++ b/cura/CuraPackageManager.py @@ -78,23 +78,11 @@ class CuraPackageManager(PackageManager): def getAllLocalPackages(self) -> List[Dict[str, Any]]: """ returns an unordered list of all the package_info installed, to be installed or to be returned""" - class PkgInfo: + + class PkgInfo(dict): # Needed helper class because a dict isn't hashable - def __init__(self, package_info): - self._info = package_info - def __eq__(self, item): - return item == self._info["package_id"] - - def __repr__(self): - return repr(self._info) - - def __iter__(self): - for k, v in self._info.items(): - yield k, v - - def asdict(self): - return self._info + return item == self["package_id"] packages = [PkgInfo(package_info) for package in self.getAllInstalledPackagesInfo().values() for package_info in package] packages.extend([PkgInfo(package["package_info"]) for package in self.getPackagesToRemove().values() if package["package_info"]["package_id"] not in packages]) From 1adae61f39d17f1f9833324c74728314aed9bdb7 Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Thu, 9 Dec 2021 15:49:05 +0100 Subject: [PATCH 138/195] Enable and disabled now toggle correctly Contributes to: CURA-8587 --- plugins/Marketplace/PackageList.py | 2 -- plugins/Marketplace/PackageModel.py | 5 +++++ plugins/Marketplace/resources/qml/PackageCardHeader.qml | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/plugins/Marketplace/PackageList.py b/plugins/Marketplace/PackageList.py index 4271166318..8ddd74ead9 100644 --- a/plugins/Marketplace/PackageList.py +++ b/plugins/Marketplace/PackageList.py @@ -267,8 +267,6 @@ class PackageList(ListModel): package.installPackageTriggered.connect(self.installPackage) package.uninstallPackageTriggered.connect(self.uninstallPackage) package.updatePackageTriggered.connect(self.updatePackage) - package.enablePackageTriggered.connect(self._plugin_registry.enablePlugin) - package.disablePackageTriggered.connect(self._plugin_registry.disablePlugin) def installPackage(self, package_id: str) -> None: """Install a package from the Marketplace diff --git a/plugins/Marketplace/PackageModel.py b/plugins/Marketplace/PackageModel.py index 14a71206d9..aa63d6da13 100644 --- a/plugins/Marketplace/PackageModel.py +++ b/plugins/Marketplace/PackageModel.py @@ -11,6 +11,7 @@ from cura.CuraApplication import CuraApplication from cura.CuraPackageManager import CuraPackageManager from cura.Settings.CuraContainerRegistry import CuraContainerRegistry # To get names of materials we're compatible with. from UM.i18n import i18nCatalog # To translate placeholder names if data is not present. +from UM.Logger import Logger from UM.PluginRegistry import PluginRegistry catalog = i18nCatalog("cura") @@ -93,6 +94,9 @@ class PackageModel(QObject): self.setIsInstalling(False) self._package_manager.installedPackagesChanged.connect(finished_installed) + self.enablePackageTriggered.connect(self._plugin_registry.enablePlugin) + self.disablePackageTriggered.connect(self._plugin_registry.disablePlugin) + self._plugin_registry.hasPluginsEnabledOrDisabledChanged.connect(self.stateManageButtonChanged) def __eq__(self, other: object): if isinstance(other, PackageModel): @@ -310,6 +314,7 @@ class PackageModel(QObject): @pyqtProperty(bool, notify = stateManageButtonChanged) def isActive(self): + Logger.debug(f"getDisabledPlugins = {self._plugin_registry.getDisabledPlugins()}") return not self._package_id in self._plugin_registry.getDisabledPlugins() def setIsInstalling(self, value: bool) -> None: diff --git a/plugins/Marketplace/resources/qml/PackageCardHeader.qml b/plugins/Marketplace/resources/qml/PackageCardHeader.qml index ff999c1ee6..5a693ece83 100644 --- a/plugins/Marketplace/resources/qml/PackageCardHeader.qml +++ b/plugins/Marketplace/resources/qml/PackageCardHeader.qml @@ -187,7 +187,7 @@ Item busy: false confirmed: false - button_style: packageData.isInstalled && !packageData.isActive + button_style: !packageData.isActive Layout.alignment: Qt.AlignTop text: button_style ? catalog.i18nc("@button", "Enable") : catalog.i18nc("@button", "Disable") From 4991c3953520709b2b1d8d55512a57d457778fb9 Mon Sep 17 00:00:00 2001 From: casper Date: Thu, 9 Dec 2021 22:41:14 +0100 Subject: [PATCH 139/195] Make sure `ManageButton` has the correct width and height Cura 8587 --- .../resources/qml/ManageButton.qml | 26 +++++-------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/plugins/Marketplace/resources/qml/ManageButton.qml b/plugins/Marketplace/resources/qml/ManageButton.qml index fdeb37a87a..2c05af02e9 100644 --- a/plugins/Marketplace/resources/qml/ManageButton.qml +++ b/plugins/Marketplace/resources/qml/ManageButton.qml @@ -16,19 +16,17 @@ Item property bool busy property bool confirmed + Layout.preferredWidth: childrenRect.width + Layout.preferredHeight: childrenRect.height + signal clicked(bool primary_action) property Component primaryButton: Component { Cura.PrimaryButton { - id: primaryButton text: manageButton.text - - onClicked: - { - manageButton.clicked(true) - } + onClicked: manageButton.clicked(true) } } @@ -36,13 +34,8 @@ Item { Cura.SecondaryButton { - id: secondaryButton text: manageButton.text - - onClicked: - { - manageButton.clicked(false) - } + onClicked: manageButton.clicked(false) } } @@ -50,7 +43,8 @@ Item { Item { - id: busyMessage + height: UM.Theme.getSize("action_button").height + width: childrenRect.width UM.RecolorImage { @@ -76,7 +70,6 @@ Item } Label { - id: busyMessageText visible: parent.visible anchors.left: busyIndicator.right anchors.leftMargin: UM.Theme.getSize("narrow_margin").width @@ -93,13 +86,11 @@ Item { Item { - height: UM.Theme.getSize("action_button").height width: childrenRect.width Label { - id: confirmedMessageText anchors.verticalCenter: parent.verticalCenter text: manageButton.text @@ -109,9 +100,6 @@ Item } } - height: UM.Theme.getSize("action_button").height - width: childrenRect.width - Loader { From d9f77d7ffda2ad13759d97d0eba5a4a0e9145541 Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Fri, 10 Dec 2021 11:14:40 +0100 Subject: [PATCH 140/195] Moved the update logic to the PackageManager Contributes to CURA-8587 --- plugins/Marketplace/LocalPackageList.py | 21 +++------ plugins/Marketplace/PackageList.py | 12 ++--- plugins/Marketplace/PackageModel.py | 47 ++++++------------- .../resources/qml/PackageCardHeader.qml | 8 ++-- 4 files changed, 29 insertions(+), 59 deletions(-) diff --git a/plugins/Marketplace/LocalPackageList.py b/plugins/Marketplace/LocalPackageList.py index 7bd2005fd2..83aac65516 100644 --- a/plugins/Marketplace/LocalPackageList.py +++ b/plugins/Marketplace/LocalPackageList.py @@ -1,5 +1,5 @@ -# Copyright (c) 2021 Ultimaker B.V. -# Cura is released under the terms of the LGPLv3 or higher. +# Copyright (c) 2021 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. from typing import Any, Dict, List, Optional, TYPE_CHECKING from operator import attrgetter @@ -39,6 +39,7 @@ class LocalPackageList(PackageList): super().__init__(parent) self._has_footer = False self._ongoing_requests["check_updates"] = None + self._manager.packagesWithUpdateChanged.connect(lambda: self.sort(attrgetter("sectionTitle", "_can_update", "displayName"), key = "package", reverse = True)) @pyqtSlot() def updatePackages(self) -> None: @@ -92,16 +93,6 @@ class LocalPackageList(PackageList): if len(response_data["data"]) == 0: return - try: - for package_data in response_data["data"]: - package = self.getPackageModel(package_data["package_id"]) - package.download_url = package_data.get("download_url", "") - package.setCanUpdate(True) - - self.sort(attrgetter("sectionTitle", "_can_update", "displayName"), key = "package", reverse = True) - self._ongoing_requests["check_updates"] = None - except RuntimeError: - # Setting the ownership of this object to not qml can still result in a RuntimeError. Which can occur when quickly toggling - # between de-/constructing RemotePackageLists. This try-except is here to prevent a hard crash when the wrapped C++ object - # was deleted when it was still parsing the response - return + packages = response_data["data"] + self._manager.setPackagesWithUpdate(dict(zip([p['package_id'] for p in packages], [p for p in packages]))) + self._ongoing_requests["check_updates"] = None diff --git a/plugins/Marketplace/PackageList.py b/plugins/Marketplace/PackageList.py index 8ddd74ead9..c245b44df9 100644 --- a/plugins/Marketplace/PackageList.py +++ b/plugins/Marketplace/PackageList.py @@ -1,5 +1,5 @@ -# Copyright (c) 2021 Ultimaker B.V. -# Cura is released under the terms of the LGPLv3 or higher. +# Copyright (c) 2021 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. import tempfile import json import os.path @@ -268,13 +268,11 @@ class PackageList(ListModel): package.uninstallPackageTriggered.connect(self.uninstallPackage) package.updatePackageTriggered.connect(self.updatePackage) - def installPackage(self, package_id: str) -> None: + def installPackage(self, package_id: str, url: str) -> None: """Install a package from the Marketplace :param package_id: the package identification string """ - package = self.getPackageModel(package_id) - url = package.download_url self.download(package_id, url, False) def uninstallPackage(self, package_id: str) -> None: @@ -282,7 +280,6 @@ class PackageList(ListModel): :param package_id: the package identification string """ - package = self.getPackageModel(package_id) self._manager.removePackage(package_id) self.unsunscribeUserFromPackage(package_id) @@ -291,7 +288,6 @@ class PackageList(ListModel): :param package_id: the package identification string """ - package = self.getPackageModel(package_id) self._manager.removePackage(package_id, force_add = True) - url = package.download_url + url = self._manager.packagesWithUpdate[package_id]["download_url"] self.download(package_id, url, True) diff --git a/plugins/Marketplace/PackageModel.py b/plugins/Marketplace/PackageModel.py index aa63d6da13..e2d785606d 100644 --- a/plugins/Marketplace/PackageModel.py +++ b/plugins/Marketplace/PackageModel.py @@ -1,5 +1,5 @@ -# Copyright (c) 2021 Ultimaker B.V. -# Cura is released under the terms of the LGPLv3 or higher. +# Copyright (c) 2021 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. import re from enum import Enum @@ -45,7 +45,7 @@ class PackageModel(QObject): self._description = package_data.get("description", "") self._formatted_description = self._format(self._description) - self.download_url = package_data.get("download_url", "") + self._download_url = package_data.get("download_url", "") self._release_notes = package_data.get("release_notes", "") # Not used yet, propose to add to description? subdata = package_data.get("data", {}) @@ -72,32 +72,21 @@ class PackageModel(QObject): self.sdk_version = package_data.get("sdk_version_semver", "") # Note that there's a lot more info in the package_data than just these specified here. - def install_clicked(package_id): - self._install_status_changing = True - self.setIsInstalling(True) - - self.installPackageTriggered.connect(install_clicked) - - def uninstall_clicked(package_id): - self._install_status_changing = False - self.setIsInstalling(True) - - self.uninstallPackageTriggered.connect(uninstall_clicked) - - def update_clicked(package_id): - self.setIsUpdating(True) - - self.updatePackageTriggered.connect(update_clicked) + self.enablePackageTriggered.connect(self._plugin_registry.enablePlugin) + self.disablePackageTriggered.connect(self._plugin_registry.disablePlugin) + self.installPackageTriggered.connect(lambda pkg_id: self.setIsInstalling(True)) + self.uninstallPackageTriggered.connect(lambda pkg_id: self.setIsInstalling(True)) + self.updatePackageTriggered.connect(lambda pkg_id: self.setIsUpdating(True)) def finished_installed(): self.setIsUpdating(False) self.setIsInstalling(False) self._package_manager.installedPackagesChanged.connect(finished_installed) - self.enablePackageTriggered.connect(self._plugin_registry.enablePlugin) - self.disablePackageTriggered.connect(self._plugin_registry.disablePlugin) self._plugin_registry.hasPluginsEnabledOrDisabledChanged.connect(self.stateManageButtonChanged) + self._package_manager.packagesWithUpdateChanged.connect(lambda : self.setCanUpdate(self._package_id in self._package_manager.packagesWithUpdate)) + def __eq__(self, other: object): if isinstance(other, PackageModel): return other == self @@ -298,11 +287,15 @@ class PackageModel(QObject): def isBundled(self) -> bool: return self._is_bundled + @pyqtProperty(str, constant = True) + def downloadURL(self) -> str: + return self._download_url + # --- manage buttons signals --- stateManageButtonChanged = pyqtSignal() - installPackageTriggered = pyqtSignal(str) + installPackageTriggered = pyqtSignal(str, str) uninstallPackageTriggered = pyqtSignal(str) @@ -314,7 +307,6 @@ class PackageModel(QObject): @pyqtProperty(bool, notify = stateManageButtonChanged) def isActive(self): - Logger.debug(f"getDisabledPlugins = {self._plugin_registry.getDisabledPlugins()}") return not self._package_id in self._plugin_registry.getDisabledPlugins() def setIsInstalling(self, value: bool) -> None: @@ -326,15 +318,6 @@ class PackageModel(QObject): def isInstalling(self) -> bool: return self._is_installing - def setInstallStatusChanging(self, value: bool) -> None: - if value != self._install_status_changing: - self._install_status_changing = value - self.stateManageButtonChanged.emit() - - @pyqtProperty(bool, fset = setInstallStatusChanging, notify = stateManageButtonChanged) - def installStatusChanging(self) -> bool: - return self._install_status_changing - @pyqtProperty(bool, notify = stateManageButtonChanged) def isInstalled(self) -> bool: return self._package_id in self._package_manager.local_packages_id diff --git a/plugins/Marketplace/resources/qml/PackageCardHeader.qml b/plugins/Marketplace/resources/qml/PackageCardHeader.qml index 5a693ece83..ec33505391 100644 --- a/plugins/Marketplace/resources/qml/PackageCardHeader.qml +++ b/plugins/Marketplace/resources/qml/PackageCardHeader.qml @@ -254,7 +254,7 @@ Item ManageButton { id: updateManageButton - visible: (showManageButtons || confirmed) && packageData.canUpdate && !installManageButton.confirmed + visible: (showManageButtons || confirmed) && (packageData.canUpdate || confirmed) && !installManageButton.confirmed enabled: !installManageButton.busy busy: packageData.isUpdating @@ -265,12 +265,12 @@ Item text: { - if (packageData.isUpdating) { return catalog.i18nc("@button", "Updating..."); } - else if (packageData.isRecentlyUpdated) { return catalog.i18nc("@button", "Updated"); } + if (busy) { return catalog.i18nc("@button", "Updating..."); } + else if (confirmed) { return catalog.i18nc("@button", "Updated"); } else { return catalog.i18nc("@button", "Update"); } } - onClicked: packageData.updatePackageTriggered(packageData.packageId) + onClicked: packageData.updatePackageTriggered(packageData.packageId, packageData.downloadURL) } } } From e0754c7015be45a652274f237bd0e8517d752bcc Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Fri, 10 Dec 2021 11:16:08 +0100 Subject: [PATCH 141/195] Disabled old toolbox functionality for updating The signals messed up the new update process. Contributes to CURA-8587 and relates to CURA-8588 --- plugins/Toolbox/src/Toolbox.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/Toolbox/src/Toolbox.py b/plugins/Toolbox/src/Toolbox.py index e525a88d89..d0bfb0efd4 100644 --- a/plugins/Toolbox/src/Toolbox.py +++ b/plugins/Toolbox/src/Toolbox.py @@ -1,5 +1,5 @@ -# Copyright (c) 2021 Ultimaker B.V. -# Toolbox is released under the terms of the LGPLv3 or higher. +# Copyright (c) 2021 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. import json import os @@ -634,8 +634,8 @@ class Toolbox(QObject, Extension): self._models[request_type].setFilter({"tags": "generic"}) elif request_type == "updates": # Tell the package manager that there's a new set of updates available. - packages = set([pkg["package_id"] for pkg in self._server_response_data[request_type]]) - self._package_manager.setPackagesWithUpdate(packages) + packages = self._server_response_data[request_type] + # self._package_manager.setPackagesWithUpdate(dict(zip([p['package_id'] for p in packages], [p for p in packages]))) self.metadataChanged.emit() From ec8254bca969be904a7c731845d2b435f671e038 Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Fri, 10 Dec 2021 11:50:02 +0100 Subject: [PATCH 142/195] Fixed old Marketplace update functionality Contributes to CURA-8587 --- plugins/Toolbox/src/Toolbox.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/Toolbox/src/Toolbox.py b/plugins/Toolbox/src/Toolbox.py index d0bfb0efd4..5644bace7a 100644 --- a/plugins/Toolbox/src/Toolbox.py +++ b/plugins/Toolbox/src/Toolbox.py @@ -635,7 +635,7 @@ class Toolbox(QObject, Extension): elif request_type == "updates": # Tell the package manager that there's a new set of updates available. packages = self._server_response_data[request_type] - # self._package_manager.setPackagesWithUpdate(dict(zip([p['package_id'] for p in packages], [p for p in packages]))) + self._package_manager.setPackagesWithUpdate(dict(zip([p['package_id'] for p in packages], [p for p in packages]))) self.metadataChanged.emit() @@ -645,7 +645,7 @@ class Toolbox(QObject, Extension): # This function goes through all known remote versions of a package and notifies the package manager of this change def _notifyPackageManager(self): for package in self._server_response_data["packages"]: - self._package_manager.addAvailablePackageVersion(package["package_id"], Version(package["package_version"])) + self._package_manager.addAvailablePackageVersion(package["package_id"], Version(package["package_version"]), package) def _onDownloadFinished(self, reply: "QNetworkReply") -> None: self.resetDownload() From 9b7731a21be9d11dbe0df4243fdb160df7376d12 Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Fri, 10 Dec 2021 11:50:38 +0100 Subject: [PATCH 143/195] Use package_infos to obtain the url Contributes to CURA-8587 --- plugins/Marketplace/PackageList.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/Marketplace/PackageList.py b/plugins/Marketplace/PackageList.py index c245b44df9..9d1fb08488 100644 --- a/plugins/Marketplace/PackageList.py +++ b/plugins/Marketplace/PackageList.py @@ -289,5 +289,5 @@ class PackageList(ListModel): :param package_id: the package identification string """ self._manager.removePackage(package_id, force_add = True) - url = self._manager.packagesWithUpdate[package_id]["download_url"] + url = self._manager.package_infosWithUpdate[package_id]["download_url"] self.download(package_id, url, True) From a571e875530746c4b728312f0fae07a1c673a7a7 Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Fri, 10 Dec 2021 12:39:44 +0100 Subject: [PATCH 144/195] Fixed the spinner Contributes to CURA-8587 --- plugins/Marketplace/PackageModel.py | 17 ++++++++++++----- .../Marketplace/resources/qml/ManageButton.qml | 2 +- .../resources/qml/PackageCardHeader.qml | 4 ++-- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/plugins/Marketplace/PackageModel.py b/plugins/Marketplace/PackageModel.py index e2d785606d..6a7c943e2c 100644 --- a/plugins/Marketplace/PackageModel.py +++ b/plugins/Marketplace/PackageModel.py @@ -64,23 +64,30 @@ class PackageModel(QObject): self._icon_url = author_data.get("icon_url", "") self._is_installing = False + self._recently_installed = False self._install_status_changing = False self._can_update = False self._is_updating = False + self._recently_updated = False self._section_title = section_title self.sdk_version = package_data.get("sdk_version_semver", "") # Note that there's a lot more info in the package_data than just these specified here. self.enablePackageTriggered.connect(self._plugin_registry.enablePlugin) self.disablePackageTriggered.connect(self._plugin_registry.disablePlugin) - self.installPackageTriggered.connect(lambda pkg_id: self.setIsInstalling(True)) + self.installPackageTriggered.connect(lambda pkg_id, url: self.setIsInstalling(True)) self.uninstallPackageTriggered.connect(lambda pkg_id: self.setIsInstalling(True)) self.updatePackageTriggered.connect(lambda pkg_id: self.setIsUpdating(True)) def finished_installed(): - self.setIsUpdating(False) - self.setIsInstalling(False) + if self.isInstalling: + self._recently_installed = True + self.setIsInstalling(False) + if self.isUpdating: + self._recently_updated = True + self._can_update = not self._package_id in self._package_manager.getPackagesToInstall() + self.setIsUpdating(False) self._package_manager.installedPackagesChanged.connect(finished_installed) self._plugin_registry.hasPluginsEnabledOrDisabledChanged.connect(self.stateManageButtonChanged) @@ -324,7 +331,7 @@ class PackageModel(QObject): @pyqtProperty(bool, notify = stateManageButtonChanged) def isRecentlyInstalled(self) -> bool: - return self._package_id in self._package_manager.getPackagesToInstall() + return self._recently_installed and self._package_id in self._package_manager.getPackagesToInstall() @pyqtProperty(bool, notify = stateManageButtonChanged) def isRecentlyUninstalled(self) -> bool: @@ -346,7 +353,7 @@ class PackageModel(QObject): @pyqtProperty(bool, notify = stateManageButtonChanged) def isRecentlyUpdated(self): - return self._package_id in self._package_manager.getPackagesToInstall() and self._package_id in self._package_manager.getPackagesToRemove() + return self._recently_updated and self._package_id in self._package_manager.getPackagesToInstall() def setCanUpdate(self, value: bool) -> None: if value != self._can_update: diff --git a/plugins/Marketplace/resources/qml/ManageButton.qml b/plugins/Marketplace/resources/qml/ManageButton.qml index 2c05af02e9..ac7577b543 100644 --- a/plugins/Marketplace/resources/qml/ManageButton.qml +++ b/plugins/Marketplace/resources/qml/ManageButton.qml @@ -61,7 +61,7 @@ Item RotationAnimator { target: busyIndicator - running: busyMessage.visible + running: parent.visible from: 0 to: 360 loops: Animation.Infinite diff --git a/plugins/Marketplace/resources/qml/PackageCardHeader.qml b/plugins/Marketplace/resources/qml/PackageCardHeader.qml index ec33505391..56cfe4fa61 100644 --- a/plugins/Marketplace/resources/qml/PackageCardHeader.qml +++ b/plugins/Marketplace/resources/qml/PackageCardHeader.qml @@ -242,7 +242,7 @@ Item { if (primary_action) { - packageData.installPackageTriggered(packageData.packageId) + packageData.installPackageTriggered(packageData.packageId, packageData.downloadURL) } else { @@ -270,7 +270,7 @@ Item else { return catalog.i18nc("@button", "Update"); } } - onClicked: packageData.updatePackageTriggered(packageData.packageId, packageData.downloadURL) + onClicked: packageData.updatePackageTriggered(packageData.packageId) } } } From 11c2ccd227b4971e5657b62d2c089643469821a8 Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Fri, 10 Dec 2021 12:41:37 +0100 Subject: [PATCH 145/195] Use the ongoing_request queue Contributes to CURA-8587 --- plugins/Marketplace/RemotePackageList.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/Marketplace/RemotePackageList.py b/plugins/Marketplace/RemotePackageList.py index 5325fc8640..7cbd00ad76 100644 --- a/plugins/Marketplace/RemotePackageList.py +++ b/plugins/Marketplace/RemotePackageList.py @@ -1,5 +1,5 @@ -# Copyright (c) 2021 Ultimaker B.V. -# Cura is released under the terms of the LGPLv3 or higher. +# Copyright (c) 2021 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot from PyQt5.QtNetwork import QNetworkReply @@ -131,7 +131,7 @@ class RemotePackageList(PackageList): return self._request_url = response_data["links"].get("next", "") # Use empty string to signify that there is no next page. - self._ongoing_request = None + self._ongoing_requests["get_packages"] = None self.setIsLoading(False) self.setHasMore(self._request_url != "") @@ -143,9 +143,9 @@ class RemotePackageList(PackageList): """ if error == QNetworkReply.NetworkError.OperationCanceledError: Logger.debug("Cancelled request for packages.") - self._ongoing_request = None + self._ongoing_requests["get_packages"] = None return # Don't show an error about this to the user. Logger.error("Could not reach Marketplace server.") self.setErrorMessage(catalog.i18nc("@info:error", "Could not reach Marketplace.")) - self._ongoing_request = None + self._ongoing_requests["get_packages"] = None self.setIsLoading(False) From d876b85259a5346dbcadb9679a954be94dd81bed Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Fri, 10 Dec 2021 12:43:42 +0100 Subject: [PATCH 146/195] Don't forcefully remove bundled packages when updating Contributes to CURA-8587 --- plugins/Marketplace/PackageList.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/Marketplace/PackageList.py b/plugins/Marketplace/PackageList.py index 9d1fb08488..ca3d4aff41 100644 --- a/plugins/Marketplace/PackageList.py +++ b/plugins/Marketplace/PackageList.py @@ -288,6 +288,6 @@ class PackageList(ListModel): :param package_id: the package identification string """ - self._manager.removePackage(package_id, force_add = True) + self._manager.removePackage(package_id, force_add = not self._manager.isBundledPackage(package_id)) url = self._manager.package_infosWithUpdate[package_id]["download_url"] self.download(package_id, url, True) From e72655cc22b0bb8f27aae82afcf6c2ee1298114e Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Fri, 10 Dec 2021 17:38:20 +0100 Subject: [PATCH 147/195] Moved busy/confirmed logic to QML COntributes to CURA-8587 --- plugins/Marketplace/PackageList.py | 14 +- plugins/Marketplace/PackageModel.py | 141 ++++++++++-------- .../resources/qml/PackageCardHeader.qml | 74 ++++++--- 3 files changed, 132 insertions(+), 97 deletions(-) diff --git a/plugins/Marketplace/PackageList.py b/plugins/Marketplace/PackageList.py index ca3d4aff41..390bf841df 100644 --- a/plugins/Marketplace/PackageList.py +++ b/plugins/Marketplace/PackageList.py @@ -165,7 +165,7 @@ class PackageList(ListModel): if dialog is not None: dialog.deleteLater() # reset package card - package = self.getPackageModel(package_id) + self._manager.packageInstallingFailed.emit(package_id) def _requestInstall(self, package_id: str, update: bool = False) -> None: package_path = self._to_install[package_id] @@ -182,8 +182,9 @@ class PackageList(ListModel): def _install(self, package_id: str, update: bool = False) -> None: package_path = self._to_install.pop(package_id) to_be_installed = self._manager.installPackage(package_path) is not None + if not to_be_installed: + return package = self.getPackageModel(package_id) - # TODO handle failure self.subscribeUserToPackage(package_id, str(package.sdk_version)) def download(self, package_id: str, url: str, update: bool = False) -> None: @@ -231,14 +232,7 @@ class PackageList(ListModel): if reply: reply_string = bytes(reply.readAll()).decode() Logger.error(f"Failed to download package: {package_id} due to {reply_string}") - try: - package = self.getPackageModel(package_id) - # TODO: handle error - except RuntimeError: - # Setting the ownership of this object to not qml can still result in a RuntimeError. Which can occur when quickly toggling - # between de-/constructing Remote or Local PackageLists. This try-except is here to prevent a hard crash when the wrapped C++ object - # was deleted when it was still parsing the response - return + self._manager.packageInstallingFailed.emit(package_id) def subscribeUserToPackage(self, package_id: str, sdk_version: str) -> None: """Subscribe the user (if logged in) to the package for a given SDK diff --git a/plugins/Marketplace/PackageModel.py b/plugins/Marketplace/PackageModel.py index 6a7c943e2c..de2732f147 100644 --- a/plugins/Marketplace/PackageModel.py +++ b/plugins/Marketplace/PackageModel.py @@ -16,6 +16,7 @@ from UM.PluginRegistry import PluginRegistry catalog = i18nCatalog("cura") + class PackageModel(QObject): """ Represents a package, containing all the relevant information to be displayed about a package. @@ -38,7 +39,8 @@ class PackageModel(QObject): self._icon_url = package_data.get("icon_url", "") self._display_name = package_data.get("display_name", catalog.i18nc("@label:property", "Unknown Package")) tags = package_data.get("tags", []) - self._is_checked_by_ultimaker = (self._package_type == "plugin" and "verified" in tags) or (self._package_type == "material" and "certified" in tags) + self._is_checked_by_ultimaker = (self._package_type == "plugin" and "verified" in tags) or ( + self._package_type == "material" and "certified" in tags) self._package_version = package_data.get("package_version", "") # Display purpose, no need for 'UM.Version'. self._package_info_url = package_data.get("website", "") # Not to be confused with 'download_url'. self._download_count = package_data.get("download_count", 0) @@ -63,38 +65,32 @@ class PackageModel(QObject): if not self._icon_url or self._icon_url == "": self._icon_url = author_data.get("icon_url", "") - self._is_installing = False - self._recently_installed = False - self._install_status_changing = False - self._can_update = False - self._is_updating = False - self._recently_updated = False self._section_title = section_title self.sdk_version = package_data.get("sdk_version_semver", "") # Note that there's a lot more info in the package_data than just these specified here. self.enablePackageTriggered.connect(self._plugin_registry.enablePlugin) self.disablePackageTriggered.connect(self._plugin_registry.disablePlugin) - self.installPackageTriggered.connect(lambda pkg_id, url: self.setIsInstalling(True)) - self.uninstallPackageTriggered.connect(lambda pkg_id: self.setIsInstalling(True)) - self.updatePackageTriggered.connect(lambda pkg_id: self.setIsUpdating(True)) - def finished_installed(): - if self.isInstalling: - self._recently_installed = True - self.setIsInstalling(False) - if self.isUpdating: - self._recently_updated = True - self._can_update = not self._package_id in self._package_manager.getPackagesToInstall() - self.setIsUpdating(False) + self._is_updating = False + self._is_recently_updated = self._getRecentlyUpdated() + + self._is_installing = False + self._is_uninstalling = False + self._is_recently_installed = self._getRecentlyInstalled() + + self.updatePackageTriggered.connect(lambda pkg: self._setIsUpdating(True)) + self.installPackageTriggered.connect(lambda pkg, url: self._setIsInstalling(True)) + self.uninstallPackageTriggered.connect(lambda pkg: self._setIsUninstalling(True)) - self._package_manager.installedPackagesChanged.connect(finished_installed) self._plugin_registry.hasPluginsEnabledOrDisabledChanged.connect(self.stateManageButtonChanged) + self._package_manager.packageInstalled.connect(lambda pkg_id: self._packageInstalled(pkg_id, True)) + self._package_manager.packageUninstalled.connect(lambda pkg_id: self._packageInstalled(pkg_id, True)) + self._package_manager.packageInstallingFailed.connect(lambda pkg_id: self._packageInstalled(pkg_id, False)) + self._package_manager.packagesWithUpdateChanged.connect(lambda: self.setCanUpdate(self._package_id in self._package_manager.packagesWithUpdate)) - self._package_manager.packagesWithUpdateChanged.connect(lambda : self.setCanUpdate(self._package_id in self._package_manager.packagesWithUpdate)) - - def __eq__(self, other: object): + def __eq__(self, other: object) -> bool: if isinstance(other, PackageModel): return other == self elif isinstance(other, str): @@ -102,7 +98,7 @@ class PackageModel(QObject): else: return False - def __repr__(self): + def __repr__(self) -> str: return f"<{self._package_id} : {self._package_version} : {self._section_title}>" def _findLink(self, subdata: Dict[str, Any], link_type: str) -> str: @@ -218,8 +214,8 @@ class PackageModel(QObject): def packageType(self) -> str: return self._package_type - @pyqtProperty(str, constant=True) - def iconUrl(self): + @pyqtProperty(str, constant = True) + def iconUrl(self) -> str: return self._icon_url @pyqtProperty(str, constant = True) @@ -230,32 +226,32 @@ class PackageModel(QObject): def isCheckedByUltimaker(self): return self._is_checked_by_ultimaker - @pyqtProperty(str, constant=True) - def packageVersion(self): + @pyqtProperty(str, constant = True) + def packageVersion(self) -> str: return self._package_version - @pyqtProperty(str, constant=True) - def packageInfoUrl(self): + @pyqtProperty(str, constant = True) + def packageInfoUrl(self) -> str: return self._package_info_url - @pyqtProperty(int, constant=True) - def downloadCount(self): + @pyqtProperty(int, constant = True) + def downloadCount(self) -> str: return self._download_count - @pyqtProperty(str, constant=True) - def description(self): + @pyqtProperty(str, constant = True) + def description(self) -> str: return self._description @pyqtProperty(str, constant = True) def formattedDescription(self) -> str: return self._formatted_description - @pyqtProperty(str, constant=True) - def authorName(self): + @pyqtProperty(str, constant = True) + def authorName(self) -> str: return self._author_name - @pyqtProperty(str, constant=True) - def authorInfoUrl(self): + @pyqtProperty(str, constant = True) + def authorInfoUrl(self) -> str: return self._author_info_url @pyqtProperty(str, constant = True) @@ -312,49 +308,62 @@ class PackageModel(QObject): disablePackageTriggered = pyqtSignal(str) - @pyqtProperty(bool, notify = stateManageButtonChanged) - def isActive(self): - return not self._package_id in self._plugin_registry.getDisabledPlugins() + installedPackagesChanged = pyqtSignal(bool) - def setIsInstalling(self, value: bool) -> None: - if value != self._is_installing: - self._is_installing = value - self.stateManageButtonChanged.emit() + uninstalledPackagesChanged = pyqtSignal(bool) - @pyqtProperty(bool, fset = setIsInstalling, notify = stateManageButtonChanged) - def isInstalling(self) -> bool: - return self._is_installing + updatePackagesChanged = pyqtSignal(bool) + + def _setIsUpdating(self, value: bool) -> None: + self._is_updating = value + + def _setIsInstalling(self, value: bool) -> None: + self._is_installing = value + + def _setIsUninstalling(self, value: bool) -> None: + self._is_uninstalling = value + + def _packageInstalled(self, package_id: str, success: bool) -> None: + if self._package_id != package_id: + return + if self._is_updating: + self.updatePackagesChanged.emit(success) + self._is_updating = False + if self._is_installing: + self.installedPackagesChanged.emit(success) + self._is_installing = False + if self._is_uninstalling: + self.uninstalledPackagesChanged.emit(success) + self._is_uninstalling = False + + def _getRecentlyInstalled(self) -> bool: + return (self._package_id in self._package_manager.getPackagesToInstall() or self._package_id in self._package_manager.getPackagesToRemove()) \ + and self._package_id not in self._package_manager.package_infosWithUpdate + + @pyqtProperty(bool, constant = True) + def isRecentlyInstalledChanged(self) -> bool: + return self._is_recently_installed + + def _getRecentlyUpdated(self) -> bool: + return self._package_id in self._package_manager.package_infosWithUpdate and self._package_id in self._package_manager.getPackagesToInstall() + + @pyqtProperty(bool, constant = True) + def isRecentlyUpdatedChanged(self) -> bool: + return self._is_recently_updated @pyqtProperty(bool, notify = stateManageButtonChanged) def isInstalled(self) -> bool: return self._package_id in self._package_manager.local_packages_id @pyqtProperty(bool, notify = stateManageButtonChanged) - def isRecentlyInstalled(self) -> bool: - return self._recently_installed and self._package_id in self._package_manager.getPackagesToInstall() - - @pyqtProperty(bool, notify = stateManageButtonChanged) - def isRecentlyUninstalled(self) -> bool: - return self._package_id in self._package_manager.getPackagesToRemove() + def isActive(self) -> bool: + return not self._package_id in self._plugin_registry.getDisabledPlugins() @pyqtProperty(bool, notify = stateManageButtonChanged) def canDowngrade(self) -> bool: """Flag if the installed package can be downgraded to a bundled version""" return self._package_manager.canDowngrade(self._package_id) - def setIsUpdating(self, value): - if value != self._is_updating: - self._is_updating = value - self.stateManageButtonChanged.emit() - - @pyqtProperty(bool, fset = setIsUpdating, notify = stateManageButtonChanged) - def isUpdating(self): - return self._is_updating - - @pyqtProperty(bool, notify = stateManageButtonChanged) - def isRecentlyUpdated(self): - return self._recently_updated and self._package_id in self._package_manager.getPackagesToInstall() - def setCanUpdate(self, value: bool) -> None: if value != self._can_update: self._can_update = value diff --git a/plugins/Marketplace/resources/qml/PackageCardHeader.qml b/plugins/Marketplace/resources/qml/PackageCardHeader.qml index 56cfe4fa61..627fc27d46 100644 --- a/plugins/Marketplace/resources/qml/PackageCardHeader.qml +++ b/plugins/Marketplace/resources/qml/PackageCardHeader.qml @@ -209,45 +209,62 @@ Item { id: installManageButton visible: (showManageButtons || confirmed) && ((packageData.isBundled && packageData.canDowngrade) || !packageData.isBundled) && !updateManageButton.confirmed + enabled: !updateManageButton.busy - enabled: !packageData.isUpdating + busy: false + confirmed: { packageData.isRecentlyInstalledChanged } - busy: packageData.isInstalling - confirmed: packageData.isRecentlyInstalled || packageData.isRecentlyUninstalled - - button_style: !packageData.isInstalled + button_style: confirmed ? packageData.isInstalled : !packageData.isInstalled Layout.alignment: Qt.AlignTop text: { - if (packageData.canDowngrade) { return catalog.i18nc("@button", "Downgrade"); } - if (packageData.isRecentlyInstalled) { return catalog.i18nc("@button", "Installed"); } - if (packageData.isRecentlyUninstalled) - { - if (packageData.canDowngrade) { return catalog.i18nc("@button", "Downgraded") } - else { return catalog.i18nc("@button", "Uninstalled"); } } if (button_style) { - if (packageData.isInstalling) { return catalog.i18nc("@button", "Installing..."); } + if (busy) { return catalog.i18nc("@button", "Installing..."); } + else if (confirmed) { return catalog.i18nc("@button", "Installed"); } else { return catalog.i18nc("@button", "Install"); } } else { - if (packageData.isInstalling) { return catalog.i18nc("@button", "Uninstalling..."); } - else { return catalog.i18nc("@button", "Uninstall"); } + if (packageData.canDowngrade) + { + if (busy) { return catalog.i18nc("@button", "Downgrading..."); } + else if (confirmed) { return catalog.i18nc("@button", "Downgraded"); } + else { return catalog.i18nc("@button", "Downgrade"); } + } + else + { + if (busy) { return catalog.i18nc("@button", "Uninstalling..."); } + else if (confirmed) { return catalog.i18nc("@button", "Uninstalled"); } + else { return catalog.i18nc("@button", "Uninstall"); } + } } } onClicked: { - if (primary_action) + busy = true + if (primary_action){ packageData.installPackageTriggered(packageData.packageId, packageData.downloadURL); } + else { packageData.uninstallPackageTriggered(packageData.packageId); } + } + + Connections + { + target: packageData + + function onInstalledPackagesChanged(success) { - packageData.installPackageTriggered(packageData.packageId, packageData.downloadURL) + installManageButton.busy = false; + installManageButton.confirmed = success; } - else + function onUninstalledPackagesChanged(success) { - packageData.uninstallPackageTriggered(packageData.packageId) + installManageButton.busy = false; + installManageButton.confirmed = success; + installManageButton.button_style = !installManageButton.button_style; } + } } @@ -257,8 +274,8 @@ Item visible: (showManageButtons || confirmed) && (packageData.canUpdate || confirmed) && !installManageButton.confirmed enabled: !installManageButton.busy - busy: packageData.isUpdating - confirmed: packageData.isRecentlyUpdated + busy: false + confirmed: { packageData.isRecentlyUpdatedChanged } button_style: true Layout.alignment: Qt.AlignTop @@ -270,7 +287,22 @@ Item else { return catalog.i18nc("@button", "Update"); } } - onClicked: packageData.updatePackageTriggered(packageData.packageId) + onClicked: + { + busy = true; + packageData.updatePackageTriggered(packageData.packageId); + } + + Connections + { + target: packageData + + function onUpdatePackagesChanged(succes) + { + updateManageButton.busy = false; + updateManageButton.confirmed = succes; + } + } } } } From 4f997319b0a71052b97b789481c35d4c3bcfa007 Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Fri, 10 Dec 2021 18:31:55 +0100 Subject: [PATCH 148/195] Try sorting on section, update and display name Contributes to CURA-8587 --- plugins/Marketplace/LocalPackageList.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/plugins/Marketplace/LocalPackageList.py b/plugins/Marketplace/LocalPackageList.py index 83aac65516..50673818bb 100644 --- a/plugins/Marketplace/LocalPackageList.py +++ b/plugins/Marketplace/LocalPackageList.py @@ -35,11 +35,16 @@ class LocalPackageList(PackageList): } } # The section headers to be used for the different package categories + def __init__(self, parent: Optional["QObject"] = None) -> None: super().__init__(parent) self._has_footer = False self._ongoing_requests["check_updates"] = None - self._manager.packagesWithUpdateChanged.connect(lambda: self.sort(attrgetter("sectionTitle", "_can_update", "displayName"), key = "package", reverse = True)) + self._manager.packagesWithUpdateChanged.connect(self._sortSectionsOnUpdate) + + def _sortSectionsOnUpdate(self) -> None: + SECTION_ORDER = dict(zip([i for k, v in self.PACKAGE_CATEGORIES.items() for i in self.PACKAGE_CATEGORIES[k].values()], ["a", "b", "c", "d"])) + self.sort(lambda model: f"{SECTION_ORDER[model.sectionTitle]}_{model._can_update}_{model.displayName}".lower(), key = "package") @pyqtSlot() def updatePackages(self) -> None: @@ -52,7 +57,7 @@ class LocalPackageList(PackageList): # Obtain and sort the local packages self.setItems([{"package": p} for p in [self._makePackageModel(p) for p in self._manager.local_packages]]) - self.sort(attrgetter("sectionTitle", "_can_update", "displayName"), key = "package", reverse = True) + self._sortSectionsOnUpdate() self.checkForUpdates(self._manager.local_packages) self.setIsLoading(False) From eb11e92637fce88d12557959fea85c1a1cf9e1af Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 13 Dec 2021 11:54:13 +0100 Subject: [PATCH 149/195] Rename local_packages_id to local_packages_ids CURA-8587 --- cura/CuraPackageManager.py | 10 +++++----- plugins/Marketplace/PackageModel.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cura/CuraPackageManager.py b/cura/CuraPackageManager.py index fcbf5f6ea6..535d331a62 100644 --- a/cura/CuraPackageManager.py +++ b/cura/CuraPackageManager.py @@ -20,12 +20,12 @@ class CuraPackageManager(PackageManager): def __init__(self, application: "QtApplication", parent: Optional["QObject"] = None) -> None: super().__init__(application, parent) self._local_packages: Optional[List[Dict[str, Any]]] = None - self._local_packages_id: Optional[Set[str]] = None + self._local_packages_ids: Optional[Set[str]] = None self.installedPackagesChanged.connect(self._updateLocalPackages) def _updateLocalPackages(self) -> None: self._local_packages = self.getAllLocalPackages() - self._local_packages_id = set(pkg["package_id"] for pkg in self._local_packages) + self._local_packages_ids = set(pkg["package_id"] for pkg in self._local_packages) @property def local_packages(self) -> List[Dict[str, Any]]: @@ -37,13 +37,13 @@ class CuraPackageManager(PackageManager): return cast(List[Dict[str, Any]], self._local_packages) @property - def local_packages_id(self) -> Set[str]: + def local_packages_ids(self) -> Set[str]: """locally installed packages, lazy execution""" - if self._local_packages_id is None: + if self._local_packages_ids is None: self._updateLocalPackages() # _updateLocalPackages always results in a list of packages, not None. # It's guaranteed to be a list now. - return cast(Set[str], self._local_packages_id) + return cast(Set[str], self._local_packages_ids) def initialize(self) -> None: self._installation_dirs_dict["materials"] = Resources.getStoragePath(CuraApplication.ResourceTypes.MaterialInstanceContainer) diff --git a/plugins/Marketplace/PackageModel.py b/plugins/Marketplace/PackageModel.py index de2732f147..0e808dd369 100644 --- a/plugins/Marketplace/PackageModel.py +++ b/plugins/Marketplace/PackageModel.py @@ -353,7 +353,7 @@ class PackageModel(QObject): @pyqtProperty(bool, notify = stateManageButtonChanged) def isInstalled(self) -> bool: - return self._package_id in self._package_manager.local_packages_id + return self._package_id in self._package_manager.local_packages_ids @pyqtProperty(bool, notify = stateManageButtonChanged) def isActive(self) -> bool: From 50f1afa5d9ff282b68474e1d52b7be4b64771aef Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 13 Dec 2021 11:57:00 +0100 Subject: [PATCH 150/195] Make plugin_registry protected CURA-8587 --- plugins/Marketplace/Marketplace.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/Marketplace/Marketplace.py b/plugins/Marketplace/Marketplace.py index 5ebc7830ed..143469d82e 100644 --- a/plugins/Marketplace/Marketplace.py +++ b/plugins/Marketplace/Marketplace.py @@ -27,7 +27,7 @@ class Marketplace(Extension): def __init__(self) -> None: super().__init__() self._window: Optional["QObject"] = None # If the window has been loaded yet, it'll be cached in here. - self.plugin_registry: Optional[PluginRegistry] = None + self._plugin_registry: Optional[PluginRegistry] = None qmlRegisterType(RemotePackageList, "Marketplace", 1, 0, "RemotePackageList") qmlRegisterType(LocalPackageList, "Marketplace", 1, 0, "LocalPackageList") @@ -41,7 +41,7 @@ class Marketplace(Extension): If the window hadn't been loaded yet into Qt, it will be created lazily. """ if self._window is None: - self.plugin_registry = PluginRegistry.getInstance() + self._plugin_registry = PluginRegistry.getInstance() plugin_path = PluginRegistry.getInstance().getPluginPath(self.getPluginId()) if plugin_path is None: plugin_path = os.path.dirname(__file__) From e92cc584fb566bb847ca573296805318937f9a85 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 13 Dec 2021 12:01:37 +0100 Subject: [PATCH 151/195] Add missing return type CURA-8587 --- plugins/Marketplace/LocalPackageList.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/Marketplace/LocalPackageList.py b/plugins/Marketplace/LocalPackageList.py index 50673818bb..49a08296f5 100644 --- a/plugins/Marketplace/LocalPackageList.py +++ b/plugins/Marketplace/LocalPackageList.py @@ -74,7 +74,7 @@ class LocalPackageList(PackageList): self._connectManageButtonSignals(package) return package - def checkForUpdates(self, packages: List[Dict[str, Any]]): + def checkForUpdates(self, packages: List[Dict[str, Any]]) -> None: installed_packages = "installed_packages=".join([f"{package['package_id']}:{package['package_version']}&" for package in packages]) request_url = f"{PACKAGE_UPDATES_URL}?installed_packages={installed_packages[:-1]}" From f993243d57bf030f78f8e1e8383428e990349db0 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 13 Dec 2021 12:08:33 +0100 Subject: [PATCH 152/195] Remove unneeded import CURA-8587 --- plugins/Marketplace/resources/qml/LicenseDialog.qml | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/Marketplace/resources/qml/LicenseDialog.qml b/plugins/Marketplace/resources/qml/LicenseDialog.qml index d44ef335eb..1c99569793 100644 --- a/plugins/Marketplace/resources/qml/LicenseDialog.qml +++ b/plugins/Marketplace/resources/qml/LicenseDialog.qml @@ -6,7 +6,6 @@ import QtQuick.Dialogs 1.1 import QtQuick.Window 2.2 import QtQuick.Controls 2.3 import QtQuick.Layouts 1.3 -import QtQuick.Controls.Styles 1.4 import UM 1.6 as UM import Cura 1.6 as Cura From 08685af9de6dad4ffc9c2e21ead667ef08bcd2e5 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 13 Dec 2021 12:24:18 +0100 Subject: [PATCH 153/195] Switch ManageButton over to implicitWidth & height instead of layout This makes it a much easier to re-use component. CURA-8587 --- plugins/Marketplace/resources/qml/ManageButton.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/Marketplace/resources/qml/ManageButton.qml b/plugins/Marketplace/resources/qml/ManageButton.qml index ac7577b543..d3b86a6fa9 100644 --- a/plugins/Marketplace/resources/qml/ManageButton.qml +++ b/plugins/Marketplace/resources/qml/ManageButton.qml @@ -16,8 +16,8 @@ Item property bool busy property bool confirmed - Layout.preferredWidth: childrenRect.width - Layout.preferredHeight: childrenRect.height + implicitWidth: childrenRect.width + implicitHeight: childrenRect.height signal clicked(bool primary_action) From 274b98f9b17d14785f72fd74d303eca60d23167f Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 13 Dec 2021 13:09:17 +0100 Subject: [PATCH 154/195] Remove unneeded "primary_action" from managebutton --- plugins/Marketplace/resources/qml/ManageButton.qml | 6 +++--- .../Marketplace/resources/qml/PackageCardHeader.qml | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/plugins/Marketplace/resources/qml/ManageButton.qml b/plugins/Marketplace/resources/qml/ManageButton.qml index d3b86a6fa9..9bd1fae032 100644 --- a/plugins/Marketplace/resources/qml/ManageButton.qml +++ b/plugins/Marketplace/resources/qml/ManageButton.qml @@ -19,14 +19,14 @@ Item implicitWidth: childrenRect.width implicitHeight: childrenRect.height - signal clicked(bool primary_action) + signal clicked() property Component primaryButton: Component { Cura.PrimaryButton { text: manageButton.text - onClicked: manageButton.clicked(true) + onClicked: manageButton.clicked() } } @@ -35,7 +35,7 @@ Item Cura.SecondaryButton { text: manageButton.text - onClicked: manageButton.clicked(false) + onClicked: manageButton.clicked() } } diff --git a/plugins/Marketplace/resources/qml/PackageCardHeader.qml b/plugins/Marketplace/resources/qml/PackageCardHeader.qml index 627fc27d46..76c7f91624 100644 --- a/plugins/Marketplace/resources/qml/PackageCardHeader.qml +++ b/plugins/Marketplace/resources/qml/PackageCardHeader.qml @@ -15,7 +15,7 @@ Item default property alias contents: contentItem.children; property var packageData - property bool showManageButtons + property bool showManageButtons: false width: parent.width height: UM.Theme.getSize("card").height @@ -194,13 +194,13 @@ Item onClicked: { - if (primary_action) + if(packageData.isActive) { - packageData.enablePackageTriggered(packageData.packageId) + packageData.disablePackageTriggered(packageData.packageId) } else { - packageData.disablePackageTriggered(packageData.packageId) + packageData.enablePackageTriggered(packageData.packageId) } } } @@ -245,7 +245,7 @@ Item onClicked: { busy = true - if (primary_action){ packageData.installPackageTriggered(packageData.packageId, packageData.downloadURL); } + if (packageData.isInstalled){ packageData.installPackageTriggered(packageData.packageId, packageData.downloadURL); } else { packageData.uninstallPackageTriggered(packageData.packageId); } } From 8c0d4899ede824c1cb9b2cbd35f93a9606c11cea Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 13 Dec 2021 13:12:06 +0100 Subject: [PATCH 155/195] Remove unneeded {} CURA-8587 --- plugins/Marketplace/resources/qml/PackageCardHeader.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/Marketplace/resources/qml/PackageCardHeader.qml b/plugins/Marketplace/resources/qml/PackageCardHeader.qml index 76c7f91624..df136c88ec 100644 --- a/plugins/Marketplace/resources/qml/PackageCardHeader.qml +++ b/plugins/Marketplace/resources/qml/PackageCardHeader.qml @@ -212,7 +212,7 @@ Item enabled: !updateManageButton.busy busy: false - confirmed: { packageData.isRecentlyInstalledChanged } + confirmed: packageData.isRecentlyInstalledChanged button_style: confirmed ? packageData.isInstalled : !packageData.isInstalled Layout.alignment: Qt.AlignTop @@ -275,7 +275,7 @@ Item enabled: !installManageButton.busy busy: false - confirmed: { packageData.isRecentlyUpdatedChanged } + confirmed: packageData.isRecentlyUpdatedChanged button_style: true Layout.alignment: Qt.AlignTop From aa291144701583b67893b7ff019be1fb9c1fa5de Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 13 Dec 2021 13:13:42 +0100 Subject: [PATCH 156/195] Correctly set defaults of managebutton CURA-8587 --- plugins/Marketplace/resources/qml/ManageButton.qml | 4 ++-- plugins/Marketplace/resources/qml/PackageCardHeader.qml | 5 ----- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/plugins/Marketplace/resources/qml/ManageButton.qml b/plugins/Marketplace/resources/qml/ManageButton.qml index 9bd1fae032..87c72d8710 100644 --- a/plugins/Marketplace/resources/qml/ManageButton.qml +++ b/plugins/Marketplace/resources/qml/ManageButton.qml @@ -13,8 +13,8 @@ Item id: manageButton property bool button_style property string text - property bool busy - property bool confirmed + property bool busy: false + property bool confirmed: false implicitWidth: childrenRect.width implicitHeight: childrenRect.height diff --git a/plugins/Marketplace/resources/qml/PackageCardHeader.qml b/plugins/Marketplace/resources/qml/PackageCardHeader.qml index df136c88ec..51e6f83079 100644 --- a/plugins/Marketplace/resources/qml/PackageCardHeader.qml +++ b/plugins/Marketplace/resources/qml/PackageCardHeader.qml @@ -184,9 +184,6 @@ Item visible: showManageButtons && packageData.isInstalled && !(installManageButton.confirmed || updateManageButton.confirmed || packageData.packageType == "material") enabled: !(installManageButton.busy || updateManageButton.busy) - busy: false - confirmed: false - button_style: !packageData.isActive Layout.alignment: Qt.AlignTop @@ -211,7 +208,6 @@ Item visible: (showManageButtons || confirmed) && ((packageData.isBundled && packageData.canDowngrade) || !packageData.isBundled) && !updateManageButton.confirmed enabled: !updateManageButton.busy - busy: false confirmed: packageData.isRecentlyInstalledChanged button_style: confirmed ? packageData.isInstalled : !packageData.isInstalled @@ -274,7 +270,6 @@ Item visible: (showManageButtons || confirmed) && (packageData.canUpdate || confirmed) && !installManageButton.confirmed enabled: !installManageButton.busy - busy: false confirmed: packageData.isRecentlyUpdatedChanged button_style: true From 4b358496d916f0e99087bf11725bb05a2674abca Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 13 Dec 2021 13:15:16 +0100 Subject: [PATCH 157/195] Simplify enable/disable button visibility CURA-8587 --- plugins/Marketplace/resources/qml/PackageCardHeader.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/Marketplace/resources/qml/PackageCardHeader.qml b/plugins/Marketplace/resources/qml/PackageCardHeader.qml index 51e6f83079..55a4ae8731 100644 --- a/plugins/Marketplace/resources/qml/PackageCardHeader.qml +++ b/plugins/Marketplace/resources/qml/PackageCardHeader.qml @@ -181,7 +181,7 @@ Item ManageButton { id: enableManageButton - visible: showManageButtons && packageData.isInstalled && !(installManageButton.confirmed || updateManageButton.confirmed || packageData.packageType == "material") + visible: showManageButtons && !(installManageButton.confirmed || updateManageButton.confirmed || packageData.packageType == "material") enabled: !(installManageButton.busy || updateManageButton.busy) button_style: !packageData.isActive From 62f99a28b31e1d0fec01151f3413abf36a3b9e09 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 13 Dec 2021 14:15:16 +0100 Subject: [PATCH 158/195] Simplify the update logic in the package model / card CURA-8587 --- plugins/Marketplace/PackageModel.py | 57 +++++++++---------- .../resources/qml/PackageCardHeader.qml | 41 +++---------- 2 files changed, 36 insertions(+), 62 deletions(-) diff --git a/plugins/Marketplace/PackageModel.py b/plugins/Marketplace/PackageModel.py index 0e808dd369..ac2e2df2b2 100644 --- a/plugins/Marketplace/PackageModel.py +++ b/plugins/Marketplace/PackageModel.py @@ -5,7 +5,7 @@ import re from enum import Enum from typing import Any, cast, Dict, List, Optional -from PyQt5.QtCore import pyqtProperty, QObject, pyqtSignal +from PyQt5.QtCore import pyqtProperty, QObject, pyqtSignal, pyqtSlot from cura.CuraApplication import CuraApplication from cura.CuraPackageManager import CuraPackageManager @@ -73,16 +73,11 @@ class PackageModel(QObject): self.enablePackageTriggered.connect(self._plugin_registry.enablePlugin) self.disablePackageTriggered.connect(self._plugin_registry.disablePlugin) - self._is_updating = False self._is_recently_updated = self._getRecentlyUpdated() - - self._is_installing = False - self._is_uninstalling = False self._is_recently_installed = self._getRecentlyInstalled() self.updatePackageTriggered.connect(lambda pkg: self._setIsUpdating(True)) - self.installPackageTriggered.connect(lambda pkg, url: self._setIsInstalling(True)) - self.uninstallPackageTriggered.connect(lambda pkg: self._setIsUninstalling(True)) + self._plugin_registry.hasPluginsEnabledOrDisabledChanged.connect(self.stateManageButtonChanged) self._package_manager.packageInstalled.connect(lambda pkg_id: self._packageInstalled(pkg_id, True)) @@ -90,6 +85,8 @@ class PackageModel(QObject): self._package_manager.packageInstallingFailed.connect(lambda pkg_id: self._packageInstalled(pkg_id, False)) self._package_manager.packagesWithUpdateChanged.connect(lambda: self.setCanUpdate(self._package_id in self._package_manager.packagesWithUpdate)) + self._is_busy = False + def __eq__(self, other: object) -> bool: if isinstance(other, PackageModel): return other == self @@ -308,42 +305,44 @@ class PackageModel(QObject): disablePackageTriggered = pyqtSignal(str) - installedPackagesChanged = pyqtSignal(bool) + installed = pyqtSignal(bool) - uninstalledPackagesChanged = pyqtSignal(bool) + updated = pyqtSignal(bool) - updatePackagesChanged = pyqtSignal(bool) + busyChanged = pyqtSignal() - def _setIsUpdating(self, value: bool) -> None: - self._is_updating = value + @pyqtSlot() + def install(self): + self.setBusy(True) + self.installPackageTriggered.emit(self.packageId, self.downloadURL) - def _setIsInstalling(self, value: bool) -> None: - self._is_installing = value + @pyqtSlot() + def uninstall(self): + self.setBusy(True) + self.uninstallPackageTriggered.emit(self.packageId) - def _setIsUninstalling(self, value: bool) -> None: - self._is_uninstalling = value + @pyqtProperty(bool, notify= busyChanged) + def busy(self): + """ + Property indicating that some kind of upgrade is active. + """ + return self._is_busy + + def setBusy(self, value: bool): + if self._is_busy != value: + self._is_busy = value + self.busyChanged.emit() def _packageInstalled(self, package_id: str, success: bool) -> None: if self._package_id != package_id: return - if self._is_updating: - self.updatePackagesChanged.emit(success) - self._is_updating = False - if self._is_installing: - self.installedPackagesChanged.emit(success) - self._is_installing = False - if self._is_uninstalling: - self.uninstalledPackagesChanged.emit(success) - self._is_uninstalling = False + self.setBusy(not self._is_busy) + self.stateManageButtonChanged.emit() def _getRecentlyInstalled(self) -> bool: return (self._package_id in self._package_manager.getPackagesToInstall() or self._package_id in self._package_manager.getPackagesToRemove()) \ and self._package_id not in self._package_manager.package_infosWithUpdate - @pyqtProperty(bool, constant = True) - def isRecentlyInstalledChanged(self) -> bool: - return self._is_recently_installed - def _getRecentlyUpdated(self) -> bool: return self._package_id in self._package_manager.package_infosWithUpdate and self._package_id in self._package_manager.getPackagesToInstall() diff --git a/plugins/Marketplace/resources/qml/PackageCardHeader.qml b/plugins/Marketplace/resources/qml/PackageCardHeader.qml index 55a4ae8731..96335ecc9d 100644 --- a/plugins/Marketplace/resources/qml/PackageCardHeader.qml +++ b/plugins/Marketplace/resources/qml/PackageCardHeader.qml @@ -207,18 +207,15 @@ Item id: installManageButton visible: (showManageButtons || confirmed) && ((packageData.isBundled && packageData.canDowngrade) || !packageData.isBundled) && !updateManageButton.confirmed enabled: !updateManageButton.busy - - confirmed: packageData.isRecentlyInstalledChanged - - button_style: confirmed ? packageData.isInstalled : !packageData.isInstalled + busy: packageData.busy + button_style: !packageData.isInstalled Layout.alignment: Qt.AlignTop text: { - if (button_style) + if (!packageData.isInstalled) { if (busy) { return catalog.i18nc("@button", "Installing..."); } - else if (confirmed) { return catalog.i18nc("@button", "Installed"); } else { return catalog.i18nc("@button", "Install"); } } else @@ -226,41 +223,19 @@ Item if (packageData.canDowngrade) { if (busy) { return catalog.i18nc("@button", "Downgrading..."); } - else if (confirmed) { return catalog.i18nc("@button", "Downgraded"); } else { return catalog.i18nc("@button", "Downgrade"); } } else { - if (busy) { return catalog.i18nc("@button", "Uninstalling..."); } - else if (confirmed) { return catalog.i18nc("@button", "Uninstalled"); } - else { return catalog.i18nc("@button", "Uninstall"); } + return catalog.i18nc("@button", "Uninstall"); } } } onClicked: { - busy = true - if (packageData.isInstalled){ packageData.installPackageTriggered(packageData.packageId, packageData.downloadURL); } - else { packageData.uninstallPackageTriggered(packageData.packageId); } - } - - Connections - { - target: packageData - - function onInstalledPackagesChanged(success) - { - installManageButton.busy = false; - installManageButton.confirmed = success; - } - function onUninstalledPackagesChanged(success) - { - installManageButton.busy = false; - installManageButton.confirmed = success; - installManageButton.button_style = !installManageButton.button_style; - } - + if (packageData.isInstalled){ packageData.uninstall() } + else { packageData.install()} } } @@ -284,7 +259,7 @@ Item onClicked: { - busy = true; + busy = true packageData.updatePackageTriggered(packageData.packageId); } @@ -292,7 +267,7 @@ Item { target: packageData - function onUpdatePackagesChanged(succes) + function updated(succes) { updateManageButton.busy = false; updateManageButton.confirmed = succes; From d422e0d4eeb014350f4c9ba029f064ed3c21bf72 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 13 Dec 2021 14:18:50 +0100 Subject: [PATCH 159/195] Simplify onClicked for install button CURA-8587 --- plugins/Marketplace/resources/qml/PackageCardHeader.qml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/plugins/Marketplace/resources/qml/PackageCardHeader.qml b/plugins/Marketplace/resources/qml/PackageCardHeader.qml index 96335ecc9d..fa45a4d0bb 100644 --- a/plugins/Marketplace/resources/qml/PackageCardHeader.qml +++ b/plugins/Marketplace/resources/qml/PackageCardHeader.qml @@ -232,11 +232,8 @@ Item } } - onClicked: - { - if (packageData.isInstalled){ packageData.uninstall() } - else { packageData.install()} - } + onClicked: packageData.isInstalled ? packageData.uninstall(): packageData.install() + } ManageButton From d50dc59aacb66ebeb2f82333e40f589898997414 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 13 Dec 2021 15:13:00 +0100 Subject: [PATCH 160/195] Simplifications and cleanup CURA-8587 --- plugins/Marketplace/PackageModel.py | 29 ++++++--- plugins/Marketplace/RemotePackageList.py | 2 +- .../resources/qml/ManageButton.qml | 2 +- .../resources/qml/PackageCardHeader.qml | 62 ++++--------------- 4 files changed, 37 insertions(+), 58 deletions(-) diff --git a/plugins/Marketplace/PackageModel.py b/plugins/Marketplace/PackageModel.py index ac2e2df2b2..2d9b013f72 100644 --- a/plugins/Marketplace/PackageModel.py +++ b/plugins/Marketplace/PackageModel.py @@ -6,6 +6,7 @@ from enum import Enum from typing import Any, cast, Dict, List, Optional from PyQt5.QtCore import pyqtProperty, QObject, pyqtSignal, pyqtSlot +from PyQt5.QtQml import QQmlEngine from cura.CuraApplication import CuraApplication from cura.CuraPackageManager import CuraPackageManager @@ -30,6 +31,7 @@ class PackageModel(QObject): :param parent: The parent QML object that controls the lifetime of this model (normally a PackageList). """ super().__init__(parent) + QQmlEngine.setObjectOwnership(self, QQmlEngine.CppOwnership) self._package_manager: CuraPackageManager = cast(CuraPackageManager, CuraApplication.getInstance().getPackageManager()) self._plugin_registry: PluginRegistry = CuraApplication.getInstance().getPluginRegistry() @@ -78,11 +80,10 @@ class PackageModel(QObject): self.updatePackageTriggered.connect(lambda pkg: self._setIsUpdating(True)) - self._plugin_registry.hasPluginsEnabledOrDisabledChanged.connect(self.stateManageButtonChanged) - self._package_manager.packageInstalled.connect(lambda pkg_id: self._packageInstalled(pkg_id, True)) - self._package_manager.packageUninstalled.connect(lambda pkg_id: self._packageInstalled(pkg_id, True)) - self._package_manager.packageInstallingFailed.connect(lambda pkg_id: self._packageInstalled(pkg_id, False)) + self._package_manager.packageInstalled.connect(lambda pkg_id: self._packageInstalled(pkg_id)) + self._package_manager.packageUninstalled.connect(lambda pkg_id: self._packageInstalled(pkg_id)) + self._package_manager.packageInstallingFailed.connect(lambda pkg_id: self._packageInstalled(pkg_id)) self._package_manager.packagesWithUpdateChanged.connect(lambda: self.setCanUpdate(self._package_id in self._package_manager.packagesWithUpdate)) self._is_busy = False @@ -328,16 +329,30 @@ class PackageModel(QObject): """ return self._is_busy + @pyqtSlot() + def enable(self): + self.enablePackageTriggered.emit(self.packageId) + + @pyqtSlot() + def disable(self): + self.disablePackageTriggered.emit(self.packageId) + def setBusy(self, value: bool): if self._is_busy != value: self._is_busy = value - self.busyChanged.emit() + try: + self.busyChanged.emit() + except RuntimeError: + pass - def _packageInstalled(self, package_id: str, success: bool) -> None: + def _packageInstalled(self, package_id: str) -> None: if self._package_id != package_id: return self.setBusy(not self._is_busy) - self.stateManageButtonChanged.emit() + try: + self.stateManageButtonChanged.emit() + except RuntimeError: + pass def _getRecentlyInstalled(self) -> bool: return (self._package_id in self._package_manager.getPackagesToInstall() or self._package_id in self._package_manager.getPackagesToRemove()) \ diff --git a/plugins/Marketplace/RemotePackageList.py b/plugins/Marketplace/RemotePackageList.py index 7cbd00ad76..e877cd9eb5 100644 --- a/plugins/Marketplace/RemotePackageList.py +++ b/plugins/Marketplace/RemotePackageList.py @@ -128,7 +128,7 @@ class RemotePackageList(PackageList): # Setting the ownership of this object to not qml can still result in a RuntimeError. Which can occur when quickly toggling # between de-/constructing RemotePackageLists. This try-except is here to prevent a hard crash when the wrapped C++ object # was deleted when it was still parsing the response - return + continue self._request_url = response_data["links"].get("next", "") # Use empty string to signify that there is no next page. self._ongoing_requests["get_packages"] = None diff --git a/plugins/Marketplace/resources/qml/ManageButton.qml b/plugins/Marketplace/resources/qml/ManageButton.qml index 87c72d8710..36022ffd54 100644 --- a/plugins/Marketplace/resources/qml/ManageButton.qml +++ b/plugins/Marketplace/resources/qml/ManageButton.qml @@ -11,7 +11,7 @@ import Cura 1.6 as Cura Item { id: manageButton - property bool button_style + property bool button_style: true property string text property bool busy: false property bool confirmed: false diff --git a/plugins/Marketplace/resources/qml/PackageCardHeader.qml b/plugins/Marketplace/resources/qml/PackageCardHeader.qml index fa45a4d0bb..5a661e32fb 100644 --- a/plugins/Marketplace/resources/qml/PackageCardHeader.qml +++ b/plugins/Marketplace/resources/qml/PackageCardHeader.qml @@ -181,7 +181,7 @@ Item ManageButton { id: enableManageButton - visible: showManageButtons && !(installManageButton.confirmed || updateManageButton.confirmed || packageData.packageType == "material") + visible: showManageButtons && packageData.packageType != "material" enabled: !(installManageButton.busy || updateManageButton.busy) button_style: !packageData.isActive @@ -189,23 +189,13 @@ Item text: button_style ? catalog.i18nc("@button", "Enable") : catalog.i18nc("@button", "Disable") - onClicked: - { - if(packageData.isActive) - { - packageData.disablePackageTriggered(packageData.packageId) - } - else - { - packageData.enablePackageTriggered(packageData.packageId) - } - } + onClicked: packageData.isActive ? packageData.disable(): packageData.enable() } ManageButton { id: installManageButton - visible: (showManageButtons || confirmed) && ((packageData.isBundled && packageData.canDowngrade) || !packageData.isBundled) && !updateManageButton.confirmed + visible: showManageButtons && (packageData.canDowngrade || !packageData.isBundled) enabled: !updateManageButton.busy busy: packageData.busy button_style: !packageData.isInstalled @@ -213,6 +203,11 @@ Item text: { + if (packageData.canDowngrade) + { + if (busy) { return catalog.i18nc("@button", "Downgrading..."); } + else { return catalog.i18nc("@button", "Downgrade"); } + } if (!packageData.isInstalled) { if (busy) { return catalog.i18nc("@button", "Installing..."); } @@ -220,56 +215,25 @@ Item } else { - if (packageData.canDowngrade) - { - if (busy) { return catalog.i18nc("@button", "Downgrading..."); } - else { return catalog.i18nc("@button", "Downgrade"); } - } - else - { - return catalog.i18nc("@button", "Uninstall"); - } + return catalog.i18nc("@button", "Uninstall"); } } onClicked: packageData.isInstalled ? packageData.uninstall(): packageData.install() - } ManageButton { id: updateManageButton - visible: (showManageButtons || confirmed) && (packageData.canUpdate || confirmed) && !installManageButton.confirmed + visible: showManageButtons && packageData.canUpdate enabled: !installManageButton.busy - confirmed: packageData.isRecentlyUpdatedChanged - - button_style: true + busy: packageData.busy Layout.alignment: Qt.AlignTop - text: - { - if (busy) { return catalog.i18nc("@button", "Updating..."); } - else if (confirmed) { return catalog.i18nc("@button", "Updated"); } - else { return catalog.i18nc("@button", "Update"); } - } + text: busy ? catalog.i18nc("@button", "Updating..."): catalog.i18nc("@button", "Update") - onClicked: - { - busy = true - packageData.updatePackageTriggered(packageData.packageId); - } - - Connections - { - target: packageData - - function updated(succes) - { - updateManageButton.busy = false; - updateManageButton.confirmed = succes; - } - } + onClicked: packageData.updatePackageTriggered(packageData.packageId); } } } From 08eba9f21a97192513783fd93024d2862885d178 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 13 Dec 2021 15:17:19 +0100 Subject: [PATCH 161/195] Fix updating package CURA-8587 --- plugins/Marketplace/PackageModel.py | 16 ---------------- .../resources/qml/PackageCardHeader.qml | 2 +- 2 files changed, 1 insertion(+), 17 deletions(-) diff --git a/plugins/Marketplace/PackageModel.py b/plugins/Marketplace/PackageModel.py index 2d9b013f72..8b7b800dec 100644 --- a/plugins/Marketplace/PackageModel.py +++ b/plugins/Marketplace/PackageModel.py @@ -75,11 +75,6 @@ class PackageModel(QObject): self.enablePackageTriggered.connect(self._plugin_registry.enablePlugin) self.disablePackageTriggered.connect(self._plugin_registry.disablePlugin) - self._is_recently_updated = self._getRecentlyUpdated() - self._is_recently_installed = self._getRecentlyInstalled() - - self.updatePackageTriggered.connect(lambda pkg: self._setIsUpdating(True)) - self._plugin_registry.hasPluginsEnabledOrDisabledChanged.connect(self.stateManageButtonChanged) self._package_manager.packageInstalled.connect(lambda pkg_id: self._packageInstalled(pkg_id)) self._package_manager.packageUninstalled.connect(lambda pkg_id: self._packageInstalled(pkg_id)) @@ -354,17 +349,6 @@ class PackageModel(QObject): except RuntimeError: pass - def _getRecentlyInstalled(self) -> bool: - return (self._package_id in self._package_manager.getPackagesToInstall() or self._package_id in self._package_manager.getPackagesToRemove()) \ - and self._package_id not in self._package_manager.package_infosWithUpdate - - def _getRecentlyUpdated(self) -> bool: - return self._package_id in self._package_manager.package_infosWithUpdate and self._package_id in self._package_manager.getPackagesToInstall() - - @pyqtProperty(bool, constant = True) - def isRecentlyUpdatedChanged(self) -> bool: - return self._is_recently_updated - @pyqtProperty(bool, notify = stateManageButtonChanged) def isInstalled(self) -> bool: return self._package_id in self._package_manager.local_packages_ids diff --git a/plugins/Marketplace/resources/qml/PackageCardHeader.qml b/plugins/Marketplace/resources/qml/PackageCardHeader.qml index 5a661e32fb..9124998ccc 100644 --- a/plugins/Marketplace/resources/qml/PackageCardHeader.qml +++ b/plugins/Marketplace/resources/qml/PackageCardHeader.qml @@ -233,7 +233,7 @@ Item text: busy ? catalog.i18nc("@button", "Updating..."): catalog.i18nc("@button", "Update") - onClicked: packageData.updatePackageTriggered(packageData.packageId); + onClicked: packageData.install() } } } From c338d8f5ce6f50b48fa35dc8028aff44d3317143 Mon Sep 17 00:00:00 2001 From: casper Date: Tue, 14 Dec 2021 16:37:16 +0100 Subject: [PATCH 162/195] Only show enable/disable button if plugin is installed CURA-8587 --- plugins/Marketplace/resources/qml/PackageCardHeader.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/Marketplace/resources/qml/PackageCardHeader.qml b/plugins/Marketplace/resources/qml/PackageCardHeader.qml index 9124998ccc..db1661870b 100644 --- a/plugins/Marketplace/resources/qml/PackageCardHeader.qml +++ b/plugins/Marketplace/resources/qml/PackageCardHeader.qml @@ -181,7 +181,7 @@ Item ManageButton { id: enableManageButton - visible: showManageButtons && packageData.packageType != "material" + visible: showManageButtons && packageData.isInstalled && packageData.packageType != "material" enabled: !(installManageButton.busy || updateManageButton.busy) button_style: !packageData.isActive From 5f444fa5b72738f09c8b244f3a43f571b6c0c660 Mon Sep 17 00:00:00 2001 From: casper Date: Tue, 14 Dec 2021 17:49:25 +0100 Subject: [PATCH 163/195] Simplify enabled busy state both the `installManageButton` and `updateManageButton` are busy when `packageData.busy`, so the `!(installManageButton.busy || updateManageButton.busy)` check didn't make much sense. CURA-8587 --- plugins/Marketplace/resources/qml/PackageCardHeader.qml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/plugins/Marketplace/resources/qml/PackageCardHeader.qml b/plugins/Marketplace/resources/qml/PackageCardHeader.qml index db1661870b..ed55d7b362 100644 --- a/plugins/Marketplace/resources/qml/PackageCardHeader.qml +++ b/plugins/Marketplace/resources/qml/PackageCardHeader.qml @@ -182,7 +182,7 @@ Item { id: enableManageButton visible: showManageButtons && packageData.isInstalled && packageData.packageType != "material" - enabled: !(installManageButton.busy || updateManageButton.busy) + enabled: !packageData.busy button_style: !packageData.isActive Layout.alignment: Qt.AlignTop @@ -196,7 +196,7 @@ Item { id: installManageButton visible: showManageButtons && (packageData.canDowngrade || !packageData.isBundled) - enabled: !updateManageButton.busy + enabled: !packageData.busy busy: packageData.busy button_style: !packageData.isInstalled Layout.alignment: Qt.AlignTop @@ -226,8 +226,7 @@ Item { id: updateManageButton visible: showManageButtons && packageData.canUpdate - enabled: !installManageButton.busy - + enabled: !packageData.busy busy: packageData.busy Layout.alignment: Qt.AlignTop From aea316799a246a3c083a9b8af5c29f8064ce7803 Mon Sep 17 00:00:00 2001 From: casper Date: Wed, 15 Dec 2021 10:43:57 +0100 Subject: [PATCH 164/195] Always set `busy = False` on installed package CURA-8587 --- plugins/Marketplace/PackageModel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/Marketplace/PackageModel.py b/plugins/Marketplace/PackageModel.py index 8b7b800dec..02cdcba17e 100644 --- a/plugins/Marketplace/PackageModel.py +++ b/plugins/Marketplace/PackageModel.py @@ -343,7 +343,7 @@ class PackageModel(QObject): def _packageInstalled(self, package_id: str) -> None: if self._package_id != package_id: return - self.setBusy(not self._is_busy) + self.setBusy(False) try: self.stateManageButtonChanged.emit() except RuntimeError: From 62596a42e69bdf9691a9fec4dcc57e2f82f9a843 Mon Sep 17 00:00:00 2001 From: casper Date: Wed, 15 Dec 2021 10:44:25 +0100 Subject: [PATCH 165/195] Remove un-needed column component CURA-8587 --- .../Marketplace/resources/qml/PackageCard.qml | 118 ++++++++---------- 1 file changed, 55 insertions(+), 63 deletions(-) diff --git a/plugins/Marketplace/resources/qml/PackageCard.qml b/plugins/Marketplace/resources/qml/PackageCard.qml index 9097417c80..633d2b25b9 100644 --- a/plugins/Marketplace/resources/qml/PackageCard.qml +++ b/plugins/Marketplace/resources/qml/PackageCard.qml @@ -17,86 +17,78 @@ Rectangle color: UM.Theme.getColor("main_background") radius: UM.Theme.getSize("default_radius").width - Column + PackageCardHeader { - width: parent.width + id: packageCardHeader - spacing: 0 - - PackageCardHeader + Item { - id: packageCardHeader + id: shortDescription - // description - Item + anchors.fill: parent + + Label { - id: shortDescription + id: descriptionLabel + width: parent.width + property real lastLineWidth: 0; //Store the width of the last line, to properly position the elision. - anchors.fill: parent + text: packageData.description + textFormat: Text.PlainText //Must be plain text, or we won't get onLineLaidOut signals. Don't auto-detect! + font: UM.Theme.getFont("default") + color: UM.Theme.getColor("text") + maximumLineCount: 2 + wrapMode: Text.Wrap + elide: Text.ElideRight + visible: text !== "" - Label + onLineLaidOut: { - id: descriptionLabel - width: parent.width - property real lastLineWidth: 0; //Store the width of the last line, to properly position the elision. - - text: packageData.description - textFormat: Text.PlainText //Must be plain text, or we won't get onLineLaidOut signals. Don't auto-detect! - font: UM.Theme.getFont("default") - color: UM.Theme.getColor("text") - maximumLineCount: 2 - wrapMode: Text.Wrap - elide: Text.ElideRight - visible: text !== "" - - onLineLaidOut: + if(truncated && line.isLast) { - if(truncated && line.isLast) + let max_line_width = parent.width - readMoreButton.width - fontMetrics.advanceWidth("… ") - 2 * UM.Theme.getSize("default_margin").width; + if(line.implicitWidth > max_line_width) { - let max_line_width = parent.width - readMoreButton.width - fontMetrics.advanceWidth("… ") - 2 * UM.Theme.getSize("default_margin").width; - if(line.implicitWidth > max_line_width) - { - line.width = max_line_width; - } - else - { - line.width = line.implicitWidth - fontMetrics.advanceWidth("…"); //Truncate the ellipsis. We're adding this ourselves. - } - descriptionLabel.lastLineWidth = line.implicitWidth; + line.width = max_line_width; } + else + { + line.width = line.implicitWidth - fontMetrics.advanceWidth("…"); //Truncate the ellipsis. We're adding this ourselves. + } + descriptionLabel.lastLineWidth = line.implicitWidth; } } - Label - { - id: tripleDotLabel - anchors.left: parent.left - anchors.leftMargin: descriptionLabel.lastLineWidth - anchors.bottom: descriptionLabel.bottom + } + Label + { + id: tripleDotLabel + anchors.left: parent.left + anchors.leftMargin: descriptionLabel.lastLineWidth + anchors.bottom: descriptionLabel.bottom - text: "… " - font: descriptionLabel.font - color: descriptionLabel.color - visible: descriptionLabel.truncated && descriptionLabel.text !== "" - } - Cura.TertiaryButton - { - id: readMoreButton - anchors.right: parent.right - anchors.bottom: descriptionLabel.bottom - height: fontMetrics.height //Height of a single line. + text: "… " + font: descriptionLabel.font + color: descriptionLabel.color + visible: descriptionLabel.truncated && descriptionLabel.text !== "" + } + Cura.TertiaryButton + { + id: readMoreButton + anchors.right: parent.right + anchors.bottom: descriptionLabel.bottom + height: fontMetrics.height //Height of a single line. - text: catalog.i18nc("@info", "Read more") - iconSource: UM.Theme.getIcon("LinkExternal") + text: catalog.i18nc("@info", "Read more") + iconSource: UM.Theme.getIcon("LinkExternal") - visible: descriptionLabel.truncated && descriptionLabel.text !== "" - enabled: visible - leftPadding: UM.Theme.getSize("default_margin").width - rightPadding: UM.Theme.getSize("wide_margin").width - textFont: descriptionLabel.font - isIconOnRightSide: true + visible: descriptionLabel.truncated && descriptionLabel.text !== "" + enabled: visible + leftPadding: UM.Theme.getSize("default_margin").width + rightPadding: UM.Theme.getSize("wide_margin").width + textFont: descriptionLabel.font + isIconOnRightSide: true - onClicked: Qt.openUrlExternally(packageData.packageInfoUrl) - } + onClicked: Qt.openUrlExternally(packageData.packageInfoUrl) } } } From 33f1bd8c5d2126e0f3045d9c71514dd23788f9c9 Mon Sep 17 00:00:00 2001 From: casper Date: Wed, 15 Dec 2021 14:18:40 +0100 Subject: [PATCH 166/195] Simplify `CuraPackageManager` CURA-8587 --- cura/CuraPackageManager.py | 26 +++++++++----------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/cura/CuraPackageManager.py b/cura/CuraPackageManager.py index 535d331a62..313653c8a0 100644 --- a/cura/CuraPackageManager.py +++ b/cura/CuraPackageManager.py @@ -19,13 +19,11 @@ if TYPE_CHECKING: class CuraPackageManager(PackageManager): def __init__(self, application: "QtApplication", parent: Optional["QObject"] = None) -> None: super().__init__(application, parent) - self._local_packages: Optional[List[Dict[str, Any]]] = None - self._local_packages_ids: Optional[Set[str]] = None + self._local_packages: Optional[Dict[str, Dict[str, Any]]] = None self.installedPackagesChanged.connect(self._updateLocalPackages) def _updateLocalPackages(self) -> None: self._local_packages = self.getAllLocalPackages() - self._local_packages_ids = set(pkg["package_id"] for pkg in self._local_packages) @property def local_packages(self) -> List[Dict[str, Any]]: @@ -34,16 +32,16 @@ class CuraPackageManager(PackageManager): self._updateLocalPackages() # _updateLocalPackages always results in a list of packages, not None. # It's guaranteed to be a list now. - return cast(List[Dict[str, Any]], self._local_packages) + return list(self._local_packages.values()) @property def local_packages_ids(self) -> Set[str]: """locally installed packages, lazy execution""" - if self._local_packages_ids is None: + if self._local_packages is None: self._updateLocalPackages() # _updateLocalPackages always results in a list of packages, not None. # It's guaranteed to be a list now. - return cast(Set[str], self._local_packages_ids) + return set(self._local_packages.keys()) def initialize(self) -> None: self._installation_dirs_dict["materials"] = Resources.getStoragePath(CuraApplication.ResourceTypes.MaterialInstanceContainer) @@ -75,17 +73,11 @@ class CuraPackageManager(PackageManager): return machine_with_materials, machine_with_qualities - def getAllLocalPackages(self) -> List[Dict[str, Any]]: + def getAllLocalPackages(self) -> Dict[str, Dict[str, Any]]: """ returns an unordered list of all the package_info installed, to be installed or to be returned""" + packages = dict([(package_info["package_id"], dict(package_info)) for package in self.getAllInstalledPackagesInfo().values() for package_info in package]) + packages.update([(package["package_info"]["package_id"], dict(package["package_info"])) for package in self.getPackagesToRemove().values()]) + packages.update([(package["package_info"]["package_id"], dict(package["package_info"])) for package in self.getPackagesToInstall().values()]) - class PkgInfo(dict): - # Needed helper class because a dict isn't hashable - def __eq__(self, item): - return item == self["package_id"] - - packages = [PkgInfo(package_info) for package in self.getAllInstalledPackagesInfo().values() for package_info in package] - packages.extend([PkgInfo(package["package_info"]) for package in self.getPackagesToRemove().values() if package["package_info"]["package_id"] not in packages]) - packages.extend([PkgInfo(package["package_info"]) for package in self.getPackagesToInstall().values() if package["package_info"]["package_id"] not in packages]) - - return [dict(package) for package in packages] + return packages From 8b2ced122ca746c3b5c9effb4db73e43b8f9c9c5 Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Wed, 15 Dec 2021 15:07:23 +0100 Subject: [PATCH 167/195] Explicitly close the downloaded curapackage Sometime very small curapackage where download and the `canInstallChanged` signal was emitted before the zipfile was completely processed. This could result in a failed installation, which was often the case for materials. I also narrowed down the try-catch block. --- plugins/Marketplace/PackageList.py | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/plugins/Marketplace/PackageList.py b/plugins/Marketplace/PackageList.py index 390bf841df..57d8c22183 100644 --- a/plugins/Marketplace/PackageList.py +++ b/plugins/Marketplace/PackageList.py @@ -209,24 +209,26 @@ class PackageList(ListModel): ) def _downloadFinished(self, package_id: str, reply: "QNetworkReply", update: bool = False) -> None: - try: - with tempfile.NamedTemporaryFile(mode = "wb+", suffix = ".curapackage", delete = False) as temp_file: + with tempfile.NamedTemporaryFile(mode = "wb+", suffix = ".curapackage", delete = False) as temp_file: + try: bytes_read = reply.read(self.DISK_WRITE_BUFFER_SIZE) while bytes_read: temp_file.write(bytes_read) bytes_read = reply.read(self.DISK_WRITE_BUFFER_SIZE) - self._to_install[package_id] = temp_file.name - self._ongoing_requests["download_package"] = None - self.canInstallChanged.emit(package_id, update) - except IOError as e: - Logger.error(f"Failed to write downloaded package to temp file {e}") - temp_file.close() - self._downloadError(package_id, update) - except RuntimeError: - # Setting the ownership of this object to not qml can still result in a RuntimeError. Which can occur when quickly toggling - # between de-/constructing Remote or Local PackageLists. This try-except is here to prevent a hard crash when the wrapped C++ object - # was deleted when it was still parsing the response - return + except IOError as e: + Logger.error(f"Failed to write downloaded package to temp file {e}") + temp_file.close() + self._downloadError(package_id, update) + except RuntimeError: + # Setting the ownership of this object to not qml can still result in a RuntimeError. Which can occur when quickly toggling + # between de-/constructing Remote or Local PackageLists. This try-except is here to prevent a hard crash when the wrapped C++ object + # was deleted when it was still parsing the response + temp_file.close() + return + temp_file.close() + self._to_install[package_id] = temp_file.name + self._ongoing_requests["download_package"] = None + self.canInstallChanged.emit(package_id, update) def _downloadError(self, package_id: str, update: bool = False, reply: Optional["QNetworkReply"] = None, error: Optional["QNetworkReply.NetworkError"] = None) -> None: if reply: From c66e17dd9e52987ff75b708557e9037f4d9d66f2 Mon Sep 17 00:00:00 2001 From: casper Date: Wed, 15 Dec 2021 15:13:38 +0100 Subject: [PATCH 168/195] Show install button after package has been uninstalled CURA-8587 --- cura/CuraPackageManager.py | 32 ++++++++++++++++++++--------- plugins/Marketplace/PackageModel.py | 2 +- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/cura/CuraPackageManager.py b/cura/CuraPackageManager.py index 313653c8a0..2a016bdf6e 100644 --- a/cura/CuraPackageManager.py +++ b/cura/CuraPackageManager.py @@ -19,11 +19,23 @@ if TYPE_CHECKING: class CuraPackageManager(PackageManager): def __init__(self, application: "QtApplication", parent: Optional["QObject"] = None) -> None: super().__init__(application, parent) + self._local_packages: Optional[Dict[str, Dict[str, Any]]] = None + self._local_packages_installed: Optional[Dict[str, Dict[str, Any]]] = None + self._local_packages_to_remove: Optional[Dict[str, Dict[str, Any]]] = None + self._local_packages_to_install: Optional[Dict[str, Dict[str, Any]]] = None + self.installedPackagesChanged.connect(self._updateLocalPackages) def _updateLocalPackages(self) -> None: - self._local_packages = self.getAllLocalPackages() + self._local_packages_installed = dict([(package_info["package_id"], dict(package_info)) for package in self.getAllInstalledPackagesInfo().values() for package_info in package]) + self._local_packages_to_remove = dict([(package["package_info"]["package_id"], dict(package["package_info"])) for package in self.getPackagesToRemove().values()]) + self._local_packages_to_install = dict([(package["package_info"]["package_id"], dict(package["package_info"])) for package in self.getPackagesToInstall().values()]) + + self._local_packages = {} + self._local_packages.update(self._local_packages_installed) + self._local_packages.update(self._local_packages_to_remove) + self._local_packages.update(self._local_packages_to_install) @property def local_packages(self) -> List[Dict[str, Any]]: @@ -43,6 +55,15 @@ class CuraPackageManager(PackageManager): # It's guaranteed to be a list now. return set(self._local_packages.keys()) + @property + def installed_packages_ids(self) -> Set[str]: + """locally installed packages, lazy execution""" + if self._local_packages is None: + self._updateLocalPackages() + # _updateLocalPackages always results in a list of packages, not None. + # It's guaranteed to be a list now. + return set(self._local_packages_installed.keys()) + def initialize(self) -> None: self._installation_dirs_dict["materials"] = Resources.getStoragePath(CuraApplication.ResourceTypes.MaterialInstanceContainer) self._installation_dirs_dict["qualities"] = Resources.getStoragePath(CuraApplication.ResourceTypes.QualityInstanceContainer) @@ -72,12 +93,3 @@ class CuraPackageManager(PackageManager): machine_with_qualities.append((global_stack, str(extruder_nr), container_id)) return machine_with_materials, machine_with_qualities - - def getAllLocalPackages(self) -> Dict[str, Dict[str, Any]]: - """ returns an unordered list of all the package_info installed, to be installed or to be returned""" - - packages = dict([(package_info["package_id"], dict(package_info)) for package in self.getAllInstalledPackagesInfo().values() for package_info in package]) - packages.update([(package["package_info"]["package_id"], dict(package["package_info"])) for package in self.getPackagesToRemove().values()]) - packages.update([(package["package_info"]["package_id"], dict(package["package_info"])) for package in self.getPackagesToInstall().values()]) - - return packages diff --git a/plugins/Marketplace/PackageModel.py b/plugins/Marketplace/PackageModel.py index 02cdcba17e..f88b1416b5 100644 --- a/plugins/Marketplace/PackageModel.py +++ b/plugins/Marketplace/PackageModel.py @@ -351,7 +351,7 @@ class PackageModel(QObject): @pyqtProperty(bool, notify = stateManageButtonChanged) def isInstalled(self) -> bool: - return self._package_id in self._package_manager.local_packages_ids + return self._package_id in self._package_manager.installed_packages_ids @pyqtProperty(bool, notify = stateManageButtonChanged) def isActive(self) -> bool: From 9e1e98bdbd053d70d50f796c4204498f59dd0c1c Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Wed, 15 Dec 2021 16:38:54 +0100 Subject: [PATCH 169/195] Revert "Show install button after package has been uninstalled" This reverts commit c66e17dd9e52987ff75b708557e9037f4d9d66f2. --- cura/CuraPackageManager.py | 32 +++++++++-------------------- plugins/Marketplace/PackageModel.py | 2 +- 2 files changed, 11 insertions(+), 23 deletions(-) diff --git a/cura/CuraPackageManager.py b/cura/CuraPackageManager.py index 2a016bdf6e..313653c8a0 100644 --- a/cura/CuraPackageManager.py +++ b/cura/CuraPackageManager.py @@ -19,23 +19,11 @@ if TYPE_CHECKING: class CuraPackageManager(PackageManager): def __init__(self, application: "QtApplication", parent: Optional["QObject"] = None) -> None: super().__init__(application, parent) - self._local_packages: Optional[Dict[str, Dict[str, Any]]] = None - self._local_packages_installed: Optional[Dict[str, Dict[str, Any]]] = None - self._local_packages_to_remove: Optional[Dict[str, Dict[str, Any]]] = None - self._local_packages_to_install: Optional[Dict[str, Dict[str, Any]]] = None - self.installedPackagesChanged.connect(self._updateLocalPackages) def _updateLocalPackages(self) -> None: - self._local_packages_installed = dict([(package_info["package_id"], dict(package_info)) for package in self.getAllInstalledPackagesInfo().values() for package_info in package]) - self._local_packages_to_remove = dict([(package["package_info"]["package_id"], dict(package["package_info"])) for package in self.getPackagesToRemove().values()]) - self._local_packages_to_install = dict([(package["package_info"]["package_id"], dict(package["package_info"])) for package in self.getPackagesToInstall().values()]) - - self._local_packages = {} - self._local_packages.update(self._local_packages_installed) - self._local_packages.update(self._local_packages_to_remove) - self._local_packages.update(self._local_packages_to_install) + self._local_packages = self.getAllLocalPackages() @property def local_packages(self) -> List[Dict[str, Any]]: @@ -55,15 +43,6 @@ class CuraPackageManager(PackageManager): # It's guaranteed to be a list now. return set(self._local_packages.keys()) - @property - def installed_packages_ids(self) -> Set[str]: - """locally installed packages, lazy execution""" - if self._local_packages is None: - self._updateLocalPackages() - # _updateLocalPackages always results in a list of packages, not None. - # It's guaranteed to be a list now. - return set(self._local_packages_installed.keys()) - def initialize(self) -> None: self._installation_dirs_dict["materials"] = Resources.getStoragePath(CuraApplication.ResourceTypes.MaterialInstanceContainer) self._installation_dirs_dict["qualities"] = Resources.getStoragePath(CuraApplication.ResourceTypes.QualityInstanceContainer) @@ -93,3 +72,12 @@ class CuraPackageManager(PackageManager): machine_with_qualities.append((global_stack, str(extruder_nr), container_id)) return machine_with_materials, machine_with_qualities + + def getAllLocalPackages(self) -> Dict[str, Dict[str, Any]]: + """ returns an unordered list of all the package_info installed, to be installed or to be returned""" + + packages = dict([(package_info["package_id"], dict(package_info)) for package in self.getAllInstalledPackagesInfo().values() for package_info in package]) + packages.update([(package["package_info"]["package_id"], dict(package["package_info"])) for package in self.getPackagesToRemove().values()]) + packages.update([(package["package_info"]["package_id"], dict(package["package_info"])) for package in self.getPackagesToInstall().values()]) + + return packages diff --git a/plugins/Marketplace/PackageModel.py b/plugins/Marketplace/PackageModel.py index f88b1416b5..02cdcba17e 100644 --- a/plugins/Marketplace/PackageModel.py +++ b/plugins/Marketplace/PackageModel.py @@ -351,7 +351,7 @@ class PackageModel(QObject): @pyqtProperty(bool, notify = stateManageButtonChanged) def isInstalled(self) -> bool: - return self._package_id in self._package_manager.installed_packages_ids + return self._package_id in self._package_manager.local_packages_ids @pyqtProperty(bool, notify = stateManageButtonChanged) def isActive(self) -> bool: From 0ffaafc8c0159769f797f38f68241d56ce803288 Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Wed, 15 Dec 2021 16:39:02 +0100 Subject: [PATCH 170/195] Revert "Simplify `CuraPackageManager`" This reverts commit 33f1bd8c5d2126e0f3045d9c71514dd23788f9c9. --- cura/CuraPackageManager.py | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/cura/CuraPackageManager.py b/cura/CuraPackageManager.py index 313653c8a0..535d331a62 100644 --- a/cura/CuraPackageManager.py +++ b/cura/CuraPackageManager.py @@ -19,11 +19,13 @@ if TYPE_CHECKING: class CuraPackageManager(PackageManager): def __init__(self, application: "QtApplication", parent: Optional["QObject"] = None) -> None: super().__init__(application, parent) - self._local_packages: Optional[Dict[str, Dict[str, Any]]] = None + self._local_packages: Optional[List[Dict[str, Any]]] = None + self._local_packages_ids: Optional[Set[str]] = None self.installedPackagesChanged.connect(self._updateLocalPackages) def _updateLocalPackages(self) -> None: self._local_packages = self.getAllLocalPackages() + self._local_packages_ids = set(pkg["package_id"] for pkg in self._local_packages) @property def local_packages(self) -> List[Dict[str, Any]]: @@ -32,16 +34,16 @@ class CuraPackageManager(PackageManager): self._updateLocalPackages() # _updateLocalPackages always results in a list of packages, not None. # It's guaranteed to be a list now. - return list(self._local_packages.values()) + return cast(List[Dict[str, Any]], self._local_packages) @property def local_packages_ids(self) -> Set[str]: """locally installed packages, lazy execution""" - if self._local_packages is None: + if self._local_packages_ids is None: self._updateLocalPackages() # _updateLocalPackages always results in a list of packages, not None. # It's guaranteed to be a list now. - return set(self._local_packages.keys()) + return cast(Set[str], self._local_packages_ids) def initialize(self) -> None: self._installation_dirs_dict["materials"] = Resources.getStoragePath(CuraApplication.ResourceTypes.MaterialInstanceContainer) @@ -73,11 +75,17 @@ class CuraPackageManager(PackageManager): return machine_with_materials, machine_with_qualities - def getAllLocalPackages(self) -> Dict[str, Dict[str, Any]]: + def getAllLocalPackages(self) -> List[Dict[str, Any]]: """ returns an unordered list of all the package_info installed, to be installed or to be returned""" - packages = dict([(package_info["package_id"], dict(package_info)) for package in self.getAllInstalledPackagesInfo().values() for package_info in package]) - packages.update([(package["package_info"]["package_id"], dict(package["package_info"])) for package in self.getPackagesToRemove().values()]) - packages.update([(package["package_info"]["package_id"], dict(package["package_info"])) for package in self.getPackagesToInstall().values()]) - return packages + class PkgInfo(dict): + # Needed helper class because a dict isn't hashable + def __eq__(self, item): + return item == self["package_id"] + + packages = [PkgInfo(package_info) for package in self.getAllInstalledPackagesInfo().values() for package_info in package] + packages.extend([PkgInfo(package["package_info"]) for package in self.getPackagesToRemove().values() if package["package_info"]["package_id"] not in packages]) + packages.extend([PkgInfo(package["package_info"]) for package in self.getPackagesToInstall().values() if package["package_info"]["package_id"] not in packages]) + + return [dict(package) for package in packages] From 23cc7084c48ec346ef32e2a5b35f264e44648d28 Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Wed, 15 Dec 2021 17:51:38 +0100 Subject: [PATCH 171/195] Switch between correct states of the un-/installed buttons Contributes to CURA-8587 --- plugins/Marketplace/PackageList.py | 5 ++++- plugins/Marketplace/PackageModel.py | 7 +++++-- plugins/Marketplace/resources/qml/PackageCardHeader.qml | 6 +++--- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/plugins/Marketplace/PackageList.py b/plugins/Marketplace/PackageList.py index 57d8c22183..2c4137394b 100644 --- a/plugins/Marketplace/PackageList.py +++ b/plugins/Marketplace/PackageList.py @@ -19,7 +19,7 @@ from cura.CuraPackageManager import CuraPackageManager from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope # To make requests to the Ultimaker API with correct authorization. from .PackageModel import PackageModel -from .Constants import USER_PACKAGES_URL +from .Constants import USER_PACKAGES_URL, PACKAGES_URL if TYPE_CHECKING: from PyQt5.QtCore import QObject @@ -195,6 +195,9 @@ class PackageList(ListModel): :param update: A flag if this is download request is an update process """ + if url == "": + url = f"{PACKAGES_URL}/{package_id}/download" + def downloadFinished(reply: "QNetworkReply") -> None: self._downloadFinished(package_id, reply, update) diff --git a/plugins/Marketplace/PackageModel.py b/plugins/Marketplace/PackageModel.py index 02cdcba17e..213d5bf616 100644 --- a/plugins/Marketplace/PackageModel.py +++ b/plugins/Marketplace/PackageModel.py @@ -314,7 +314,6 @@ class PackageModel(QObject): @pyqtSlot() def uninstall(self): - self.setBusy(True) self.uninstallPackageTriggered.emit(self.packageId) @pyqtProperty(bool, notify= busyChanged) @@ -351,7 +350,11 @@ class PackageModel(QObject): @pyqtProperty(bool, notify = stateManageButtonChanged) def isInstalled(self) -> bool: - return self._package_id in self._package_manager.local_packages_ids + return self._package_id in self._package_manager.getAllInstalledPackageIDs() + + @pyqtProperty(bool, notify = stateManageButtonChanged) + def isToBeInstalled(self) -> bool: + return self._package_id in self._package_manager.getPackagesToInstall() @pyqtProperty(bool, notify = stateManageButtonChanged) def isActive(self) -> bool: diff --git a/plugins/Marketplace/resources/qml/PackageCardHeader.qml b/plugins/Marketplace/resources/qml/PackageCardHeader.qml index ed55d7b362..41c0bd6a95 100644 --- a/plugins/Marketplace/resources/qml/PackageCardHeader.qml +++ b/plugins/Marketplace/resources/qml/PackageCardHeader.qml @@ -198,7 +198,7 @@ Item visible: showManageButtons && (packageData.canDowngrade || !packageData.isBundled) enabled: !packageData.busy busy: packageData.busy - button_style: !packageData.isInstalled + button_style: packageData.isInstalled || packageData.isToBeInstalled Layout.alignment: Qt.AlignTop text: @@ -208,7 +208,7 @@ Item if (busy) { return catalog.i18nc("@button", "Downgrading..."); } else { return catalog.i18nc("@button", "Downgrade"); } } - if (!packageData.isInstalled) + if (!(packageData.isInstalled || packageData.isToBeInstalled)) { if (busy) { return catalog.i18nc("@button", "Installing..."); } else { return catalog.i18nc("@button", "Install"); } @@ -219,7 +219,7 @@ Item } } - onClicked: packageData.isInstalled ? packageData.uninstall(): packageData.install() + onClicked: packageData.isInstalled || packageData.isToBeInstalled ? packageData.uninstall(): packageData.install() } ManageButton From 951c0234d6c02e7140e31bbb6dd1c8fda9f03062 Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Thu, 16 Dec 2021 10:06:54 +0100 Subject: [PATCH 172/195] Renamed _manager to _package_manager for more consistent naming Contributes to CURA-8587 --- plugins/Marketplace/LocalPackageList.py | 10 +++++----- plugins/Marketplace/PackageList.py | 19 ++++++++++--------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/plugins/Marketplace/LocalPackageList.py b/plugins/Marketplace/LocalPackageList.py index 49a08296f5..616f6d95bf 100644 --- a/plugins/Marketplace/LocalPackageList.py +++ b/plugins/Marketplace/LocalPackageList.py @@ -40,7 +40,7 @@ class LocalPackageList(PackageList): super().__init__(parent) self._has_footer = False self._ongoing_requests["check_updates"] = None - self._manager.packagesWithUpdateChanged.connect(self._sortSectionsOnUpdate) + self._package_manager.packagesWithUpdateChanged.connect(self._sortSectionsOnUpdate) def _sortSectionsOnUpdate(self) -> None: SECTION_ORDER = dict(zip([i for k, v in self.PACKAGE_CATEGORIES.items() for i in self.PACKAGE_CATEGORIES[k].values()], ["a", "b", "c", "d"])) @@ -56,9 +56,9 @@ class LocalPackageList(PackageList): self.setIsLoading(True) # Obtain and sort the local packages - self.setItems([{"package": p} for p in [self._makePackageModel(p) for p in self._manager.local_packages]]) + self.setItems([{"package": p} for p in [self._makePackageModel(p) for p in self._package_manager.local_packages]]) self._sortSectionsOnUpdate() - self.checkForUpdates(self._manager.local_packages) + self.checkForUpdates(self._package_manager.local_packages) self.setIsLoading(False) self.setHasMore(False) # All packages should have been loaded at this time @@ -67,7 +67,7 @@ class LocalPackageList(PackageList): """ Create a PackageModel from the package_info and determine its section_title""" package_id = package_info["package_id"] - bundled_or_installed = "bundled" if self._manager.isBundledPackage(package_id) else "installed" + bundled_or_installed = "bundled" if self._package_manager.isBundledPackage(package_id) else "installed" package_type = package_info["package_type"] section_title = self.PACKAGE_CATEGORIES[bundled_or_installed][package_type] package = PackageModel(package_info, section_title = section_title, parent = self) @@ -99,5 +99,5 @@ class LocalPackageList(PackageList): return packages = response_data["data"] - self._manager.setPackagesWithUpdate(dict(zip([p['package_id'] for p in packages], [p for p in packages]))) + self._package_manager.setPackagesWithUpdate(dict(zip([p['package_id'] for p in packages], [p for p in packages]))) self._ongoing_requests["check_updates"] = None diff --git a/plugins/Marketplace/PackageList.py b/plugins/Marketplace/PackageList.py index 2c4137394b..af23a2a6fe 100644 --- a/plugins/Marketplace/PackageList.py +++ b/plugins/Marketplace/PackageList.py @@ -37,7 +37,7 @@ class PackageList(ListModel): def __init__(self, parent: Optional["QObject"] = None) -> None: super().__init__(parent) - self._manager: CuraPackageManager = cast(CuraPackageManager, CuraApplication.getInstance().getPackageManager()) + self._package_manager: CuraPackageManager = cast(CuraPackageManager, CuraApplication.getInstance().getPackageManager()) self._plugin_registry: PluginRegistry = CuraApplication.getInstance().getPluginRegistry() self._account = CuraApplication.getInstance().getCuraAPI().account self._error_message = "" @@ -47,7 +47,7 @@ class PackageList(ListModel): self._has_footer = True self._to_install: Dict[str, str] = {} self.canInstallChanged.connect(self._requestInstall) - self._local_packages: Set[str] = {p["package_id"] for p in self._manager.local_packages} + self._local_packages: Set[str] = {p["package_id"] for p in self._package_manager.local_packages} self._ongoing_requests: Dict[str, Optional[HttpRequestData]] = {"download_package": None} self._scope = JsonDecoratorScope(UltimakerCloudScope(CuraApplication.getInstance())) @@ -165,11 +165,11 @@ class PackageList(ListModel): if dialog is not None: dialog.deleteLater() # reset package card - self._manager.packageInstallingFailed.emit(package_id) + self._package_manager.packageInstallingFailed.emit(package_id) def _requestInstall(self, package_id: str, update: bool = False) -> None: package_path = self._to_install[package_id] - license_content = self._manager.getPackageLicense(package_path) + license_content = self._package_manager.getPackageLicense(package_path) if not update and license_content is not None: # If installation is not and update, and the packages contains a license then @@ -181,8 +181,9 @@ class PackageList(ListModel): def _install(self, package_id: str, update: bool = False) -> None: package_path = self._to_install.pop(package_id) - to_be_installed = self._manager.installPackage(package_path) is not None + to_be_installed = self._package_manager.installPackage(package_path) is not None if not to_be_installed: + Logger.warning(f"Could not install {package_id}") return package = self.getPackageModel(package_id) self.subscribeUserToPackage(package_id, str(package.sdk_version)) @@ -237,7 +238,7 @@ class PackageList(ListModel): if reply: reply_string = bytes(reply.readAll()).decode() Logger.error(f"Failed to download package: {package_id} due to {reply_string}") - self._manager.packageInstallingFailed.emit(package_id) + self._package_manager.packageInstallingFailed.emit(package_id) def subscribeUserToPackage(self, package_id: str, sdk_version: str) -> None: """Subscribe the user (if logged in) to the package for a given SDK @@ -279,7 +280,7 @@ class PackageList(ListModel): :param package_id: the package identification string """ - self._manager.removePackage(package_id) + self._package_manager.removePackage(package_id) self.unsunscribeUserFromPackage(package_id) def updatePackage(self, package_id: str) -> None: @@ -287,6 +288,6 @@ class PackageList(ListModel): :param package_id: the package identification string """ - self._manager.removePackage(package_id, force_add = not self._manager.isBundledPackage(package_id)) - url = self._manager.package_infosWithUpdate[package_id]["download_url"] + self._package_manager.removePackage(package_id, force_add = not self._package_manager.isBundledPackage(package_id)) + url = self._package_manager.package_infosWithUpdate[package_id]["download_url"] self.download(package_id, url, True) From 447e0443a289a7d8a6dd99393ca06a102fd83591 Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Thu, 16 Dec 2021 10:08:24 +0100 Subject: [PATCH 173/195] Reinstall a package scheduled for removal before attempting to dl and install Contributes to CURA-8587 --- plugins/Marketplace/PackageList.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/Marketplace/PackageList.py b/plugins/Marketplace/PackageList.py index af23a2a6fe..741532499d 100644 --- a/plugins/Marketplace/PackageList.py +++ b/plugins/Marketplace/PackageList.py @@ -273,7 +273,8 @@ class PackageList(ListModel): :param package_id: the package identification string """ - self.download(package_id, url, False) + if not self._package_manager.reinstallPackage(package_id): + self.download(package_id, url, False) def uninstallPackage(self, package_id: str) -> None: """Uninstall a package from the Marketplace From 020313da25770950eb061689b1d83e5e8ad24a71 Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Thu, 16 Dec 2021 10:23:23 +0100 Subject: [PATCH 174/195] Subscribe the user to a reinstalled package again Contributes to CURA-8587 --- plugins/Marketplace/PackageList.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plugins/Marketplace/PackageList.py b/plugins/Marketplace/PackageList.py index 741532499d..2fd62156f0 100644 --- a/plugins/Marketplace/PackageList.py +++ b/plugins/Marketplace/PackageList.py @@ -275,6 +275,9 @@ class PackageList(ListModel): """ if not self._package_manager.reinstallPackage(package_id): self.download(package_id, url, False) + else: + package = self.getPackageModel(package_id) + self.subscribeUserToPackage(package_id, str(package.sdk_version)) def uninstallPackage(self, package_id: str) -> None: """Uninstall a package from the Marketplace From aa93186707488bf1df59c94904f0e7019cad6f59 Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Thu, 16 Dec 2021 10:34:08 +0100 Subject: [PATCH 175/195] Don't show License Dialog when there is no License text Contributes to CURA-8587 --- plugins/Marketplace/PackageList.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/Marketplace/PackageList.py b/plugins/Marketplace/PackageList.py index 2fd62156f0..4ba312ea6c 100644 --- a/plugins/Marketplace/PackageList.py +++ b/plugins/Marketplace/PackageList.py @@ -171,7 +171,7 @@ class PackageList(ListModel): package_path = self._to_install[package_id] license_content = self._package_manager.getPackageLicense(package_path) - if not update and license_content is not None: + if not update and license_content is not None and license_content != "": # If installation is not and update, and the packages contains a license then # open dialog, prompting the using to accept the plugin license self._openLicenseDialog(package_id, license_content) From 14406e13bdee59c393249003d081e50a63fbbda1 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 16 Dec 2021 10:42:44 +0100 Subject: [PATCH 176/195] Rename hasPluginsEnabledOrDisabledChanged to pluginsEnabledOrDisabledChanged This makes it more in line with the other signal naming CURA-8587 --- plugins/Marketplace/PackageModel.py | 2 +- plugins/Marketplace/RestartManager.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/Marketplace/PackageModel.py b/plugins/Marketplace/PackageModel.py index 213d5bf616..1a3ebcee44 100644 --- a/plugins/Marketplace/PackageModel.py +++ b/plugins/Marketplace/PackageModel.py @@ -75,7 +75,7 @@ class PackageModel(QObject): self.enablePackageTriggered.connect(self._plugin_registry.enablePlugin) self.disablePackageTriggered.connect(self._plugin_registry.disablePlugin) - self._plugin_registry.hasPluginsEnabledOrDisabledChanged.connect(self.stateManageButtonChanged) + self._plugin_registry.pluginsEnabledOrDisabledChanged.connect(self.stateManageButtonChanged) self._package_manager.packageInstalled.connect(lambda pkg_id: self._packageInstalled(pkg_id)) self._package_manager.packageUninstalled.connect(lambda pkg_id: self._packageInstalled(pkg_id)) self._package_manager.packageInstallingFailed.connect(lambda pkg_id: self._packageInstalled(pkg_id)) diff --git a/plugins/Marketplace/RestartManager.py b/plugins/Marketplace/RestartManager.py index 19650dd64e..9fe52b4116 100644 --- a/plugins/Marketplace/RestartManager.py +++ b/plugins/Marketplace/RestartManager.py @@ -18,7 +18,7 @@ class RestartManager(QObject): self._plugin_registry: "PluginRegistry" = CuraApplication.getInstance().getPluginRegistry() self._manager.installedPackagesChanged.connect(self.checkIfRestartNeeded) - self._plugin_registry.hasPluginsEnabledOrDisabledChanged.connect(self.checkIfRestartNeeded) + self._plugin_registry.pluginsEnabledOrDisabledChanged.connect(self.checkIfRestartNeeded) self._restart_needed = False From 6703813f1de360f992b605d1cadeb1531dbf0a78 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 16 Dec 2021 10:48:36 +0100 Subject: [PATCH 177/195] Remove unused signals CURA-8587 --- plugins/Marketplace/PackageModel.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/plugins/Marketplace/PackageModel.py b/plugins/Marketplace/PackageModel.py index 1a3ebcee44..dc62682aba 100644 --- a/plugins/Marketplace/PackageModel.py +++ b/plugins/Marketplace/PackageModel.py @@ -301,10 +301,6 @@ class PackageModel(QObject): disablePackageTriggered = pyqtSignal(str) - installed = pyqtSignal(bool) - - updated = pyqtSignal(bool) - busyChanged = pyqtSignal() @pyqtSlot() From 62c6af1ef30219ed1d1fc717c9b821969ef7025b Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Thu, 16 Dec 2021 11:31:51 +0100 Subject: [PATCH 178/195] Don't show the enable button on recently installed plugins Contributes to CURA-8587 --- plugins/Marketplace/resources/qml/PackageCardHeader.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/Marketplace/resources/qml/PackageCardHeader.qml b/plugins/Marketplace/resources/qml/PackageCardHeader.qml index 41c0bd6a95..921a57870b 100644 --- a/plugins/Marketplace/resources/qml/PackageCardHeader.qml +++ b/plugins/Marketplace/resources/qml/PackageCardHeader.qml @@ -181,7 +181,7 @@ Item ManageButton { id: enableManageButton - visible: showManageButtons && packageData.isInstalled && packageData.packageType != "material" + visible: showManageButtons && packageData.isInstalled && !packageData.isToBeInstalled && packageData.packageType != "material" enabled: !packageData.busy button_style: !packageData.isActive From 8abeb24cccae9850985eece83422737033eade0d Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Thu, 16 Dec 2021 12:21:36 +0100 Subject: [PATCH 179/195] Remove a recently installed and then uninstalled package from the manage list Contributes to CURA-8587 --- plugins/Marketplace/LocalPackageList.py | 10 ++++++++++ plugins/Marketplace/PackageList.py | 1 - plugins/Marketplace/RemotePackageList.py | 2 +- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/plugins/Marketplace/LocalPackageList.py b/plugins/Marketplace/LocalPackageList.py index 616f6d95bf..3e6127ad54 100644 --- a/plugins/Marketplace/LocalPackageList.py +++ b/plugins/Marketplace/LocalPackageList.py @@ -41,11 +41,21 @@ class LocalPackageList(PackageList): self._has_footer = False self._ongoing_requests["check_updates"] = None self._package_manager.packagesWithUpdateChanged.connect(self._sortSectionsOnUpdate) + self._package_manager.packageUninstalled.connect(self._removePackageModel) def _sortSectionsOnUpdate(self) -> None: SECTION_ORDER = dict(zip([i for k, v in self.PACKAGE_CATEGORIES.items() for i in self.PACKAGE_CATEGORIES[k].values()], ["a", "b", "c", "d"])) self.sort(lambda model: f"{SECTION_ORDER[model.sectionTitle]}_{model._can_update}_{model.displayName}".lower(), key = "package") + def _removePackageModel(self, package_id): + if package_id not in self._package_manager.local_packages_ids: + index = self.find("package", package_id) + if index < 0: + Logger.error(f"Could not find card in Listview corresponding with {package_id}") + self.updatePackages() + return + self.removeItem(index) + @pyqtSlot() def updatePackages(self) -> None: """Update the list with local packages, these are materials or plugin, either bundled or user installed. The list diff --git a/plugins/Marketplace/PackageList.py b/plugins/Marketplace/PackageList.py index 4ba312ea6c..133f42ebb0 100644 --- a/plugins/Marketplace/PackageList.py +++ b/plugins/Marketplace/PackageList.py @@ -47,7 +47,6 @@ class PackageList(ListModel): self._has_footer = True self._to_install: Dict[str, str] = {} self.canInstallChanged.connect(self._requestInstall) - self._local_packages: Set[str] = {p["package_id"] for p in self._package_manager.local_packages} self._ongoing_requests: Dict[str, Optional[HttpRequestData]] = {"download_package": None} self._scope = JsonDecoratorScope(UltimakerCloudScope(CuraApplication.getInstance())) diff --git a/plugins/Marketplace/RemotePackageList.py b/plugins/Marketplace/RemotePackageList.py index e877cd9eb5..16b0e721ad 100644 --- a/plugins/Marketplace/RemotePackageList.py +++ b/plugins/Marketplace/RemotePackageList.py @@ -118,7 +118,7 @@ class RemotePackageList(PackageList): for package_data in response_data["data"]: package_id = package_data["package_id"] - if package_id in self._local_packages: + if package_id in self._package_manager.local_packages_ids: continue # We should only show packages which are not already installed try: package = PackageModel(package_data, parent = self) From 23d6c2390f80239bf66a03cbe0df54e72f53c303 Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Thu, 16 Dec 2021 12:37:41 +0100 Subject: [PATCH 180/195] Fixed the updating button Contributes to CURA-8587 --- plugins/Marketplace/PackageList.py | 5 +++-- plugins/Marketplace/PackageModel.py | 7 ++++++- plugins/Marketplace/resources/qml/PackageCardHeader.qml | 2 +- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/plugins/Marketplace/PackageList.py b/plugins/Marketplace/PackageList.py index 133f42ebb0..7eb98b3233 100644 --- a/plugins/Marketplace/PackageList.py +++ b/plugins/Marketplace/PackageList.py @@ -286,11 +286,12 @@ class PackageList(ListModel): self._package_manager.removePackage(package_id) self.unsunscribeUserFromPackage(package_id) - def updatePackage(self, package_id: str) -> None: + def updatePackage(self, package_id: str, url: str) -> None: """Update a package from the Marketplace :param package_id: the package identification string """ self._package_manager.removePackage(package_id, force_add = not self._package_manager.isBundledPackage(package_id)) - url = self._package_manager.package_infosWithUpdate[package_id]["download_url"] + if url == "": + url = self._package_manager.package_infosWithUpdate[package_id]["download_url"] self.download(package_id, url, True) diff --git a/plugins/Marketplace/PackageModel.py b/plugins/Marketplace/PackageModel.py index dc62682aba..917a54ede6 100644 --- a/plugins/Marketplace/PackageModel.py +++ b/plugins/Marketplace/PackageModel.py @@ -295,7 +295,7 @@ class PackageModel(QObject): uninstallPackageTriggered = pyqtSignal(str) - updatePackageTriggered = pyqtSignal(str) + updatePackageTriggered = pyqtSignal(str, str) enablePackageTriggered = pyqtSignal(str) @@ -308,6 +308,11 @@ class PackageModel(QObject): self.setBusy(True) self.installPackageTriggered.emit(self.packageId, self.downloadURL) + @pyqtSlot() + def update(self): + self.setBusy(True) + self.updatePackageTriggered.emit(self.packageId, self.downloadURL) + @pyqtSlot() def uninstall(self): self.uninstallPackageTriggered.emit(self.packageId) diff --git a/plugins/Marketplace/resources/qml/PackageCardHeader.qml b/plugins/Marketplace/resources/qml/PackageCardHeader.qml index 921a57870b..3d99b23907 100644 --- a/plugins/Marketplace/resources/qml/PackageCardHeader.qml +++ b/plugins/Marketplace/resources/qml/PackageCardHeader.qml @@ -232,7 +232,7 @@ Item text: busy ? catalog.i18nc("@button", "Updating..."): catalog.i18nc("@button", "Update") - onClicked: packageData.install() + onClicked: packageData.update() } } } From ffa34ab5feaf5266612bbbdbed1eb513393850d5 Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Thu, 16 Dec 2021 17:00:29 +0100 Subject: [PATCH 181/195] Make sure Signal are disconnected when PackageModel is deleted Contributes to CURA-8587 --- plugins/Marketplace/PackageList.py | 2 ++ plugins/Marketplace/PackageModel.py | 9 ++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/plugins/Marketplace/PackageList.py b/plugins/Marketplace/PackageList.py index 7eb98b3233..80588a0c12 100644 --- a/plugins/Marketplace/PackageList.py +++ b/plugins/Marketplace/PackageList.py @@ -55,6 +55,8 @@ class PackageList(ListModel): def __del__(self) -> None: """ When this object is deleted it will loop through all registered API requests and aborts them """ self.cleanUpAPIRequest() + self.isLoadingChanged.disconnect() + self.hasMoreChanged.disconnect() def abortRequest(self, request_id: str) -> None: """Aborts a single request""" diff --git a/plugins/Marketplace/PackageModel.py b/plugins/Marketplace/PackageModel.py index 917a54ede6..334b54e6f5 100644 --- a/plugins/Marketplace/PackageModel.py +++ b/plugins/Marketplace/PackageModel.py @@ -79,10 +79,17 @@ class PackageModel(QObject): self._package_manager.packageInstalled.connect(lambda pkg_id: self._packageInstalled(pkg_id)) self._package_manager.packageUninstalled.connect(lambda pkg_id: self._packageInstalled(pkg_id)) self._package_manager.packageInstallingFailed.connect(lambda pkg_id: self._packageInstalled(pkg_id)) - self._package_manager.packagesWithUpdateChanged.connect(lambda: self.setCanUpdate(self._package_id in self._package_manager.packagesWithUpdate)) + self._package_manager.packagesWithUpdateChanged.connect(self._processUpdatedPackages) self._is_busy = False + @pyqtSlot() + def _processUpdatedPackages(self): + self.setCanUpdate(self._package_id in self._package_manager.packagesWithUpdate) + + def __del__(self): + self._package_manager.packagesWithUpdateChanged.disconnect(self._processUpdatedPackages) + def __eq__(self, other: object) -> bool: if isinstance(other, PackageModel): return other == self From 0a7aee5c09a215d6f4ee0b90ec96f6b8e46451d4 Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Thu, 16 Dec 2021 17:03:14 +0100 Subject: [PATCH 182/195] Only remove Card from List when package is deleted Updated packages should still be present in the list Contribute to CURA-8587 --- plugins/Marketplace/LocalPackageList.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/Marketplace/LocalPackageList.py b/plugins/Marketplace/LocalPackageList.py index 3e6127ad54..3d4ee8cc1b 100644 --- a/plugins/Marketplace/LocalPackageList.py +++ b/plugins/Marketplace/LocalPackageList.py @@ -48,7 +48,8 @@ class LocalPackageList(PackageList): self.sort(lambda model: f"{SECTION_ORDER[model.sectionTitle]}_{model._can_update}_{model.displayName}".lower(), key = "package") def _removePackageModel(self, package_id): - if package_id not in self._package_manager.local_packages_ids: + package = self.getPackageModel(package_id) + if not package.canUpdate and package_id in self._package_manager.getPackagesToRemove() and package_id not in self._package_manager.getPackagesToInstall(): index = self.find("package", package_id) if index < 0: Logger.error(f"Could not find card in Listview corresponding with {package_id}") From 4d8592c6b77de24a372b720f6c9bf52d9530d3a8 Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Thu, 16 Dec 2021 17:07:42 +0100 Subject: [PATCH 183/195] Catch runtime errors when trying to disconnect signal Contribute to CURA-8587 --- plugins/Marketplace/PackageList.py | 7 +++++-- plugins/Marketplace/PackageModel.py | 5 ++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/plugins/Marketplace/PackageList.py b/plugins/Marketplace/PackageList.py index 80588a0c12..2c1da39182 100644 --- a/plugins/Marketplace/PackageList.py +++ b/plugins/Marketplace/PackageList.py @@ -55,8 +55,11 @@ class PackageList(ListModel): def __del__(self) -> None: """ When this object is deleted it will loop through all registered API requests and aborts them """ self.cleanUpAPIRequest() - self.isLoadingChanged.disconnect() - self.hasMoreChanged.disconnect() + try: + self.isLoadingChanged.disconnect() + self.hasMoreChanged.disconnect() + except RuntimeError: + pass def abortRequest(self, request_id: str) -> None: """Aborts a single request""" diff --git a/plugins/Marketplace/PackageModel.py b/plugins/Marketplace/PackageModel.py index 334b54e6f5..52edd60d1b 100644 --- a/plugins/Marketplace/PackageModel.py +++ b/plugins/Marketplace/PackageModel.py @@ -88,7 +88,10 @@ class PackageModel(QObject): self.setCanUpdate(self._package_id in self._package_manager.packagesWithUpdate) def __del__(self): - self._package_manager.packagesWithUpdateChanged.disconnect(self._processUpdatedPackages) + try: + self._package_manager.packagesWithUpdateChanged.disconnect(self._processUpdatedPackages) + except RuntimeError: + pass def __eq__(self, other: object) -> bool: if isinstance(other, PackageModel): From bd7a73e7ef6122abd71d553064231e9587c4466c Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Thu, 16 Dec 2021 17:31:08 +0100 Subject: [PATCH 184/195] Invert style of install button Contribute to CURA-8587 --- plugins/Marketplace/resources/qml/PackageCardHeader.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/Marketplace/resources/qml/PackageCardHeader.qml b/plugins/Marketplace/resources/qml/PackageCardHeader.qml index 3d99b23907..feebe4d8b5 100644 --- a/plugins/Marketplace/resources/qml/PackageCardHeader.qml +++ b/plugins/Marketplace/resources/qml/PackageCardHeader.qml @@ -198,7 +198,7 @@ Item visible: showManageButtons && (packageData.canDowngrade || !packageData.isBundled) enabled: !packageData.busy busy: packageData.busy - button_style: packageData.isInstalled || packageData.isToBeInstalled + button_style: !(packageData.isInstalled || packageData.isToBeInstalled) Layout.alignment: Qt.AlignTop text: From fa7ad7ddb19e44363548eeba02b04b9611f83bb9 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 16 Dec 2021 22:54:12 +0100 Subject: [PATCH 185/195] Simplify the packageList CURA-8587 --- plugins/Marketplace/PackageList.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/plugins/Marketplace/PackageList.py b/plugins/Marketplace/PackageList.py index 2c1da39182..e559d6b43e 100644 --- a/plugins/Marketplace/PackageList.py +++ b/plugins/Marketplace/PackageList.py @@ -297,6 +297,4 @@ class PackageList(ListModel): :param package_id: the package identification string """ self._package_manager.removePackage(package_id, force_add = not self._package_manager.isBundledPackage(package_id)) - if url == "": - url = self._package_manager.package_infosWithUpdate[package_id]["download_url"] self.download(package_id, url, True) From 477f62916c8aec86d71ac309621d72c8a32d0889 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 16 Dec 2021 23:21:44 +0100 Subject: [PATCH 186/195] Further simplify the data being sent over to the package manager CURA-8587 --- cura/CuraPackageManager.py | 3 --- plugins/Marketplace/LocalPackageList.py | 6 ++++-- plugins/Toolbox/src/Toolbox.py | 2 +- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/cura/CuraPackageManager.py b/cura/CuraPackageManager.py index 535d331a62..79763351b9 100644 --- a/cura/CuraPackageManager.py +++ b/cura/CuraPackageManager.py @@ -85,7 +85,4 @@ class CuraPackageManager(PackageManager): return item == self["package_id"] packages = [PkgInfo(package_info) for package in self.getAllInstalledPackagesInfo().values() for package_info in package] - packages.extend([PkgInfo(package["package_info"]) for package in self.getPackagesToRemove().values() if package["package_info"]["package_id"] not in packages]) - packages.extend([PkgInfo(package["package_info"]) for package in self.getPackagesToInstall().values() if package["package_info"]["package_id"] not in packages]) - return [dict(package) for package in packages] diff --git a/plugins/Marketplace/LocalPackageList.py b/plugins/Marketplace/LocalPackageList.py index 3d4ee8cc1b..b531c4040f 100644 --- a/plugins/Marketplace/LocalPackageList.py +++ b/plugins/Marketplace/LocalPackageList.py @@ -49,7 +49,7 @@ class LocalPackageList(PackageList): def _removePackageModel(self, package_id): package = self.getPackageModel(package_id) - if not package.canUpdate and package_id in self._package_manager.getPackagesToRemove() and package_id not in self._package_manager.getPackagesToInstall(): + if not package.canUpdate and package_id in self._package_manager.getToRemovePackageIDs() and package_id not in self._package_manager.getPackagesToInstall(): index = self.find("package", package_id) if index < 0: Logger.error(f"Could not find card in Listview corresponding with {package_id}") @@ -110,5 +110,7 @@ class LocalPackageList(PackageList): return packages = response_data["data"] - self._package_manager.setPackagesWithUpdate(dict(zip([p['package_id'] for p in packages], [p for p in packages]))) + + self._package_manager.setPackagesWithUpdate({p['package_id'] for p in packages}) + self._ongoing_requests["check_updates"] = None diff --git a/plugins/Toolbox/src/Toolbox.py b/plugins/Toolbox/src/Toolbox.py index 5644bace7a..20eec3352b 100644 --- a/plugins/Toolbox/src/Toolbox.py +++ b/plugins/Toolbox/src/Toolbox.py @@ -635,7 +635,7 @@ class Toolbox(QObject, Extension): elif request_type == "updates": # Tell the package manager that there's a new set of updates available. packages = self._server_response_data[request_type] - self._package_manager.setPackagesWithUpdate(dict(zip([p['package_id'] for p in packages], [p for p in packages]))) + self._package_manager.setPackagesWithUpdate({p['package_id'] for p in packages}) self.metadataChanged.emit() From 0cfd576c8f4fa5799c64515a47f623c6814ae441 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 16 Dec 2021 23:35:38 +0100 Subject: [PATCH 187/195] Fix setting available version numbers CURA-8587 --- plugins/Toolbox/src/Toolbox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/Toolbox/src/Toolbox.py b/plugins/Toolbox/src/Toolbox.py index 20eec3352b..e300d0ff34 100644 --- a/plugins/Toolbox/src/Toolbox.py +++ b/plugins/Toolbox/src/Toolbox.py @@ -645,7 +645,7 @@ class Toolbox(QObject, Extension): # This function goes through all known remote versions of a package and notifies the package manager of this change def _notifyPackageManager(self): for package in self._server_response_data["packages"]: - self._package_manager.addAvailablePackageVersion(package["package_id"], Version(package["package_version"]), package) + self._package_manager.addAvailablePackageVersion(package["package_id"], Version(package["package_version"])) def _onDownloadFinished(self, reply: "QNetworkReply") -> None: self.resetDownload() From 4b4229e20a47685576fe76212c27593558228d28 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 17 Dec 2021 00:44:56 +0100 Subject: [PATCH 188/195] Fix downgrading bundled packages CURA-8587 --- plugins/Marketplace/PackageModel.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/plugins/Marketplace/PackageModel.py b/plugins/Marketplace/PackageModel.py index 52edd60d1b..c67a971934 100644 --- a/plugins/Marketplace/PackageModel.py +++ b/plugins/Marketplace/PackageModel.py @@ -85,7 +85,7 @@ class PackageModel(QObject): @pyqtSlot() def _processUpdatedPackages(self): - self.setCanUpdate(self._package_id in self._package_manager.packagesWithUpdate) + self.setCanUpdate(self._package_manager.checkIfPackageCanUpdate(self._package_id)) def __del__(self): try: @@ -377,9 +377,8 @@ class PackageModel(QObject): return self._package_manager.canDowngrade(self._package_id) def setCanUpdate(self, value: bool) -> None: - if value != self._can_update: - self._can_update = value - self.stateManageButtonChanged.emit() + self._can_update = value + self.stateManageButtonChanged.emit() @pyqtProperty(bool, fset = setCanUpdate, notify = stateManageButtonChanged) def canUpdate(self) -> bool: From d015d94965cc8c5ad2cc2165e4362e8d456f9492 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 17 Dec 2021 16:23:54 +0100 Subject: [PATCH 189/195] Update plugins/Marketplace/LocalPackageList.py CURA-8587 Co-authored-by: Ghostkeeper --- plugins/Marketplace/LocalPackageList.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/Marketplace/LocalPackageList.py b/plugins/Marketplace/LocalPackageList.py index b531c4040f..95db67a58f 100644 --- a/plugins/Marketplace/LocalPackageList.py +++ b/plugins/Marketplace/LocalPackageList.py @@ -86,7 +86,7 @@ class LocalPackageList(PackageList): return package def checkForUpdates(self, packages: List[Dict[str, Any]]) -> None: - installed_packages = "installed_packages=".join([f"{package['package_id']}:{package['package_version']}&" for package in packages]) + installed_packages = "&".join([f"installed_packages={package['package_id']}:{package['package_version']}" for package in packages]) request_url = f"{PACKAGE_UPDATES_URL}?installed_packages={installed_packages[:-1]}" self._ongoing_requests["check_updates"] = HttpRequestManager.getInstance().get( From 8a198f79112ee6c14887b9128c6ff9633d6a97c7 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 17 Dec 2021 16:31:11 +0100 Subject: [PATCH 190/195] Simplify sorting CURA-8587 --- plugins/Marketplace/LocalPackageList.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/Marketplace/LocalPackageList.py b/plugins/Marketplace/LocalPackageList.py index 95db67a58f..48d06d7b0b 100644 --- a/plugins/Marketplace/LocalPackageList.py +++ b/plugins/Marketplace/LocalPackageList.py @@ -44,8 +44,8 @@ class LocalPackageList(PackageList): self._package_manager.packageUninstalled.connect(self._removePackageModel) def _sortSectionsOnUpdate(self) -> None: - SECTION_ORDER = dict(zip([i for k, v in self.PACKAGE_CATEGORIES.items() for i in self.PACKAGE_CATEGORIES[k].values()], ["a", "b", "c", "d"])) - self.sort(lambda model: f"{SECTION_ORDER[model.sectionTitle]}_{model._can_update}_{model.displayName}".lower(), key = "package") + section_order = dict(zip([i for k, v in self.PACKAGE_CATEGORIES.items() for i in self.PACKAGE_CATEGORIES[k].values()], ["a", "b", "c", "d"])) + self.sort(lambda model: (section_order[model.sectionTitle], model.canUpdate, model.displayName.lower()), key = "package") def _removePackageModel(self, package_id): package = self.getPackageModel(package_id) From 83c78c4b4db9e9da3a0532fe26898a1da4af4bf3 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 17 Dec 2021 16:41:52 +0100 Subject: [PATCH 191/195] Remove canInstallChanged signal It's not needed, this can just be handled with a direct call! CURA-8587 --- plugins/Marketplace/PackageList.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/plugins/Marketplace/PackageList.py b/plugins/Marketplace/PackageList.py index e559d6b43e..73393aa9da 100644 --- a/plugins/Marketplace/PackageList.py +++ b/plugins/Marketplace/PackageList.py @@ -46,7 +46,6 @@ class PackageList(ListModel): self._has_more = False self._has_footer = True self._to_install: Dict[str, str] = {} - self.canInstallChanged.connect(self._requestInstall) self._ongoing_requests: Dict[str, Optional[HttpRequestData]] = {"download_package": None} self._scope = JsonDecoratorScope(UltimakerCloudScope(CuraApplication.getInstance())) @@ -135,8 +134,6 @@ class PackageList(ListModel): index = self.find("package", package_id) return self.getItem(index)["package"] - canInstallChanged = pyqtSignal(str, bool) - def _openLicenseDialog(self, package_id: str, license_content: str) -> None: plugin_path = self._plugin_registry.getPluginPath("Marketplace") if plugin_path is None: @@ -236,7 +233,8 @@ class PackageList(ListModel): temp_file.close() self._to_install[package_id] = temp_file.name self._ongoing_requests["download_package"] = None - self.canInstallChanged.emit(package_id, update) + self._requestInstall(package_id, update) + def _downloadError(self, package_id: str, update: bool = False, reply: Optional["QNetworkReply"] = None, error: Optional["QNetworkReply.NetworkError"] = None) -> None: if reply: From a2a76fefddc0254480f28f7a6953b26f686e1be9 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 17 Dec 2021 16:48:06 +0100 Subject: [PATCH 192/195] Move cleaning up of request to after signals are disconnected CURA-8587 --- plugins/Marketplace/PackageList.py | 4 +++- plugins/Marketplace/PackageModel.py | 5 +---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/plugins/Marketplace/PackageList.py b/plugins/Marketplace/PackageList.py index 73393aa9da..1a76d65141 100644 --- a/plugins/Marketplace/PackageList.py +++ b/plugins/Marketplace/PackageList.py @@ -53,13 +53,15 @@ class PackageList(ListModel): def __del__(self) -> None: """ When this object is deleted it will loop through all registered API requests and aborts them """ - self.cleanUpAPIRequest() + try: self.isLoadingChanged.disconnect() self.hasMoreChanged.disconnect() except RuntimeError: pass + self.cleanUpAPIRequest() + def abortRequest(self, request_id: str) -> None: """Aborts a single request""" if request_id in self._ongoing_requests and self._ongoing_requests[request_id]: diff --git a/plugins/Marketplace/PackageModel.py b/plugins/Marketplace/PackageModel.py index c67a971934..307cdce986 100644 --- a/plugins/Marketplace/PackageModel.py +++ b/plugins/Marketplace/PackageModel.py @@ -88,10 +88,7 @@ class PackageModel(QObject): self.setCanUpdate(self._package_manager.checkIfPackageCanUpdate(self._package_id)) def __del__(self): - try: - self._package_manager.packagesWithUpdateChanged.disconnect(self._processUpdatedPackages) - except RuntimeError: - pass + self._package_manager.packagesWithUpdateChanged.disconnect(self._processUpdatedPackages) def __eq__(self, other: object) -> bool: if isinstance(other, PackageModel): From 8df204b327e8503b2de6978e7b220105d6016462 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 17 Dec 2021 16:49:16 +0100 Subject: [PATCH 193/195] Remove unneeded control CURA-8587 --- .../resources/qml/PackageCardHeader.qml | 26 ------------------- 1 file changed, 26 deletions(-) diff --git a/plugins/Marketplace/resources/qml/PackageCardHeader.qml b/plugins/Marketplace/resources/qml/PackageCardHeader.qml index feebe4d8b5..0bf93fc67c 100644 --- a/plugins/Marketplace/resources/qml/PackageCardHeader.qml +++ b/plugins/Marketplace/resources/qml/PackageCardHeader.qml @@ -69,32 +69,6 @@ Item visible: packageData.isCheckedByUltimaker } - Control - { - Layout.preferredWidth: UM.Theme.getSize("card_tiny_icon").width - Layout.preferredHeight: UM.Theme.getSize("card_tiny_icon").height - Layout.alignment: Qt.AlignCenter - enabled: false // remove! - visible: false // replace packageInfo.XXXXXX - // TODO: waiting for materials card implementation - - Cura.ToolTip - { - tooltipText: "" // TODO - visible: parent.hovered - } - - UM.RecolorImage - { - anchors.fill: parent - - color: UM.Theme.getColor("primary") - source: UM.Theme.getIcon("CheckCircle") // TODO - } - - // onClicked: Qt.openUrlExternally( XXXXXX ) // TODO - } - Label { id: packageVersionLabel From afef4f761bbda772ba62b85af8b718669aef8aee Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 20 Dec 2021 10:17:30 +0100 Subject: [PATCH 194/195] Update documentation CURA-8587 --- plugins/Marketplace/LocalPackageList.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/plugins/Marketplace/LocalPackageList.py b/plugins/Marketplace/LocalPackageList.py index 48d06d7b0b..8adb1e841e 100644 --- a/plugins/Marketplace/LocalPackageList.py +++ b/plugins/Marketplace/LocalPackageList.py @@ -35,7 +35,6 @@ class LocalPackageList(PackageList): } } # The section headers to be used for the different package categories - def __init__(self, parent: Optional["QObject"] = None) -> None: super().__init__(parent) self._has_footer = False @@ -47,9 +46,15 @@ class LocalPackageList(PackageList): section_order = dict(zip([i for k, v in self.PACKAGE_CATEGORIES.items() for i in self.PACKAGE_CATEGORIES[k].values()], ["a", "b", "c", "d"])) self.sort(lambda model: (section_order[model.sectionTitle], model.canUpdate, model.displayName.lower()), key = "package") - def _removePackageModel(self, package_id): + def _removePackageModel(self, package_id: str) -> None: + """ + Cleanup function to remove the package model from the list. Note that this is only done if the package can't + be updated, it is in the to remove list and isn't in the to be installed list + """ package = self.getPackageModel(package_id) - if not package.canUpdate and package_id in self._package_manager.getToRemovePackageIDs() and package_id not in self._package_manager.getPackagesToInstall(): + if not package.canUpdate and \ + package_id in self._package_manager.getToRemovePackageIDs() and \ + package_id not in self._package_manager.getPackagesToInstall(): index = self.find("package", package_id) if index < 0: Logger.error(f"Could not find card in Listview corresponding with {package_id}") From 6dac500f187470ba8aa3e9f42faaa1b2fcb57a0a Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 20 Dec 2021 10:30:53 +0100 Subject: [PATCH 195/195] Simplify getAllLocalPackages CURA-8587 --- cura/CuraPackageManager.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/cura/CuraPackageManager.py b/cura/CuraPackageManager.py index 79763351b9..af75aa7b66 100644 --- a/cura/CuraPackageManager.py +++ b/cura/CuraPackageManager.py @@ -76,13 +76,10 @@ class CuraPackageManager(PackageManager): return machine_with_materials, machine_with_qualities def getAllLocalPackages(self) -> List[Dict[str, Any]]: - """ returns an unordered list of all the package_info installed, to be installed or to be returned""" + """ Returns an unordered list of all the package_info of installed, to be installed, or bundled packages""" + packages: List[Dict[str, Any]] = [] + for packages_to_add in self.getAllInstalledPackagesInfo().values(): + packages.extend(packages_to_add) - class PkgInfo(dict): - # Needed helper class because a dict isn't hashable - def __eq__(self, item): - return item == self["package_id"] - - packages = [PkgInfo(package_info) for package in self.getAllInstalledPackagesInfo().values() for package_info in package] - return [dict(package) for package in packages] + return packages