Merge pull request #11027 from Ultimaker/CURA-8587_disable_update_install_and_uninstall

Cura 8587 disable update install and uninstall
This commit is contained in:
Casper Lamboo 2021-12-20 18:58:14 +01:00 committed by GitHub
commit 6321b2b892
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 1454 additions and 711 deletions

View file

@ -1,13 +1,15 @@
# 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, cast, Dict, List, Set, 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")
if TYPE_CHECKING:
from UM.Qt.QtApplication import QtApplication
@ -17,6 +19,31 @@ 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.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]]:
"""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 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_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_ids)
def initialize(self) -> None:
self._installation_dirs_dict["materials"] = Resources.getStoragePath(CuraApplication.ResourceTypes.MaterialInstanceContainer)
@ -47,3 +74,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) -> List[Dict[str, Any]]:
""" 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)
return packages

View file

@ -0,0 +1,12 @@
# 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"

View file

@ -1,40 +1,66 @@
# 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
from typing import Any, Dict, Generator, List, Optional, TYPE_CHECKING
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 cura.CuraApplication import CuraApplication
from UM.TaskManagement.HttpRequestManager import HttpRequestManager
from UM.Logger import Logger
from .PackageList import PackageList
from .PackageModel import PackageModel # The contents of this list.
from .PackageModel import PackageModel
from .Constants import PACKAGE_UPDATES_URL
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
def __init__(self, parent: Optional["QObject"] = None) -> None:
super().__init__(parent)
self._manager = CuraApplication.getInstance().getPackageManager()
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: (section_order[model.sectionTitle], model.canUpdate, model.displayName.lower()), key = "package")
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():
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:
@ -44,50 +70,52 @@ 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._package_manager.local_packages]])
self._sortSectionsOnUpdate()
self.checkForUpdates(self._package_manager.local_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())
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_id = package_info["package_id"]
bundled_or_installed = "bundled" if self._package_manager.isBundledPackage(package_id) else "installed"
package_type = package_info["package_type"]
section_title = self.PACKAGE_SECTION_HEADER[bundled_or_installed][package_type]
return PackageModel(package_info, installation_status = bundled_or_installed, section_title = section_title, parent = self)
section_title = self.PACKAGE_CATEGORIES[bundled_or_installed][package_type]
package = PackageModel(package_info, section_title = section_title, parent = self)
self._connectManageButtonSignals(package)
return package
def checkForUpdates(self, packages: List[Dict[str, Any]]) -> None:
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(
request_url,
scope = self._scope,
callback = self._parseResponse
)
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' from response data. Keys in response: {response_data.keys()}")
return
if len(response_data["data"]) == 0:
return
packages = response_data["data"]
self._package_manager.setPackagesWithUpdate({p['package_id'] for p in packages})
self._ongoing_requests["check_updates"] = None

View file

