mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-08-07 14:04:03 -06:00
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:
commit
e04021be37
6 changed files with 308 additions and 187 deletions
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
12
plugins/Marketplace/resources/qml/Materials.qml
Normal file
12
plugins/Marketplace/resources/qml/Materials.qml
Normal 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"
|
||||
}
|
||||
}
|
26
plugins/Marketplace/resources/qml/PackageTypeTab.qml
Normal file
26
plugins/Marketplace/resources/qml/PackageTypeTab.qml
Normal 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
|
||||
}
|
||||
}
|
175
plugins/Marketplace/resources/qml/Packages.qml
Normal file
175
plugins/Marketplace/resources/qml/Packages.qml
Normal 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue