Differentiate between local and remote packages

There is a distinction between packages which are already
installed on the local machine and packages which are
available on the remote server. Even with this difference
it is important that they are handled the same and can be
reused in the same GUI elements.

In order to reduce code duplication I created a parent object
PackageList which contains the base logic and interface for
the QML and let both RemotePackageList and LocalPackageList
inherit from this.

UX specified that the gear icon (Settings.svg) should be
separate from the tabs of material and plugins. This also
ment that the current tab  item couldn't set the pageTitle
anymore. This is now defined in the Package component and
set when the loader has loaded the external QML file.

Contributes to CURA-8558
This commit is contained in:
Jelle Spijker 2021-11-01 17:02:07 +01:00
parent 03e1fc34b4
commit 86d5d315bc
No known key found for this signature in database
GPG key ID: 6662DC033BE6B99A
11 changed files with 307 additions and 136 deletions

View file

@ -2,19 +2,10 @@
# 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 typing import 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.Qt.ListModel import ListModel
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.
from .PackageModel import PackageModel # The contents of this list.
if TYPE_CHECKING:
from PyQt5.QtCore import QObject
@ -23,99 +14,60 @@ catalog = i18nCatalog("cura")
class PackageList(ListModel):
"""
Represents a list of packages to be displayed in the interface.
The list can be filtered (e.g. on package type, materials vs. plug-ins) and
paginated.
"""
PackageRole = Qt.UserRole + 1
ITEMS_PER_PAGE = 20 # Pagination of number of elements to download at once.
def __init__(self, parent: "QObject" = None) -> None:
super().__init__(parent)
self._ongoing_request: Optional[HttpRequestData] = None
self._scope = JsonDecoratorScope(UltimakerCloudScope(CuraApplication.getInstance()))
self._error_message = ""
self._package_type_filter = ""
self._request_url = self._initialRequestUrl()
self.addRoleName(self.PackageRole, "package")
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()
self._is_loading = False
self._has_more = False
@pyqtSlot()
def request(self) -> None:
def updatePackages(self) -> None:
"""
Make a request for the first paginated page of packages.
When the request is done, the list will get updated with the new package models.
Initialize the first page of packages
"""
self.setErrorMessage("") # Clear any previous errors.
http = HttpRequestManager.getInstance()
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 abortUpdating(self) -> None:
pass
def reset(self) -> None:
self.clear()
self._request_url = self._initialRequestUrl()
isLoadingChanged = pyqtSignal()
@pyqtProperty(bool, notify = isLoadingChanged)
def setIsLoading(self, value: bool) -> None:
if self._is_loading != value:
self._is_loading = value
self.isLoadingChanged.emit()
@pyqtProperty(bool, fset = setIsLoading, 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: ``True`` if the list is being gathered, or ``False`` if .
"""
return self._ongoing_request is not None
return self._is_loading
hasMoreChanged = pyqtSignal()
@pyqtProperty(bool, notify = hasMoreChanged)
def setHasMore(self, value: bool) -> None:
if self._has_more != value:
self._has_more = value
self.hasMoreChanged.emit()
@pyqtProperty(bool, fset = setHasMore, notify = hasMoreChanged)
def hasMore(self) -> bool:
"""
Returns whether there are more packages to load.
:return: ``True`` if there are more packages to load, or ``False`` if we've reached the last page of the
pagination.
"""
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
return self._has_more
def setErrorMessage(self, error_message: str) -> None:
if self._error_message != error_message:
@ -133,49 +85,3 @@ class PackageList(ListModel):
:return: An error message, if any, or an empty string if everything went okay.
"""
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.
This converts that response into PackageModels, and triggers the ListModel to update.
:param reply: A reply containing information about a number of packages.
"""
response_data = HttpRequestManager.readJSON(reply)
if "data" not in response_data or "links" 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"]:
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._ongoing_request = None
self.isLoadingChanged.emit()
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.
"""
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()