@ -6,22 +6,18 @@ 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).
from .RemotePackageList import RemotePackageList # To register this type with QML.
from .LocalPackageList import LocalPackageList # To register this type with QML.
from .RestartManager import RestartManager # 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.
class Marketplace(Extension):
"""
@ -31,9 +27,11 @@ 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
qmlRegisterType(RemotePackageList, "Marketplace", 1, 0, "RemotePackageList")
qmlRegisterType(LocalPackageList, "Marketplace", 1, 0, "LocalPackageList")
qmlRegisterType(RestartManager, "Marketplace", 1, 0, "RestartManager")
@pyqtSlot()
def show(self) -> None:
@ -43,6 +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()
plugin_path = PluginRegistry.getInstance().getPluginPath(self.getPluginId())
if plugin_path is None:
plugin_path = os.path.dirname(__file__)

View file

@ -1,14 +1,29 @@
# 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
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, Qt
from typing import Optional, TYPE_CHECKING
from typing import cast, Dict, Optional, Set, 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.Logger import Logger
from UM import PluginRegistry
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 .Constants import USER_PACKAGES_URL, PACKAGES_URL
if TYPE_CHECKING:
from PyQt5.QtCore import QObject
from PyQt5.QtNetwork import QNetworkReply
catalog = i18nCatalog("cura")
@ -18,26 +33,51 @@ 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)
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 = ""
self.addRoleName(self.PackageRole, "package")
self._is_loading = False
self._has_more = False
self._has_footer = True
self._to_install: Dict[str, str] = {}
self._ongoing_requests: Dict[str, Optional[HttpRequestData]] = {"download_package": None}
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 """
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]:
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()
@ -91,3 +131,170 @@ 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 getPackageModel(self, package_id: str) -> PackageModel:
index = self.find("package", package_id)
return self.getItem(index)["package"]
def _openLicenseDialog(self, package_id: str, license_content: str) -> None:
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:
# close dialog
dialog = self._license_dialogs.pop(package_id)
if dialog is not None:
dialog.deleteLater()
# install relevant package
self._install(package_id)
@pyqtSlot(str)
def onLicenseDeclined(self, package_id: str) -> None:
# close dialog
dialog = self._license_dialogs.pop(package_id)
if dialog is not None:
dialog.deleteLater()
# reset package card
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._package_manager.getPackageLicense(package_path)
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)
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)
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))
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
"""
if url == "":
url = f"{PACKAGES_URL}/{package_id}/download"
def downloadFinished(reply: "QNetworkReply") -> None:
self._downloadFinished(package_id, reply, update)
def downloadError(reply: "QNetworkReply", error: "QNetworkReply.NetworkError") -> None:
self._downloadError(package_id, update, reply, error)
self._ongoing_requests["download_package"] = HttpRequestManager.getInstance().get(
url,
scope = self._scope,
callback = downloadFinished,
error_callback = downloadError
)
def _downloadFinished(self, package_id: str, reply: "QNetworkReply", update: bool = False) -> None:
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)
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._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:
reply_string = bytes(reply.readAll()).decode()
Logger.error(f"Failed to download package: {package_id} due to {reply_string}")
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
:param package_id: the package identification string
:param sdk_version: the SDK version
"""
if self._account.isLoggedIn:
HttpRequestManager.getInstance().put(
url = USER_PACKAGES_URL,
data = json.dumps({"data": {"package_id": package_id, "sdk_version": sdk_version}}).encode(),
scope = self._scope
)
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:
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.updatePackage)
def installPackage(self, package_id: str, url: str) -> None:
"""Install a package from the Marketplace
:param package_id: the package identification string
"""
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
:param package_id: the package identification string
"""
self._package_manager.removePackage(package_id)
self.unsunscribeUserFromPackage(package_id)
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))
self.download(package_id, url, True)

View file

@ -1,12 +1,19 @@
# 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, QObject
import re
from typing import Any, Dict, List, Optional
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
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")
@ -14,26 +21,28 @@ 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], 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).
"""
super().__init__(parent)
QQmlEngine.setObjectOwnership(self, QQmlEngine.CppOwnership)
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_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", [])
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)
@ -58,10 +67,40 @@ 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._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._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))
self._package_manager.packagesWithUpdateChanged.connect(self._processUpdatedPackages)
self._is_busy = False
@pyqtSlot()
def _processUpdatedPackages(self):
self.setCanUpdate(self._package_manager.checkIfPackageCanUpdate(self._package_id))
def __del__(self):
self._package_manager.packagesWithUpdateChanged.disconnect(self._processUpdatedPackages)
def __eq__(self, other: object) -> bool:
if isinstance(other, PackageModel):
return other == self
elif isinstance(other, str):
return other == self._package_id
else:
return False
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:
"""
Searches the package data for a link of a certain type.
@ -175,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)
@ -187,37 +226,33 @@ 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):
return self._author_info_url
@pyqtProperty(str, constant = True)
def installationStatus(self) -> str:
return self._installation_status
def authorInfoUrl(self) -> str:
return self._author_info_url
@pyqtProperty(str, constant = True)
def sectionTitle(self) -> Optional[str]:
@ -250,3 +285,99 @@ class PackageModel(QObject):
@pyqtProperty(bool, constant = True)
def isCompatibleAirManager(self) -> bool:
return self._is_compatible_air_manager
@pyqtProperty(bool, constant = True)
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, str)
uninstallPackageTriggered = pyqtSignal(str)
updatePackageTriggered = pyqtSignal(str, str)
enablePackageTriggered = pyqtSignal(str)
disablePackageTriggered = pyqtSignal(str)
busyChanged = pyqtSignal()
@pyqtSlot()
def install(self):
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)
@pyqtProperty(bool, notify= busyChanged)
def busy(self):
"""
Property indicating that some kind of upgrade is active.
"""
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
try:
self.busyChanged.emit()
except RuntimeError:
pass
def _packageInstalled(self, package_id: str) -> None:
if self._package_id != package_id:
return
self.setBusy(False)
try:
self.stateManageButtonChanged.emit()
except RuntimeError:
pass
@pyqtProperty(bool, notify = stateManageButtonChanged)
def isInstalled(self) -> bool:
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:
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 setCanUpdate(self, value: bool) -> None:
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

View file

@ -1,18 +1,15 @@
# 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
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 .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.
@ -28,23 +25,14 @@ 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 = ""
self._request_url = self._initialRequestUrl()
self._ongoing_requests["get_packages"] = None
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:
"""
@ -55,18 +43,13 @@ 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,
error_callback = self._onError
)
@pyqtSlot()
def abortUpdating(self) -> None:
HttpRequestManager.getInstance().abortRequest(self._ongoing_request)
self._ongoing_request = None
def reset(self) -> None:
self.clear()
self._request_url = self._initialRequestUrl()
@ -113,7 +96,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 != "":
@ -134,18 +117,21 @@ class RemotePackageList(PackageList):
return
for package_data in response_data["data"]:
installation_status = "installed" if CuraApplication.getInstance().getPackageManager().isUserInstalledPackage(package_data["package_id"]) else "not_installed"
package_id = package_data["package_id"]
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, installation_status, parent = self)
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
# 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_request = None
self._ongoing_requests["get_packages"] = None
self.setIsLoading(False)
self.setHasMore(self._request_url != "")
@ -157,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)

