Merge pull request #10694 from Ultimaker/CURA-8557_plugins_vs_materials

Add tabs for materials vs. plug-ins to new Marketplace
This commit is contained in:
Jaime van Kessel 2021-10-29 11:46:36 +02:00 committed by GitHub
commit e04021be37
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 308 additions and 187 deletions

View file

@ -2,6 +2,7 @@
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, Qt
from PyQt5.QtNetwork import QNetworkReply
from typing import Optional, TYPE_CHECKING
from cura.CuraApplication import CuraApplication
@ -9,7 +10,7 @@ from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope # To ma
from UM.i18n import i18nCatalog
from UM.Logger import Logger
from UM.Qt.ListModel import ListModel
from UM.TaskManagement.HttpRequestManager import HttpRequestManager # To request the package list from the API.
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 . import Marketplace # To get the list of packages. Imported this way to prevent circular imports.
@ -17,7 +18,6 @@ from .PackageModel import PackageModel # The contents of this list.
if TYPE_CHECKING:
from PyQt5.QtCore import QObject
from PyQt5.QtNetwork import QNetworkReply
catalog = i18nCatalog("cura")
@ -33,19 +33,25 @@ class PackageList(ListModel):
PackageRole = Qt.UserRole + 1
ITEMS_PER_PAGE = 20 # Pagination of number of elements to download at once.
INCLUDED_PACKAGE_TYPE = ("material", "plugin") # Only show these kind of packages
def __init__(self, parent: "QObject" = None) -> None:
super().__init__(parent)
self._is_loading = True
self._ongoing_request: Optional[HttpRequestData] = None
self._scope = JsonDecoratorScope(UltimakerCloudScope(CuraApplication.getInstance()))
self._request_url = f"{Marketplace.PACKAGES_URL}?limit={self.ITEMS_PER_PAGE}"
self._error_message = ""
self._package_type_filter = ""
self._request_url = self._initialRequestUrl()
self.addRoleName(self.PackageRole, "package")
self.request()
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.abortRequest()
@pyqtSlot()
def request(self) -> None:
@ -54,31 +60,35 @@ class PackageList(ListModel):
When the request is done, the list will get updated with the new package models.
"""
self.setIsLoading(True)
self.setErrorMessage("") # Clear any previous errors.
http = HttpRequestManager.getInstance()
http.get(
self._ongoing_request = http.get(
self._request_url,
scope = self._scope,
callback = self._parseResponse,
error_callback = self._onError
)
self.isLoadingChanged.emit()
@pyqtSlot()
def abortRequest(self) -> None:
HttpRequestManager.getInstance().abortRequest(self._ongoing_request)
self._ongoing_request = None
def reset(self) -> None:
self.clear()
self._request_url = self._initialRequestUrl()
isLoadingChanged = pyqtSignal()
def setIsLoading(self, is_loading: bool) -> None:
if is_loading != self._is_loading:
self._is_loading = is_loading
self.isLoadingChanged.emit()
@pyqtProperty(bool, notify = isLoadingChanged, fset = setIsLoading)
@pyqtProperty(bool, notify = isLoadingChanged)
def isLoading(self) -> bool:
"""
Gives whether the list is currently loading the first page or loading more pages.
:return: ``True`` if the list is downloading, or ``False`` if not downloading.
"""
return self._is_loading
return self._ongoing_request is not None
hasMoreChanged = pyqtSignal()
@ -91,6 +101,22 @@ class PackageList(ListModel):
"""
return self._request_url != ""
packageTypeFilterChanged = pyqtSignal()
def setPackageTypeFilter(self, new_filter: str) -> None:
if new_filter != self._package_type_filter:
self._package_type_filter = new_filter
self.reset()
self.packageTypeFilterChanged.emit()
@pyqtProperty(str, notify = packageTypeFilterChanged, fset = setPackageTypeFilter)
def packageTypeFilter(self) -> str:
"""
Get the package type this package list is filtering on, like ``plugin`` or ``material``.
:return: The package type this list is filtering on.
"""
return self._package_type_filter
def setErrorMessage(self, error_message: str) -> None:
if self._error_message != error_message:
self._error_message = error_message
@ -108,6 +134,15 @@ class PackageList(ListModel):
"""
return self._error_message
def _initialRequestUrl(self) -> str:
"""
Get the URL to request the first paginated page with.
:return: A URL to request.
"""
if self._package_type_filter != "":
return f"{Marketplace.PACKAGES_URL}?package_type={self._package_type_filter}&limit={self.ITEMS_PER_PAGE}"
return f"{Marketplace.PACKAGES_URL}?limit={self.ITEMS_PER_PAGE}"
def _parseResponse(self, reply: "QNetworkReply") -> None:
"""
Parse the response from the package list API request.
@ -122,19 +157,25 @@ class PackageList(ListModel):
return
for package_data in response_data["data"]:
if package_data["package_type"] in self.INCLUDED_PACKAGE_TYPE:
package = PackageModel(package_data, parent = self)
self.appendItem({"package": package}) # Add it to this list model.
package = PackageModel(package_data, parent = self)
self.appendItem({"package": package}) # Add it to this list model.
self._request_url = response_data["links"].get("next", "") # Use empty string to signify that there is no next page.
self.hasMoreChanged.emit()
self.setIsLoading(False)
self._ongoing_request = None
self.isLoadingChanged.emit()
def _onError(self, reply: "QNetworkReply", error: Optional["QNetworkReply.NetworkError"]) -> None:
def _onError(self, reply: "QNetworkReply", error: Optional[QNetworkReply.NetworkError]) -> None:
"""
Handles networking and server errors when requesting the list of packages.
:param reply: The reply with packages. This will most likely be incomplete and should be ignored.
:param error: The error status of the request.
"""
Logger.error(f"Could not reach Marketplace server.")
if error == QNetworkReply.NetworkError.OperationCanceledError:
Logger.debug("Cancelled request for packages.")
self._ongoing_request = 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.isLoadingChanged.emit()

View file

@ -66,6 +66,35 @@ Window
text: catalog.i18nc("@header", "Install Plugins")
}
}
Item
{
Layout.preferredWidth: parent.width
Layout.preferredHeight: childrenRect.height
TabBar //Page selection.
{
id: pageSelectionTabBar
anchors.right: parent.right
anchors.rightMargin: UM.Theme.getSize("default_margin").width
spacing: 0
PackageTypeTab
{
width: implicitWidth
text: catalog.i18nc("@button", "Plug-ins")
onClicked: content.source = "Plugins.qml"
}
PackageTypeTab
{
width: implicitWidth
text: catalog.i18nc("@button", "Materials")
onClicked: content.source = "Materials.qml"
}
}
}
Rectangle //Page contents.
{
Layout.preferredWidth: parent.width

View file

@ -0,0 +1,12 @@
// Copyright (c) 2021 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import Marketplace 1.0 as Marketplace
Packages
{
model: Marketplace.PackageList
{
packageTypeFilter: "material"
}
}

View file

@ -0,0 +1,26 @@
// 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 UM 1.0 as UM
TabButton
{
background: Rectangle
{
anchors.fill: parent
color: parent.checked ? UM.Theme.getColor("main_background") : UM.Theme.getColor("detail_background")
border.color: UM.Theme.getColor("detail_background")
border.width: UM.Theme.getSize("thick_lining").width
}
contentItem: Label
{
text: parent.text
font: UM.Theme.getFont("medium")
color: UM.Theme.getColor("text")
width: contentWidth
anchors.centerIn: parent
}
}

View file

@ -0,0 +1,175 @@
// 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 UM 1.4 as UM
ScrollView
{
id: packages
clip: true
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
property alias model: packagesListview.model
Component.onCompleted: model.request()
Component.onDestruction: model.abortRequest()
ListView
{
id: packagesListview
width: parent.width
spacing: UM.Theme.getSize("default_margin").height
delegate: Rectangle
{
width: packagesListview.width
height: UM.Theme.getSize("card").height
color: UM.Theme.getColor("main_background")
radius: UM.Theme.getSize("default_radius").width
Label
{
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: Math.round((parent.height - height) / 2)
text: model.package.displayName
font: UM.Theme.getFont("medium_bold")
color: UM.Theme.getColor("text")
}
}
footer: Item //Wrapper item to add spacing between content and footer.
{
width: parent.width
height: UM.Theme.getSize("card").height + packagesListview.spacing
Button
{
id: loadMoreButton
width: parent.width
height: UM.Theme.getSize("card").height
anchors.bottom: parent.bottom
enabled: packages.model.hasMore && !packages.model.isLoading || packages.model.errorMessage != ""
onClicked: packages.model.request() //Load next page in plug-in list.
background: Rectangle
{
anchors.fill: parent
radius: UM.Theme.getSize("default_radius").width
color: UM.Theme.getColor("main_background")
}
Row
{
anchors.centerIn: parent
spacing: UM.Theme.getSize("thin_margin").width
states:
[
State
{
name: "Error"
when: packages.model.errorMessage != ""
PropertyChanges
{
target: errorIcon
visible: true
}
PropertyChanges
{
target: loadMoreIcon
visible: false
}
PropertyChanges
{
target: loadMoreLabel
text: catalog.i18nc("@button", "Failed to load packages:") + " " + packages.model.errorMessage + "\n" + catalog.i18nc("@button", "Retry?")
}
},
State
{
name: "Loading"
when: packages.model.isLoading
PropertyChanges
{
target: loadMoreIcon
source: UM.Theme.getIcon("ArrowDoubleCircleRight")
color: UM.Theme.getColor("action_button_disabled_text")
}
PropertyChanges
{
target: loadMoreLabel
text: catalog.i18nc("@button", "Loading")
color: UM.Theme.getColor("action_button_disabled_text")
}
},
State
{
name: "LastPage"
when: !packages.model.hasMore
PropertyChanges
{
target: loadMoreIcon
visible: false
}
PropertyChanges
{
target: loadMoreLabel
text: catalog.i18nc("@button", "No more results to load")
color: UM.Theme.getColor("action_button_disabled_text")
}
}
]
Item
{
width: (errorIcon.visible || loadMoreIcon.visible) ? UM.Theme.getSize("small_button_icon").width : 0
height: UM.Theme.getSize("small_button_icon").height
anchors.verticalCenter: loadMoreLabel.verticalCenter
UM.StatusIcon
{
id: errorIcon
anchors.fill: parent
status: UM.StatusIcon.Status.ERROR
visible: false
}
UM.RecolorImage
{
id: loadMoreIcon
anchors.fill: parent
source: UM.Theme.getIcon("ArrowDown")
color: UM.Theme.getColor("secondary_button_text")
RotationAnimator
{
target: loadMoreIcon
from: 0
to: 360
duration: 1000
loops: Animation.Infinite
running: packages.model.isLoading
alwaysRunToEnd: true
}
}
}
Label
{
id: loadMoreLabel
text: catalog.i18nc("@button", "Load more")
font: UM.Theme.getFont("medium_bold")
color: UM.Theme.getColor("secondary_button_text")
}
}
}
}
}
}

View file

@ -1,174 +1,12 @@
// 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 Marketplace 1.0 as Marketplace
import UM 1.4 as UM
ScrollView
Packages
{
clip: true
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ListView
model: Marketplace.PackageList
{
id: pluginColumn
width: parent.width
model: Marketplace.PackageList
{
id: pluginList
}
spacing: UM.Theme.getSize("default_margin").height
delegate: Rectangle
{
width: pluginColumn.width
height: UM.Theme.getSize("card").height
color: UM.Theme.getColor("main_background")
radius: UM.Theme.getSize("default_radius").width
Label
{
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: Math.round((parent.height - height) / 2)
text: model.package.displayName
font: UM.Theme.getFont("medium_bold")
color: UM.Theme.getColor("text")
}
}
footer: Item //Wrapper item to add spacing between content and footer.
{
width: parent.width
height: UM.Theme.getSize("card").height + pluginColumn.spacing
Button
{
id: loadMoreButton
width: parent.width
height: UM.Theme.getSize("card").height
anchors.bottom: parent.bottom
enabled: pluginList.hasMore && !pluginList.isLoading || pluginList.errorMessage != ""
onClicked: pluginList.request() //Load next page in plug-in list.
background: Rectangle
{
anchors.fill: parent
radius: UM.Theme.getSize("default_radius").width
color: UM.Theme.getColor("main_background")
}
Row
{
anchors.centerIn: parent
spacing: UM.Theme.getSize("thin_margin").width
states:
[
State
{
name: "Error"
when: pluginList.errorMessage != ""
PropertyChanges
{
target: errorIcon
visible: true
}
PropertyChanges
{
target: loadMoreIcon
visible: false
}
PropertyChanges
{
target: loadMoreLabel
text: catalog.i18nc("@button", "Failed to load plug-ins:") + " " + pluginList.errorMessage + "\n" + catalog.i18nc("@button", "Retry?")
}
},
State
{
name: "Loading"
when: pluginList.isLoading
PropertyChanges
{
target: loadMoreIcon
source: UM.Theme.getIcon("ArrowDoubleCircleRight")
color: UM.Theme.getColor("action_button_disabled_text")
}
PropertyChanges
{
target: loadMoreLabel
text: catalog.i18nc("@button", "Loading")
color: UM.Theme.getColor("action_button_disabled_text")
}
},
State
{
name: "LastPage"
when: !pluginList.hasMore
PropertyChanges
{
target: loadMoreIcon
visible: false
}
PropertyChanges
{
target: loadMoreLabel
text: catalog.i18nc("@button", "No more results to load")
color: UM.Theme.getColor("action_button_disabled_text")
}
}
]
Item
{
width: (errorIcon.visible || loadMoreIcon.visible) ? UM.Theme.getSize("small_button_icon").width : 0
height: UM.Theme.getSize("small_button_icon").height
anchors.verticalCenter: loadMoreLabel.verticalCenter
UM.StatusIcon
{
id: errorIcon
anchors.fill: parent
status: UM.StatusIcon.Status.ERROR
visible: false
}
UM.RecolorImage
{
id: loadMoreIcon
anchors.fill: parent
source: UM.Theme.getIcon("ArrowDown")
color: UM.Theme.getColor("secondary_button_text")
RotationAnimator
{
target: loadMoreIcon
from: 0
to: 360
duration: 1000
loops: Animation.Infinite
running: pluginList.isLoading
alwaysRunToEnd: true
}
}
}
Label
{
id: loadMoreLabel
text: catalog.i18nc("@button", "Load more")
font: UM.Theme.getFont("medium_bold")
color: UM.Theme.getColor("secondary_button_text")
}
}
}
}
packageTypeFilter: "plugin"
}
}
}