View file

@ -0,0 +1,36 @@
# Copyright (c) 2021 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import Optional, TYPE_CHECKING
from PyQt5.QtCore import pyqtProperty, pyqtSignal, QObject
from cura.CuraApplication import CuraApplication
if TYPE_CHECKING:
from UM.PluginRegistry import PluginRegistry
from cura.CuraPackageManager import CuraPackageManager
class RestartManager(QObject):
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()
self._manager.installedPackagesChanged.connect(self.checkIfRestartNeeded)
self._plugin_registry.pluginsEnabledOrDisabledChanged.connect(self.checkIfRestartNeeded)
self._restart_needed = False
def checkIfRestartNeeded(self) -> None:
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

View file

@ -0,0 +1,91 @@
//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
import QtQuick.Window 2.2
import QtQuick.Controls 2.3
import QtQuick.Layouts 1.3
import UM 1.6 as UM
import Cura 1.6 as Cura
UM.Dialog
{
id: licenseDialog
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
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
Row
{
Layout.fillWidth: true
height: childrenRect.height
spacing: UM.Theme.getSize("default_margin").width
leftPadding: UM.Theme.getSize("narrow_margin").width
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")
source: UM.Theme.getIcon("Certificate", "high")
}
Label
{
text: catalog.i18nc("@text", "Please read and agree with the plugin licence.")
color: UM.Theme.getColor("text")
font: UM.Theme.getFont("large")
anchors.verticalCenter: icon.verticalCenter
height: UM.Theme.getSize("marketplace_large_icon").height
verticalAlignment: Qt.AlignVCenter
wrapMode: Text.Wrap
renderType: Text.NativeRendering
}
}
Cura.ScrollableTextArea
{
Layout.fillWidth: true
Layout.fillHeight: true
anchors.topMargin: UM.Theme.getSize("default_margin").height
textArea.text: licenseContent
textArea.readOnly: true
}
}
rightButtons:
[
Cura.PrimaryButton
{
text: catalog.i18nc("@button", "Accept")
onClicked: handler.onLicenseAccepted(packageId)
}
]
leftButtons:
[
Cura.SecondaryButton
{
text: catalog.i18nc("@button", "Decline")
onClicked: handler.onLicenseDeclined(packageId)
}
]
onAccepted: handler.onLicenseAccepted(packageId)
onRejected: handler.onLicenseDeclined(packageId)
onClosing: handler.onLicenseDeclined(packageId)
}

View file

@ -0,0 +1,114 @@
// 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
Item
{
id: manageButton
property bool button_style: true
property string text
property bool busy: false
property bool confirmed: false
implicitWidth: childrenRect.width
implicitHeight: childrenRect.height
signal clicked()
property Component primaryButton: Component
{
Cura.PrimaryButton
{
text: manageButton.text
onClicked: manageButton.clicked()
}
}
property Component secondaryButton: Component
{
Cura.SecondaryButton
{
text: manageButton.text
onClicked: manageButton.clicked()
}
}
property Component busyButton: Component
{
Item
{
height: UM.Theme.getSize("action_button").height
width: childrenRect.width
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.verticalCenter: parent.verticalCenter
source: UM.Theme.getIcon("Spinner")
color: UM.Theme.getColor("primary")
RotationAnimator
{
target: busyIndicator
running: parent.visible
from: 0
to: 360
loops: Animation.Infinite
duration: 2500
}
}
Label
{
visible: parent.visible
anchors.left: busyIndicator.right
anchors.leftMargin: UM.Theme.getSize("narrow_margin").width
anchors.verticalCenter: parent.verticalCenter
text: manageButton.text
font: UM.Theme.getFont("medium_bold")
color: UM.Theme.getColor("primary")
}
}
}
property Component confirmButton: Component
{
Item
{
height: UM.Theme.getSize("action_button").height
width: childrenRect.width
Label
{
anchors.verticalCenter: parent.verticalCenter
text: manageButton.text
font: UM.Theme.getFont("medium_bold")
color: UM.Theme.getColor("primary")
}
}
}
Loader
{
sourceComponent:
{
if (busy) { return manageButton.busyButton; }
else if (confirmed) { return manageButton.confirmButton; }
else if (manageButton.button_style) { return manageButton.primaryButton; }
else { return manageButton.secondaryButton; }
}
}
}

View file

@ -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
{

View file

@ -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 restartManager: Marketplace.RestartManager { }
signal searchStringChanged(string new_search)
@ -106,9 +108,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
@ -228,4 +229,56 @@ Window
}
}
}
Rectangle
{
height: quitButton.height + 2 * UM.Theme.getSize("default_margin").width
color: UM.Theme.getColor("primary")
visible: restartManager.showRestartNotification
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("@info:button, %1 is the application name", "Quit %1").arg(CuraApplication.applicationDisplayName)
onClicked:
{
marketplaceDialog.hide();
CuraApplication.closeApplication();
}
}
}
}
}

View file

@ -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
{

View file

@ -10,597 +10,85 @@ import Cura 1.6 as Cura
Rectangle
{
property var packageData
property bool expanded: false
property alias packageData: packageCardHeader.packageData
property alias manageableInListView: packageCardHeader.showManageButtons
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
PackageCardHeader
{
width: parent.width
spacing: 0
id: packageCardHeader
Item
{
width: parent.width
height: UM.Theme.getSize("card").height
id: shortDescription
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("thick_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
}
Control
{
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
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)
}
}
Item
{
id: shortDescription
Layout.preferredWidth: parent.width
Layout.fillHeight: true
Label
{
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)
{
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;
}
}
}
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: parent.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)
}
}
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
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")
}
Label
{
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
}
}
// 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.AlignTop
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.AlignTop
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)
}
Cura.SecondaryButton
{
id: disableButton
Layout.alignment: Qt.AlignTop
text: catalog.i18nc("@button", "Disable")
visible: false // not functional right now, also only when unfolding and required
}
Cura.SecondaryButton
{
id: uninstallButton
Layout.alignment: Qt.AlignTop
text: catalog.i18nc("@button", "Uninstall")
visible: false // not functional right now, also only when unfolding and required
}
Cura.PrimaryButton
{
id: installButton
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
}
}
}
}
Column
{
id: extendedDescription
width: parent.width
padding: UM.Theme.getSize("default_margin").width
topPadding: 0
spacing: UM.Theme.getSize("default_margin").height
anchors.fill: parent
Label
{
width: parent.width - parent.padding * 2
id: descriptionLabel
width: parent.width
property real lastLineWidth: 0; //Store the width of the last line, to properly position the elision.
text: catalog.i18nc("@header", "Description")
font: UM.Theme.getFont("medium_bold")
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")
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")
maximumLineCount: 2
wrapMode: Text.Wrap
textFormat: Text.RichText
elide: Text.ElideRight
visible: text !== ""
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
onLineLaidOut:
{
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
if(truncated && line.isLast)
{
width: compatiblePrinterColumn.width
text: modelData
font: UM.Theme.getFont("medium")
color: UM.Theme.getColor("text")
elide: Text.ElideRight
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;
}
}
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
Label
{
id: compatibleSupportMaterialColumn
width: parent.width - parent.padding * 2
id: tripleDotLabel
anchors.left: parent.left
anchors.leftMargin: descriptionLabel.lastLineWidth
anchors.bottom: descriptionLabel.bottom
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
}
text: "… "
font: descriptionLabel.font
color: descriptionLabel.color
visible: descriptionLabel.truncated && descriptionLabel.text !== ""
}
Column
Cura.TertiaryButton
{
width: parent.width - parent.padding * 2
id: readMoreButton
anchors.right: parent.right
anchors.bottom: descriptionLabel.bottom
height: fontMetrics.height //Height of a single line.
visible: packageData.packageType === "material"
spacing: 0
text: catalog.i18nc("@info", "Read more")
iconSource: UM.Theme.getIcon("LinkExternal")
Label
{
width: parent.width
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
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)
}
onClicked: Qt.openUrlExternally(packageData.packageInfoUrl)
}
}
}

View file

@ -0,0 +1,213 @@
// 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: false
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
}
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: showManageButtons && packageData.isInstalled && !packageData.isToBeInstalled && packageData.packageType != "material"
enabled: !packageData.busy
button_style: !packageData.isActive
Layout.alignment: Qt.AlignTop
text: button_style ? catalog.i18nc("@button", "Enable") : catalog.i18nc("@button", "Disable")
onClicked: packageData.isActive ? packageData.disable(): packageData.enable()
}
ManageButton
{
id: installManageButton
visible: showManageButtons && (packageData.canDowngrade || !packageData.isBundled)
enabled: !packageData.busy
busy: packageData.busy
button_style: !(packageData.isInstalled || packageData.isToBeInstalled)
Layout.alignment: Qt.AlignTop
text:
{
if (packageData.canDowngrade)
{
if (busy) { return catalog.i18nc("@button", "Downgrading..."); }
else { return catalog.i18nc("@button", "Downgrade"); }
}
if (!(packageData.isInstalled || packageData.isToBeInstalled))
{
if (busy) { return catalog.i18nc("@button", "Installing..."); }
else { return catalog.i18nc("@button", "Install"); }
}
else
{
return catalog.i18nc("@button", "Uninstall");
}
}
onClicked: packageData.isInstalled || packageData.isToBeInstalled ? packageData.uninstall(): packageData.install()
}
ManageButton
{
id: updateManageButton
visible: showManageButtons && packageData.canUpdate
enabled: !packageData.busy
busy: packageData.busy
Layout.alignment: Qt.AlignTop
text: busy ? catalog.i18nc("@button", "Updating..."): catalog.i18nc("@button", "Update")
onClicked: packageData.update()
}
}
}
}

View file

@ -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
}
}
}

View file

@ -0,0 +1,295 @@
// 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 alias packageData: packageCardHeader.packageData
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
PackageCardHeader
{
id: packageCardHeader
showManageButtons: true
anchors.fill: parent
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
}
}
}
}
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")
}
}

View file

@ -19,11 +19,12 @@ ListView
property string bannerText
property string bannerReadMoreUrl
property var onRemoveBanner
property bool packagesManageableInListView
clip: true
Component.onCompleted: model.updatePackages()
Component.onDestruction: model.abortUpdating()
Component.onDestruction: model.cleanUpAPIRequest()
spacing: UM.Theme.getSize("default_margin").height
@ -35,15 +36,13 @@ 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")
}
@ -82,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")
@ -230,4 +230,3 @@ ListView
}
}
}

View file

@ -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
{

View file

@ -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 )
}

View file

@ -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,

View file

@ -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({p['package_id'] for p in packages})
self.metadataChanged.emit()

View file

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M22 12C22 14.6522 20.9464 17.1957 19.0711 19.0711C17.1957 20.9464 14.6522 22 12 22C9.34784 22 6.8043 20.9464 4.92893 19.0711C3.05357 17.1957 2 14.6522 2 12H4C4 13.5823 4.46919 15.129 5.34824 16.4446C6.22729 17.7602 7.47672 18.7855 8.93853 19.391C10.4003 19.9965 12.0089 20.155 13.5607 19.8463C15.1126 19.5376 16.538 18.7757 17.6569 17.6569C18.7757 16.538 19.5376 15.1126 19.8463 13.5607C20.155 12.0089 19.9965 10.4003 19.391 8.93853C18.7855 7.47672 17.7602 6.22729 16.4446 5.34824C15.129 4.46919 13.5823 4 12 4V2C14.6508 2.00436 17.1918 3.05933 19.0662 4.93375C20.9407 6.80817 21.9956 9.34917 22 12Z"/>
</svg>

After

Width:  |  Height:  |  Size: 703 B

View file

@ -0,0 +1,3 @@
<svg width="48" height="48" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
<path d="M39.0176 13.0001H9.01764V11.0001H39.0176V13.0001ZM24.0176 16.0001H9.01764V18.0001H24.0176V16.0001ZM20.0176 21.0001H9.01764V23.0001H20.0176V21.0001ZM12.0176 26.0001H9.01764V28.0001H12.0176V26.0001ZM43.9576 5.06006V36.9401H35.6976V43.3901L31.6976 42.0501L27.6976 43.3901V36.9401H4.07764V5.06006H43.9576ZM33.6976 33.1401L31.7276 33.6801L29.6976 33.1501V40.6101L31.6976 39.9501L33.6976 40.6101V33.1401ZM36.2876 28.9501L36.9776 26.3001L36.2576 23.6601L34.3176 21.7301L31.6676 21.0401L29.0276 21.7601L27.0976 23.7001L26.4076 26.3501L27.1276 28.9901L29.0676 30.9201L31.7176 31.6101L34.3576 30.8901L36.2876 28.9501ZM42.0776 6.94006H5.95764V35.0601H27.6976V32.3701L25.3376 30.0301L24.3376 26.3601L25.3076 22.6701L27.9876 19.9701L31.6576 18.9601L35.3476 19.9301L38.0476 22.6201L39.0576 26.2901L38.0876 29.9801L35.6976 32.3801V35.0601H42.0776V6.94006Z" fill="#000E1A"/>
</svg>

After

Width:  |  Height:  |  Size: 959 B

View file

@ -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],
@ -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],
@ -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]
}
}