diff options
102 files changed, 3056 insertions, 4945 deletions
diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index ca708709aa..1aebb9a2db 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -494,7 +494,7 @@ class CuraApplication(QtApplication): "CuraEngineBackend", #Cura is useless without this one since you can't slice. "FileLogger", #You want to be able to read the log if something goes wrong. "XmlMaterialProfile", #Cura crashes without this one. - "Toolbox", #This contains the interface to enable/disable plug-ins, so if you disable it you can't enable it back. + "Marketplace", #This contains the interface to enable/disable plug-ins, so if you disable it you can't enable it back. "PrepareStage", #Cura is useless without this one since you can't load models. "PreviewStage", #This shows the list of the plugin views that are installed in Cura. "MonitorStage", #Major part of Cura's functionality. @@ -573,6 +573,10 @@ class CuraApplication(QtApplication): preferences.addPreference("general/accepted_user_agreement", False) + preferences.addPreference("cura/market_place_show_plugin_banner", True) + preferences.addPreference("cura/market_place_show_material_banner", True) + preferences.addPreference("cura/market_place_show_manage_packages_banner", True) + for key in [ "dialog_load_path", # dialog_save_path is in LocalFileOutputDevicePlugin "dialog_profile_path", diff --git a/cura/CuraPackageManager.py b/cura/CuraPackageManager.py index 26d6591099..af75aa7b66 100644 --- a/cura/CuraPackageManager.py +++ b/cura/CuraPackageManager.py @@ -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 diff --git a/plugins/DigitalLibrary/resources/qml/ProjectSummaryCard.qml b/plugins/DigitalLibrary/resources/qml/ProjectSummaryCard.qml index 4374b2f998..ba2abf22a9 100644 --- a/plugins/DigitalLibrary/resources/qml/ProjectSummaryCard.qml +++ b/plugins/DigitalLibrary/resources/qml/ProjectSummaryCard.qml @@ -44,7 +44,7 @@ Cura.RoundedRectangle { id: projectImage anchors.verticalCenter: parent.verticalCenter - width: UM.Theme.getSize("toolbox_thumbnail_small").width + width: UM.Theme.getSize("card_icon").width height: Math.round(width * 3/4) sourceSize.width: width sourceSize.height: height diff --git a/plugins/DigitalLibrary/resources/qml/SelectProjectPage.qml b/plugins/DigitalLibrary/resources/qml/SelectProjectPage.qml index 89d282ab83..eb47163d5c 100644 --- a/plugins/DigitalLibrary/resources/qml/SelectProjectPage.qml +++ b/plugins/DigitalLibrary/resources/qml/SelectProjectPage.qml @@ -1,4 +1,4 @@ -// Copyright (C) 2021 Ultimaker B.V. +// Copyright (C) 2022 Ultimaker B.V. // Cura is released under the terms of the LGPLv3 or higher. import QtQuick 2.10 @@ -9,7 +9,7 @@ import QtQuick.Controls.Styles 1.4 import QtQuick.Layouts 1.1 import UM 1.2 as UM -import Cura 1.6 as Cura +import Cura 1.7 as Cura import DigitalFactory 1.0 as DF @@ -44,33 +44,14 @@ Item height: childrenRect.height spacing: UM.Theme.getSize("default_margin").width - Cura.TextField + Cura.SearchBar { id: searchBar Layout.fillWidth: true implicitHeight: createNewProjectButton.height leftPadding: searchIcon.width + UM.Theme.getSize("default_margin").width * 2 focus: true - onTextEdited: manager.projectFilter = text //Update the search filter when editing this text field. - - placeholderText: "Search" - - UM.RecolorImage - { - id: searchIcon - - anchors - { - verticalCenter: parent.verticalCenter - left: parent.left - leftMargin: UM.Theme.getSize("default_margin").width - } - source: UM.Theme.getIcon("search") - height: UM.Theme.getSize("small_button_icon").height - width: height - color: UM.Theme.getColor("text") - } } Cura.SecondaryButton @@ -222,7 +203,7 @@ Item LoadMoreProjectsCard { id: loadMoreProjectsCard - height: UM.Theme.getSize("toolbox_thumbnail_small").height + height: UM.Theme.getSize("card_icon").height width: parent.width visible: manager.digitalFactoryProjectModel.count > 0 hasMoreProjectsToLoad: manager.hasMoreProjectsToLoad diff --git a/plugins/Marketplace/Constants.py b/plugins/Marketplace/Constants.py new file mode 100644 index 0000000000..9f0f78b966 --- /dev/null +++ b/plugins/Marketplace/Constants.py @@ -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" diff --git a/plugins/Marketplace/LocalPackageList.py b/plugins/Marketplace/LocalPackageList.py new file mode 100644 index 0000000000..a609e72d33 --- /dev/null +++ b/plugins/Marketplace/LocalPackageList.py @@ -0,0 +1,126 @@ +# Copyright (c) 2022 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + +from typing import Any, Dict, List, Optional, TYPE_CHECKING + +from PyQt5.QtCore import pyqtSlot, QObject + +from UM.Version import Version +from UM.i18n import i18nCatalog +from UM.TaskManagement.HttpRequestManager import HttpRequestManager +from UM.Logger import Logger + +from .PackageList import PackageList +from .PackageModel import PackageModel +from .Constants import PACKAGE_UPDATES_URL + +if TYPE_CHECKING: + from PyQt5.QtCore import QObject + from PyQt5.QtNetwork import QNetworkReply + +catalog = i18nCatalog("cura") + + +class LocalPackageList(PackageList): + PACKAGE_CATEGORIES = { + "installed": + { + "plugin": catalog.i18nc("@label", "Installed Plugins"), + "material": catalog.i18nc("@label", "Installed Materials") + }, + "bundled": + { + "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._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 package and 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: + """Update the list with local packages, these are materials or plugin, either bundled or user installed. The list + will also contain **to be removed** or **to be installed** packages since the user might still want to interact + with these. + """ + self.setErrorMessage("") # Clear any previous errors. + self.setIsLoading(True) + + # 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 _makePackageModel(self, package_info: Dict[str, Any]) -> PackageModel: + """ Create a PackageModel from the package_info and determine its section_title""" + + 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_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}" + + 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"] + for package in packages: + self._package_manager.addAvailablePackageVersion(package["package_id"], Version(package["package_version"])) + package_model = self.getPackageModel(package["package_id"]) + if package_model: + # Also make sure that the local list knows where to get an update + package_model.setDownloadUrl(package["download_url"]) + + self._ongoing_requests["check_updates"] = None diff --git a/plugins/Marketplace/Marketplace.py b/plugins/Marketplace/Marketplace.py new file mode 100644 index 0000000000..2d98947572 --- /dev/null +++ b/plugins/Marketplace/Marketplace.py @@ -0,0 +1,114 @@ +# Copyright (c) 2022 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + +import os.path +from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject +from typing import Optional, cast + +from cura.CuraApplication import CuraApplication # Creating QML objects and managing packages. + +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. + + +class Marketplace(Extension, QObject): + """ + The main managing object for the Marketplace plug-in. + """ + def __init__(self, parent: Optional[QObject] = None) -> None: + QObject.__init__(self, parent) + Extension.__init__(self) + self._window: Optional["QObject"] = None # If the window has been loaded yet, it'll be cached in here. + self._plugin_registry: Optional[PluginRegistry] = None + self._package_manager = CuraApplication.getInstance().getPackageManager() + + self._material_package_list: Optional[RemotePackageList] = None + self._plugin_package_list: Optional[RemotePackageList] = None + + # Not entirely the cleanest code, since the localPackage list also checks the server if there are updates + # Since that in turn will trigger notifications to be shown, we do need to construct it here and make sure + # that it checks for updates... + self._local_package_list = LocalPackageList(self) + self._local_package_list.checkForUpdates(self._package_manager.local_packages) + + self._package_manager.installedPackagesChanged.connect(self.checkIfRestartNeeded) + + self._tab_shown: int = 0 + self._restart_needed = False + + def getTabShown(self) -> int: + return self._tab_shown + + def setTabShown(self, tab_shown: int) -> None: + if tab_shown != self._tab_shown: + self._tab_shown = tab_shown + self.tabShownChanged.emit() + + tabShownChanged = pyqtSignal() + tabShown = pyqtProperty(int, fget=getTabShown, fset=setTabShown, notify=tabShownChanged) + + @pyqtProperty(QObject, constant=True) + def MaterialPackageList(self): + if self._material_package_list is None: + self._material_package_list = RemotePackageList() + self._material_package_list.packageTypeFilter = "material" + + return self._material_package_list + + @pyqtProperty(QObject, constant=True) + def PluginPackageList(self): + if self._plugin_package_list is None: + self._plugin_package_list = RemotePackageList() + self._plugin_package_list.packageTypeFilter = "plugin" + return self._plugin_package_list + + @pyqtProperty(QObject, constant=True) + def LocalPackageList(self): + return self._local_package_list + + @pyqtSlot() + def show(self) -> None: + """ + Opens the window of the Marketplace. + + If the window hadn't been loaded yet into Qt, it will be created lazily. + """ + if self._window is None: + self._plugin_registry = PluginRegistry.getInstance() + self._plugin_registry.pluginsEnabledOrDisabledChanged.connect(self.checkIfRestartNeeded) + plugin_path = PluginRegistry.getInstance().getPluginPath(self.getPluginId()) + if plugin_path is None: + plugin_path = os.path.dirname(__file__) + path = os.path.join(plugin_path, "resources", "qml", "Marketplace.qml") + self._window = CuraApplication.getInstance().createQmlComponent(path, {"manager": self}) + if self._window is None: # Still None? Failed to load the QML then. + return + if not self._window.isVisible(): + self.setTabShown(0) + self._window.show() + self._window.requestActivate() # Bring window into focus, if it was already open in the background. + + @pyqtSlot() + def setVisibleTabToMaterials(self) -> None: + """ + Set the tab shown to the remote materials one. + Not implemented in a more generic way because it needs the ability to be called with 'callExtensionMethod'. + """ + self.setTabShown(1) + + def checkIfRestartNeeded(self) -> None: + if self._package_manager.hasPackagesToRemoveOrInstall or \ + cast(PluginRegistry, self._plugin_registry).getCurrentSessionActivationChangedPlugins(): + self._restart_needed = True + else: + self._restart_needed = False + self.showRestartNotificationChanged.emit() + + showRestartNotificationChanged = pyqtSignal() + + @pyqtProperty(bool, notify=showRestartNotificationChanged) + def showRestartNotification(self) -> bool: + return self._restart_needed diff --git a/plugins/Marketplace/PackageList.py b/plugins/Marketplace/PackageList.py new file mode 100644 index 0000000000..04b602002c --- /dev/null +++ b/plugins/Marketplace/PackageList.py @@ -0,0 +1,305 @@ +# 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 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") + + +class PackageList(ListModel): + """ A List model for Packages, this class serves as parent class for more detailed implementations. + 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 + + def reset(self) -> None: + """ Resets and clears the list""" + self.clear() + + isLoadingChanged = pyqtSignal() + + 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: + """ Indicating if the the packages are loading + :return" ``True`` if the list is being obtained, otherwise ``False`` + """ + return self._is_loading + + hasMoreChanged = pyqtSignal() + + 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: + """ Indicating if there are more packages available to load. + :return: ``True`` if there are more packages to load, or ``False``. + """ + return self._has_more + + errorMessageChanged = pyqtSignal() + + def setErrorMessage(self, error_message: str) -> None: + if self._error_message != error_message: + self._error_message = error_message + self.errorMessageChanged.emit() + + @pyqtProperty(str, notify = errorMessageChanged, fset = setErrorMessage) + def errorMessage(self) -> str: + """ If an error occurred getting the list of packages, an error message will be held here. + + If no error occurred (yet), this will be an empty string. + :return: An error message, if any, or an empty string if everything went okay. + """ + return self._error_message + + @pyqtProperty(bool, constant = True) + def hasFooter(self) -> bool: + """ 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) -> Optional[PackageModel]: + index = self.find("package", package_id) + data = self.getItem(index) + if data: + return data.get("package") + return None + + 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) + if package: + self.subscribeUserToPackage(package_id, str(package.sdk_version)) + else: + Logger.log("w", f"Unable to get data on package {package_id}") + + 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) + if package: + 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) diff --git a/plugins/Marketplace/PackageModel.py b/plugins/Marketplace/PackageModel.py new file mode 100644 index 0000000000..7c2a5d9ae1 --- /dev/null +++ b/plugins/Marketplace/PackageModel.py @@ -0,0 +1,382 @@ +# Copyright (c) 2021 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + +import re +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") + + +class PackageModel(QObject): + """ + Represents a package, containing all the relevant information to be displayed about a package. + """ + + 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 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._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) + self._description = package_data.get("description", "") + self._formatted_description = self._format(self._description) + + self._download_url = package_data.get("download_url", "") + self._release_notes = package_data.get("release_notes", "") # Not used yet, propose to add to description? + + subdata = package_data.get("data", {}) + self._technical_data_sheet = self._findLink(subdata, "technical_data_sheet") + self._safety_data_sheet = self._findLink(subdata, "safety_data_sheet") + self._where_to_buy = self._findLink(subdata, "where_to_buy") + self._compatible_printers = self._getCompatiblePrinters(subdata) + self._compatible_support_materials = self._getCompatibleSupportMaterials(subdata) + self._is_compatible_material_station = self._isCompatibleMaterialStation(subdata) + self._is_compatible_air_manager = self._isCompatibleAirManager(subdata) + + author_data = package_data.get("author", {}) + self._author_name = author_data.get("display_name", catalog.i18nc("@label:property", "Unknown Author")) + self._author_info_url = author_data.get("website", "") + if not self._icon_url or self._icon_url == "": + self._icon_url = author_data.get("icon_url", "") + + 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. + + The links are not in a fixed path in the package data. We need to iterate over the available links to find them. + :param subdata: The "data" element in the package data, which should contain links. + :param link_type: The type of link to find. + :return: A URL of where the link leads, or an empty string if there is no link of that type in the package data. + """ + links = subdata.get("links", []) + for link in links: + if link.get("type", "") == link_type: + return link.get("url", "") + else: + return "" # No link with the correct type was found. + + def _format(self, text: str) -> str: + """ + Formats a user-readable block of text for display. + :return: A block of rich text with formatting embedded. + """ + # Turn all in-line hyperlinks into actual links. + url_regex = re.compile(r"(((http|https)://)[a-zA-Z0-9@:%.\-_+~#?&/=]{2,256}\.[a-z]{2,12}(/[a-zA-Z0-9@:%.\-_+~#?&/=]*)?)") + text = re.sub(url_regex, r'<a href="\1">\1</a>', text) + + # Turn newlines into <br> so that they get displayed as newlines when rendering as rich text. + text = text.replace("\n", "<br>") + + return text + + def _getCompatiblePrinters(self, subdata: Dict[str, Any]) -> List[str]: + """ + Gets the list of printers that this package provides material compatibility with. + + Any printer is listed, even if it's only for a single nozzle on a single material in the package. + :param subdata: The "data" element in the package data, which should contain this compatibility information. + :return: A list of printer names that this package provides material compatibility with. + """ + result = set() + + for material in subdata.get("materials", []): + for compatibility in material.get("compatibility", []): + printer_name = compatibility.get("machine_name") + if printer_name is None: + continue # Missing printer name information. Skip this one. + for subcompatibility in compatibility.get("compatibilities", []): + if subcompatibility.get("hardware_compatible", False): + result.add(printer_name) + break + + return list(sorted(result)) + + def _getCompatibleSupportMaterials(self, subdata: Dict[str, Any]) -> List[str]: + """ + Gets the list of support materials that the materials in this package are compatible with. + + Since the materials are individually encoded as keys in the API response, only PVA and Breakaway are currently + supported. + :param subdata: The "data" element in the package data, which should contain this compatibility information. + :return: A list of support materials that the materials in this package are compatible with. + """ + result = set() + + container_registry = CuraContainerRegistry.getInstance() + try: + pva_name = container_registry.findContainersMetadata(id = "ultimaker_pva")[0].get("name", "Ultimaker PVA") + except IndexError: + pva_name = "Ultimaker PVA" + try: + breakaway_name = container_registry.findContainersMetadata(id = "ultimaker_bam")[0].get("name", "Ultimaker Breakaway") + except IndexError: + breakaway_name = "Ultimaker Breakaway" + + for material in subdata.get("materials", []): + if material.get("pva_compatible", False): + result.add(pva_name) + if material.get("breakaway_compatible", False): + result.add(breakaway_name) + + return list(sorted(result)) + + def _isCompatibleMaterialStation(self, subdata: Dict[str, Any]) -> bool: + """ + Finds out if this package provides any material that is compatible with the material station. + :param subdata: The "data" element in the package data, which should contain this compatibility information. + :return: Whether this package provides any material that is compatible with the material station. + """ + for material in subdata.get("materials", []): + for compatibility in material.get("compatibility", []): + if compatibility.get("material_station_optimized", False): + return True + return False + + def _isCompatibleAirManager(self, subdata: Dict[str, Any]) -> bool: + """ + Finds out if this package provides any material that is compatible with the air manager. + :param subdata: The "data" element in the package data, which should contain this compatibility information. + :return: Whether this package provides any material that is compatible with the air manager. + """ + for material in subdata.get("materials", []): + for compatibility in material.get("compatibility", []): + if compatibility.get("air_manager_optimized", False): + return True + return False + + @pyqtProperty(str, constant = True) + def packageId(self) -> str: + return self._package_id + + @pyqtProperty(str, constant = True) + def packageType(self) -> str: + return self._package_type + + @pyqtProperty(str, constant = True) + def iconUrl(self) -> str: + return self._icon_url + + @pyqtProperty(str, constant = True) + def displayName(self) -> str: + return self._display_name + + @pyqtProperty(bool, constant = True) + def isCheckedByUltimaker(self): + return self._is_checked_by_ultimaker + + @pyqtProperty(str, constant = True) + def packageVersion(self) -> str: + return self._package_version + + @pyqtProperty(str, constant = True) + def packageInfoUrl(self) -> str: + return self._package_info_url + + @pyqtProperty(int, constant = True) + def downloadCount(self) -> str: + return self._download_count + + @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) -> str: + return self._author_name + + @pyqtProperty(str, constant = True) + def authorInfoUrl(self) -> str: + return self._author_info_url + + @pyqtProperty(str, constant = True) + def sectionTitle(self) -> Optional[str]: + return self._section_title + + @pyqtProperty(str, constant = True) + def technicalDataSheet(self) -> str: + return self._technical_data_sheet + + @pyqtProperty(str, constant = True) + def safetyDataSheet(self) -> str: + return self._safety_data_sheet + + @pyqtProperty(str, constant = True) + def whereToBuy(self) -> str: + return self._where_to_buy + + @pyqtProperty("QStringList", constant = True) + def compatiblePrinters(self) -> List[str]: + return self._compatible_printers + + @pyqtProperty("QStringList", constant = True) + def compatibleSupportMaterials(self) -> List[str]: + return self._compatible_support_materials + + @pyqtProperty(bool, constant = True) + def isCompatibleMaterialStation(self) -> bool: + return self._is_compatible_material_station + + @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 + + def setDownloadUrl(self, download_url): + self._download_url = 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._download_url) + + @pyqtSlot() + def update(self): + self.setBusy(True) + self.updatePackageTriggered.emit(self.packageId, self._download_url) + + @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 diff --git a/plugins/Marketplace/RemotePackageList.py b/plugins/Marketplace/RemotePackageList.py new file mode 100644 index 0000000000..16b0e721ad --- /dev/null +++ b/plugins/Marketplace/RemotePackageList.py @@ -0,0 +1,151 @@ +# 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 UM.i18n import i18nCatalog +from UM.Logger import Logger +from UM.TaskManagement.HttpRequestManager import HttpRequestManager # To request the package list from the API. + +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. + +if TYPE_CHECKING: + from PyQt5.QtCore import QObject + +catalog = i18nCatalog("cura") + + +class RemotePackageList(PackageList): + ITEMS_PER_PAGE = 20 # Pagination of number of elements to download at once. + + def __init__(self, parent: Optional["QObject"] = None) -> None: + super().__init__(parent) + + 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() + + @pyqtSlot() + 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. + """ + self.setErrorMessage("") # Clear any previous errors. + self.setIsLoading(True) + + self._ongoing_requests["get_packages"] = HttpRequestManager.getInstance().get( + self._request_url, + scope = self._scope, + callback = self._parseResponse, + error_callback = self._onError + ) + + def reset(self) -> None: + self.clear() + self._request_url = self._initialRequestUrl() + + packageTypeFilterChanged = pyqtSignal() + searchStringChanged = 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() + + def setSearchString(self, new_search: str) -> None: + self._requested_search_string = new_search + self._onLoadingChanged() + + @pyqtProperty(str, fset = setPackageTypeFilter, notify = packageTypeFilterChanged) + 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 + + @pyqtProperty(str, fset = setSearchString, notify = searchStringChanged) + def searchString(self) -> str: + """ + Get the string the user is currently searching for (as in: the list is updating) within the packages, + or an empty string if no extra search filter has to be applied. Does not override package-type filter! + :return: String the user is searching for. Empty denotes 'no search filter'. + """ + return self._current_search_string + + def _onLoadingChanged(self) -> None: + if self._requested_search_string != self._current_search_string and not self._is_loading: + self._current_search_string = self._requested_search_string + self.reset() + self.updatePackages() + self.searchStringChanged.emit() + + def _initialRequestUrl(self) -> str: + """ + Get the URL to request the first paginated page with. + :return: A URL to request. + """ + 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 != "": + request_url += f"&search={self._current_search_string}" + return request_url + + 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_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, 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 + continue + + self._request_url = response_data["links"].get("next", "") # Use empty string to signify that there is no next page. + self._ongoing_requests["get_packages"] = None + self.setIsLoading(False) + self.setHasMore(self._request_url != "") + + 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_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_requests["get_packages"] = None + self.setIsLoading(False) diff --git a/plugins/Marketplace/__init__.py b/plugins/Marketplace/__init__.py new file mode 100644 index 0000000000..bd65062ba6 --- /dev/null +++ b/plugins/Marketplace/__init__.py @@ -0,0 +1,17 @@ +# Copyright (c) 2021 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + +from .Marketplace import Marketplace + +def getMetaData(): + """ + Extension-type plug-ins don't have any specific metadata being used by Cura. + """ + return {} + + +def register(app): + """ + Register the plug-in object with Uranium. + """ + return { "extension": Marketplace() } diff --git a/plugins/Marketplace/plugin.json b/plugins/Marketplace/plugin.json new file mode 100644 index 0000000000..7eeeb5c986 --- /dev/null +++ b/plugins/Marketplace/plugin.json @@ -0,0 +1,8 @@ +{ + "name": "Marketplace", + "author": "Ultimaker B.V.", + "version": "1.0.0", + "api": 7, + "description": "Manages extensions to the application and allows browsing extensions from the Ultimaker website.", + "i18n-catalog": "cura" +} diff --git a/plugins/Toolbox/resources/images/placeholder.svg b/plugins/Marketplace/resources/images/placeholder.svg index cc674a4b38..cc674a4b38 100644 --- a/plugins/Toolbox/resources/images/placeholder.svg +++ b/plugins/Marketplace/resources/images/placeholder.svg diff --git a/plugins/Marketplace/resources/qml/LicenseDialog.qml b/plugins/Marketplace/resources/qml/LicenseDialog.qml new file mode 100644 index 0000000000..1c99569793 --- /dev/null +++ b/plugins/Marketplace/resources/qml/LicenseDialog.qml @@ -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) +} diff --git a/plugins/Marketplace/resources/qml/ManageButton.qml b/plugins/Marketplace/resources/qml/ManageButton.qml new file mode 100644 index 0000000000..36022ffd54 --- /dev/null +++ b/plugins/Marketplace/resources/qml/ManageButton.qml @@ -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; } + } + } +} diff --git a/plugins/Marketplace/resources/qml/ManagePackagesButton.qml b/plugins/Marketplace/resources/qml/ManagePackagesButton.qml new file mode 100644 index 0000000000..92e2196beb --- /dev/null +++ b/plugins/Marketplace/resources/qml/ManagePackagesButton.qml @@ -0,0 +1,49 @@ +// Copyright (c) 2021 Ultimaker B.V. +// Cura is released under the terms of the LGPLv3 or higher. + +import UM 1.2 as UM +import Cura 1.6 as Cura + +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +TabButton +{ + id: root + width: UM.Theme.getSize("button_icon").width + UM.Theme.getSize("narrow_margin").width + height: UM.Theme.getSize("button_icon").height + hoverEnabled: true + property color inactiveBackgroundColor : hovered ? UM.Theme.getColor("action_button_hovered") : UM.Theme.getColor("detail_background") + property color activeBackgroundColor : UM.Theme.getColor("main_background") + leftInset: UM.Theme.getSize("narrow_margin").width + + background: Rectangle + { + color: parent.checked ? activeBackgroundColor : inactiveBackgroundColor + border.color: parent.checked ? UM.Theme.getColor("detail_background") : "transparent" + border.width: UM.Theme.getSize("thick_lining").width + radius: Math.round(width * 0.5) + } + + Cura.ToolTip + { + id: tooltip + + tooltipText: catalog.i18nc("@info:tooltip", "Manage packages") + visible: root.hovered + } + + UM.RecolorImage + { + id: icon + + width: UM.Theme.getSize("section_icon").width + height: UM.Theme.getSize("section_icon").height + + color: UM.Theme.getColor("icon") + source: UM.Theme.getIcon("Settings") + anchors.horizontalCenter: parent.horizontalCenter + anchors.horizontalCenterOffset: Math.round(UM.Theme.getSize("narrow_margin").width /2) + anchors.verticalCenter: parent.verticalCenter + } +} diff --git a/plugins/Marketplace/resources/qml/ManagedPackages.qml b/plugins/Marketplace/resources/qml/ManagedPackages.qml new file mode 100644 index 0000000000..8ccaacea46 --- /dev/null +++ b/plugins/Marketplace/resources/qml/ManagedPackages.qml @@ -0,0 +1,25 @@ +// 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.15 + +import UM 1.4 as UM + +Packages +{ + pageTitle: catalog.i18nc("@header", "Manage packages") + + bannerVisible: UM.Preferences.getValue("cura/market_place_show_manage_packages_banner"); + bannerIcon: UM.Theme.getIcon("ArrowDoubleCircleRight") + bannerText: catalog.i18nc("@text", "Manage your Ultimaker Cura plugins and material profiles here. Make sure to keep your plugins up to date and backup your setup regularly.") + bannerReadMoreUrl: "" // TODO add when support page is ready + onRemoveBanner: function() { + UM.Preferences.setValue("cura/market_place_show_manage_packages_banner", false); + 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: manager.LocalPackageList +} diff --git a/plugins/Marketplace/resources/qml/Marketplace.qml b/plugins/Marketplace/resources/qml/Marketplace.qml new file mode 100644 index 0000000000..fc6d3cd755 --- /dev/null +++ b/plugins/Marketplace/resources/qml/Marketplace.qml @@ -0,0 +1,299 @@ +// Copyright (c) 2021 Ultimaker B.V. +// Cura is released under the terms of the LGPLv3 or higher. + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 +import QtQuick.Window 2.2 + +import UM 1.2 as UM +import Cura 1.6 as Cura + +Window +{ + id: marketplaceDialog + property variant catalog: UM.I18nCatalog { name: "cura" } + + signal searchStringChanged(string new_search) + + minimumWidth: UM.Theme.getSize("modal_window_minimum").width + minimumHeight: UM.Theme.getSize("modal_window_minimum").height + width: minimumWidth + height: minimumHeight + + onVisibleChanged: + { + while(contextStack.depth > 1) + { + contextStack.pop(); //Do NOT use the StackView.Immediate transition here, since it causes the window to stay empty. Seemingly a Qt bug: https://bugreports.qt.io/browse/QTBUG-60670? + } + } + + Connections + { + target: Cura.API.account + function onLoginStateChanged() + { + close(); + } + } + + title: "Marketplace" //Seen by Ultimaker as a brand name, so this doesn't get translated. + modality: Qt.NonModal + + // Background color + Rectangle + { + anchors.fill: parent + color: UM.Theme.getColor("main_background") + + //The Marketplace can have a page in front of everything with package details. The stack view controls its visibility. + StackView + { + id: contextStack + anchors.fill: parent + + initialItem: packageBrowse + + ColumnLayout + { + id: packageBrowse + + spacing: UM.Theme.getSize("default_margin").height + + // Page title. + Item + { + Layout.preferredWidth: parent.width + Layout.preferredHeight: childrenRect.height + UM.Theme.getSize("default_margin").height + + Label + { + id: pageTitle + anchors + { + left: parent.left + leftMargin: UM.Theme.getSize("default_margin").width + right: parent.right + rightMargin: UM.Theme.getSize("default_margin").width + bottom: parent.bottom + } + + font: UM.Theme.getFont("large") + color: UM.Theme.getColor("text") + text: content.item ? content.item.pageTitle: catalog.i18nc("@title", "Loading...") + } + } + + OnboardBanner + { + visible: content.item && content.item.bannerVisible + text: content.item && content.item.bannerText + icon: content.item && content.item.bannerIcon + onRemove: content.item && content.item.onRemoveBanner + readMoreUrl: content.item && content.item.bannerReadMoreUrl + } + + // Search & Top-Level Tabs + Item + { + Layout.preferredHeight: childrenRect.height + Layout.preferredWidth: parent.width - 2 * UM.Theme.getSize("thin_margin").width + RowLayout + { + width: parent.width + height: UM.Theme.getSize("button_icon").height + UM.Theme.getSize("default_margin").height + spacing: UM.Theme.getSize("thin_margin").width + + Item + { + Layout.preferredHeight: parent.height + Layout.preferredWidth: searchBar.visible ? UM.Theme.getSize("thin_margin").width : 0 + Layout.fillWidth: ! searchBar.visible + } + + Cura.SearchBar + { + id: searchBar + Layout.preferredHeight: UM.Theme.getSize("button_icon").height + Layout.fillWidth: true + onTextEdited: searchStringChanged(text) + } + + // Page selection. + TabBar + { + id: pageSelectionTabBar + Layout.alignment: Qt.AlignRight + height: UM.Theme.getSize("button_icon").height + spacing: 0 + background: Rectangle { color: "transparent" } + currentIndex: manager.tabShown + + onCurrentIndexChanged: + { + manager.tabShown = currentIndex + searchBar.text = ""; + searchBar.visible = currentItem.hasSearch; + content.source = currentItem.sourcePage; + } + + PackageTypeTab + { + id: pluginTabText + width: implicitWidth + text: catalog.i18nc("@button", "Plugins") + property string sourcePage: "Plugins.qml" + property bool hasSearch: true + } + PackageTypeTab + { + id: materialsTabText + width: implicitWidth + text: catalog.i18nc("@button", "Materials") + property string sourcePage: "Materials.qml" + property bool hasSearch: true + } + ManagePackagesButton + { + property string sourcePage: "ManagedPackages.qml" + property bool hasSearch: false + + Cura.NotificationIcon + { + anchors + { + horizontalCenter: parent.right + verticalCenter: parent.top + } + visible: CuraApplication.getPackageManager().packagesWithUpdate.length > 0 + + labelText: + { + const itemCount = CuraApplication.getPackageManager().packagesWithUpdate.length + return itemCount > 9 ? "9+" : itemCount + } + } + } + } + + TextMetrics + { + id: pluginTabTextMetrics + text: pluginTabText.text + font: pluginTabText.font + } + TextMetrics + { + id: materialsTabTextMetrics + text: materialsTabText.text + font: materialsTabText.font + } + } + } + + FontMetrics + { + id: fontMetrics + font: UM.Theme.getFont("default") + } + + Cura.TertiaryButton + { + text: catalog.i18nc("@info", "Search in the browser") + iconSource: UM.Theme.getIcon("LinkExternal") + visible: pageSelectionTabBar.currentItem.hasSearch + isIconOnRightSide: true + height: fontMetrics.height + textFont: fontMetrics.font + textColor: UM.Theme.getColor("text") + + onClicked: content.item && Qt.openUrlExternally(content.item.searchInBrowserUrl) + } + + // Page contents. + Rectangle + { + Layout.preferredWidth: parent.width + Layout.fillHeight: true + color: UM.Theme.getColor("detail_background") + + // Page contents. + Loader + { + id: content + anchors.fill: parent + anchors.margins: UM.Theme.getSize("default_margin").width + source: "Plugins.qml" + + Connections + { + target: content + function onLoaded() + { + pageTitle.text = content.item.pageTitle + searchStringChanged.connect(handleSearchStringChanged) + } + function handleSearchStringChanged(new_search) + { + content.item.model.searchString = new_search + } + } + } + } + } + } + } + + Rectangle + { + height: quitButton.height + 2 * UM.Theme.getSize("default_margin").width + color: UM.Theme.getColor("primary") + visible: manager.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(); + } + } + } + } +} diff --git a/plugins/Marketplace/resources/qml/Materials.qml b/plugins/Marketplace/resources/qml/Materials.qml new file mode 100644 index 0000000000..39fae7042a --- /dev/null +++ b/plugins/Marketplace/resources/qml/Materials.qml @@ -0,0 +1,22 @@ +// Copyright (c) 2021 Ultimaker B.V. +// Cura is released under the terms of the LGPLv3 or higher. + +import UM 1.4 as UM + +Packages +{ + pageTitle: catalog.i18nc("@header", "Install Materials") + + bannerVisible: UM.Preferences.getValue("cura/market_place_show_material_banner") + bannerIcon: UM.Theme.getIcon("Spool") + bannerText: catalog.i18nc("@text", "Select and install material profiles optimised for your Ultimaker 3D printers.") + bannerReadMoreUrl: "" // TODO add when support page is ready + onRemoveBanner: function() { + UM.Preferences.setValue("cura/market_place_show_material_banner", false); + 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: manager.MaterialPackageList +} diff --git a/plugins/Marketplace/resources/qml/OnboardBanner.qml b/plugins/Marketplace/resources/qml/OnboardBanner.qml new file mode 100644 index 0000000000..25e4b53241 --- /dev/null +++ b/plugins/Marketplace/resources/qml/OnboardBanner.qml @@ -0,0 +1,119 @@ +// Copyright (c) 2021 Ultimaker B.V. +// Cura is released under the terms of the LGPLv3 or higher. + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.1 + +import UM 1.6 as UM +import Cura 1.6 as Cura + +// Onboarding banner. +Rectangle +{ + property alias icon: onboardingIcon.source + property alias text: infoText.text + property var onRemove + property string readMoreUrl + + Layout.preferredHeight: childrenRect.height + 2 * UM.Theme.getSize("default_margin").height + Layout.fillWidth: true + Layout.margins: UM.Theme.getSize("default_margin").width + + color: UM.Theme.getColor("action_panel_secondary") + + // Icon + UM.RecolorImage + { + id: onboardingIcon + anchors + { + top: parent.top + left: parent.left + margins: UM.Theme.getSize("default_margin").width + } + width: UM.Theme.getSize("banner_icon_size").width + height: UM.Theme.getSize("banner_icon_size").height + } + + // Close button + UM.SimpleButton + { + id: onboardingClose + anchors + { + top: parent.top + right: parent.right + margins: UM.Theme.getSize("default_margin").width + } + width: UM.Theme.getSize("message_close").width + height: UM.Theme.getSize("message_close").height + color: UM.Theme.getColor("primary_text") + hoverColor: UM.Theme.getColor("primary_text_hover") + iconSource: UM.Theme.getIcon("Cancel") + + onClicked: onRemove() + } + + // Body + Label { + id: infoText + anchors + { + top: parent.top + left: onboardingIcon.right + right: onboardingClose.left + margins: UM.Theme.getSize("default_margin").width + } + + font: UM.Theme.getFont("default") + + renderType: Text.NativeRendering + color: UM.Theme.getColor("primary_text") + wrapMode: Text.Wrap + elide: Text.ElideRight + + onLineLaidOut: + { + if(line.isLast) + { + // Check if read more button still fits after the body text + if (line.implicitWidth + readMoreButton.width + UM.Theme.getSize("default_margin").width > width) + { + // If it does place it after the body text + readMoreButton.anchors.bottomMargin = -(fontMetrics.height); + readMoreButton.anchors.leftMargin = UM.Theme.getSize("thin_margin").width; + } + else + { + // Otherwise place it under the text + readMoreButton.anchors.leftMargin = line.implicitWidth + UM.Theme.getSize("default_margin").width; + readMoreButton.anchors.bottomMargin = 0; + } + } + } + } + + FontMetrics + { + id: fontMetrics + font: UM.Theme.getFont("default") + } + + Cura.TertiaryButton + { + id: readMoreButton + anchors.left: infoText.left + anchors.bottom: infoText.bottom + text: "Learn More" + textFont: UM.Theme.getFont("default") + textColor: infoText.color + leftPadding: 0 + rightPadding: 0 + iconSource: UM.Theme.getIcon("LinkExternal") + isIconOnRightSide: true + height: fontMetrics.height + + onClicked: Qt.openUrlExternally(readMoreUrl) + } +}
\ No newline at end of file diff --git a/plugins/Marketplace/resources/qml/PackageCard.qml b/plugins/Marketplace/resources/qml/PackageCard.qml new file mode 100644 index 0000000000..633d2b25b9 --- /dev/null +++ b/plugins/Marketplace/resources/qml/PackageCard.qml @@ -0,0 +1,101 @@ +// 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 +{ + 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 + + PackageCardHeader + { + id: packageCardHeader + + Item + { + id: shortDescription + + anchors.fill: parent + + 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: descriptionLabel.bottom + height: fontMetrics.height //Height of a single line. + + text: catalog.i18nc("@info", "Read more") + iconSource: UM.Theme.getIcon("LinkExternal") + + visible: descriptionLabel.truncated && descriptionLabel.text !== "" + enabled: visible + leftPadding: UM.Theme.getSize("default_margin").width + rightPadding: UM.Theme.getSize("wide_margin").width + textFont: descriptionLabel.font + isIconOnRightSide: true + + onClicked: Qt.openUrlExternally(packageData.packageInfoUrl) + } + } + } + + FontMetrics + { + id: fontMetrics + font: UM.Theme.getFont("default") + } +} diff --git a/plugins/Marketplace/resources/qml/PackageCardHeader.qml b/plugins/Marketplace/resources/qml/PackageCardHeader.qml new file mode 100644 index 0000000000..3a76f7a959 --- /dev/null +++ b/plugins/Marketplace/resources/qml/PackageCardHeader.qml @@ -0,0 +1,215 @@ +// 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 + Item + { + Layout.fillWidth: true + implicitHeight: authorBy.height + Layout.alignment: Qt.AlignTop + Cura.TertiaryButton + { + 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() + } + } + } +} diff --git a/plugins/Marketplace/resources/qml/PackageDetails.qml b/plugins/Marketplace/resources/qml/PackageDetails.qml new file mode 100644 index 0000000000..2599c7f28c --- /dev/null +++ b/plugins/Marketplace/resources/qml/PackageDetails.qml @@ -0,0 +1,96 @@ +// 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.3 + +import Cura 1.0 as Cura +import UM 1.0 as UM + +Item +{ + id: detailPage + property var packageData: packages.selectedPackage + property string title: catalog.i18nc("@header", "Package details") + + RowLayout + { + id: header + anchors + { + top: parent.top + topMargin: UM.Theme.getSize("default_margin").height + left: parent.left + leftMargin: UM.Theme.getSize("default_margin").width + right: parent.right + rightMargin: anchors.leftMargin + } + + spacing: UM.Theme.getSize("default_margin").width + + Cura.SecondaryButton + { + Layout.alignment: Qt.AlignVCenter + Layout.preferredHeight: UM.Theme.getSize("action_button").height + Layout.preferredWidth: height + + onClicked: contextStack.pop() //Remove this page, returning to the main package list or whichever thing is beneath it. + + tooltip: catalog.i18nc("@button:tooltip", "Back") + toolTipContentAlignment: Cura.ToolTip.ContentAlignment.AlignRight + leftPadding: UM.Theme.getSize("narrow_margin").width + rightPadding: leftPadding + iconSource: UM.Theme.getIcon("ArrowLeft") + iconSize: height - leftPadding * 2 + } + + Label + { + Layout.alignment: Qt.AlignVCenter + Layout.fillWidth: true + + text: detailPage.title + font: UM.Theme.getFont("large") + color: UM.Theme.getColor("text") + } + } + + Rectangle + { + anchors + { + top: header.bottom + topMargin: UM.Theme.getSize("default_margin").height + left: parent.left + right: parent.right + bottom: parent.bottom + } + color: UM.Theme.getColor("detail_background") + + ScrollView + { + anchors.fill: parent + + 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: packagePage.height + UM.Theme.getSize("default_margin").height * 2 + + PackagePage + { + id: packagePage + anchors + { + left: parent.left + leftMargin: UM.Theme.getSize("default_margin").width + right: parent.right + rightMargin: anchors.leftMargin + top: parent.top + topMargin: UM.Theme.getSize("default_margin").height + } + + packageData: detailPage.packageData + } + } + } +}
\ No newline at end of file diff --git a/plugins/Marketplace/resources/qml/PackagePage.qml b/plugins/Marketplace/resources/qml/PackagePage.qml new file mode 100644 index 0000000000..21c400fff2 --- /dev/null +++ b/plugins/Marketplace/resources/qml/PackagePage.qml @@ -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") + } +} diff --git a/plugins/Marketplace/resources/qml/PackageTypeTab.qml b/plugins/Marketplace/resources/qml/PackageTypeTab.qml new file mode 100644 index 0000000000..79eaa9a16c --- /dev/null +++ b/plugins/Marketplace/resources/qml/PackageTypeTab.qml @@ -0,0 +1,33 @@ +// 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 +{ + property string pageTitle + padding: UM.Theme.getSize("narrow_margin").width + horizontalPadding: UM.Theme.getSize("default_margin").width + hoverEnabled: true + property color inactiveBackgroundColor : hovered ? UM.Theme.getColor("action_button_hovered") : UM.Theme.getColor("detail_background") + property color activeBackgroundColor : UM.Theme.getColor("main_background") + + background: Rectangle + { + anchors.fill: parent + color: parent.checked ? activeBackgroundColor : inactiveBackgroundColor + 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_bold") + color: UM.Theme.getColor("text") + width: contentWidth + anchors.centerIn: parent + } +}
\ No newline at end of file diff --git a/plugins/Marketplace/resources/qml/Packages.qml b/plugins/Marketplace/resources/qml/Packages.qml new file mode 100644 index 0000000000..194c90c248 --- /dev/null +++ b/plugins/Marketplace/resources/qml/Packages.qml @@ -0,0 +1,232 @@ +// 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 + + +ListView +{ + id: packages + width: parent.width + + property string pageTitle + property var selectedPackage + property string searchInBrowserUrl + property bool bannerVisible + property var bannerIcon + property string bannerText + property string bannerReadMoreUrl + property var onRemoveBanner + property bool packagesManageableInListView + + clip: true + + Component.onCompleted: model.updatePackages() + Component.onDestruction: model.cleanUpAPIRequest() + + spacing: UM.Theme.getSize("default_margin").height + + section.property: "package.sectionTitle" + section.delegate: Rectangle + { + width: packages.width + height: sectionHeaderText.height + UM.Theme.getSize("default_margin").height + + color: UM.Theme.getColor("detail_background") + + Label + { + id: sectionHeaderText + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + + text: section + font: UM.Theme.getFont("large") + color: UM.Theme.getColor("text") + } + } + + ScrollBar.vertical: ScrollBar + { + // Vertical ScrollBar, styled similarly to the scrollBar in the settings panel + id: verticalScrollBar + visible: packages.contentHeight > packages.height + + background: Item{} + + contentItem: Rectangle + { + id: scrollViewHandle + implicitWidth: UM.Theme.getSize("scrollbar").width + radius: Math.round(implicitWidth / 2) + color: verticalScrollBar.pressed ? UM.Theme.getColor("scrollbar_handle_down") : verticalScrollBar.hovered ? UM.Theme.getColor("scrollbar_handle_hover") : UM.Theme.getColor("scrollbar_handle") + Behavior on color { ColorAnimation { duration: 50; } } + } + } + + delegate: MouseArea + { + id: cardMouseArea + width: parent ? parent.width : 0 + height: childrenRect.height + + hoverEnabled: true + onClicked: + { + packages.selectedPackage = model.package; + contextStack.push(packageDetailsComponent); + } + + 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") + } + } + + Component + { + id: packageDetailsComponent + + PackageDetails + { + packageData: packages.selectedPackage + title: packages.pageTitle + } + } + + //Wrapper item to add spacing between content and footer. + footer: Item + { + width: parent.width - UM.Theme.getSize("default_margin").width - UM.Theme.getSize("narrow_margin").width + height: model.hasFooter || packages.model.errorMessage != "" ? UM.Theme.getSize("card").height + packages.spacing : 0 + visible: model.hasFooter || packages.model.errorMessage != "" + 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.updatePackages() //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: packages.model.count > 0 ? catalog.i18nc("@message", "No more results to load") : catalog.i18nc("@message", "No results found with current filter") + 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") + } + } + } + } +} diff --git a/plugins/Marketplace/resources/qml/Plugins.qml b/plugins/Marketplace/resources/qml/Plugins.qml new file mode 100644 index 0000000000..9983a827d8 --- /dev/null +++ b/plugins/Marketplace/resources/qml/Plugins.qml @@ -0,0 +1,22 @@ +// Copyright (c) 2021 Ultimaker B.V. +// Cura is released under the terms of the LGPLv3 or higher. + +import UM 1.4 as UM + +Packages +{ + pageTitle: catalog.i18nc("@header", "Install Plugins") + + bannerVisible: UM.Preferences.getValue("cura/market_place_show_plugin_banner") + bannerIcon: UM.Theme.getIcon("Shop") + bannerText: catalog.i18nc("@text", "Streamline your workflow and customize your Ultimaker Cura experience with plugins contributed by our amazing community of users.") + bannerReadMoreUrl: "" // TODO add when support page is ready + onRemoveBanner: function() { + UM.Preferences.setValue("cura/market_place_show_plugin_banner", false) + 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: manager.PluginPackageList +} diff --git a/plugins/Marketplace/resources/qml/VerifiedIcon.qml b/plugins/Marketplace/resources/qml/VerifiedIcon.qml new file mode 100644 index 0000000000..30ef3080a0 --- /dev/null +++ b/plugins/Marketplace/resources/qml/VerifiedIcon.qml @@ -0,0 +1,45 @@ +// Copyright (c) 2021 Ultimaker B.V. +// Cura is released under the terms of the LGPLv3 or higher. + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.1 + +import UM 1.6 as UM +import Cura 1.6 as Cura +Control +{ + implicitWidth: UM.Theme.getSize("card_tiny_icon").width + implicitHeight: UM.Theme.getSize("card_tiny_icon").height + + Cura.ToolTip + { + tooltipText: + { + switch(packageData.packageType) + { + case "plugin": return catalog.i18nc("@info", "Ultimaker Verified Plug-in"); + case "material": return catalog.i18nc("@info", "Ultimaker Certified Material"); + default: return catalog.i18nc("@info", "Ultimaker Verified Package"); + } + } + visible: parent.hovered + targetPoint: Qt.point(0, Math.round(parent.y + parent.height / 4)) + } + + Rectangle + { + anchors.fill: parent + color: UM.Theme.getColor("action_button_hovered") + radius: width + UM.RecolorImage + { + anchors.fill: parent + color: UM.Theme.getColor("primary") + source: packageData.packageType == "plugin" ? UM.Theme.getIcon("CheckCircle") : UM.Theme.getIcon("Certified") + } + } + + //NOTE: Can we link to something here? (Probably a static link explaining what verified is): + // onClicked: Qt.openUrlExternally( XXXXXX ) +}
\ No newline at end of file diff --git a/plugins/PostProcessingPlugin/PostProcessingPlugin.qml b/plugins/PostProcessingPlugin/PostProcessingPlugin.qml index bbba2e7621..bd94d1fdfd 100644 --- a/plugins/PostProcessingPlugin/PostProcessingPlugin.qml +++ b/plugins/PostProcessingPlugin/PostProcessingPlugin.qml @@ -527,10 +527,8 @@ UM.Dialog visible: activeScriptsList.count > 0 anchors { - top: parent.top - right: parent.right - rightMargin: (-0.5 * width) | 0 - topMargin: (-0.5 * height) | 0 + horizontalCenter: parent.right + verticalCenter: parent.top } labelText: activeScriptsList.count diff --git a/plugins/Toolbox/__init__.py b/plugins/Toolbox/__init__.py deleted file mode 100644 index 51f1b643d0..0000000000 --- a/plugins/Toolbox/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright (c) 2018 Ultimaker B.V. -# Toolbox is released under the terms of the LGPLv3 or higher. - -from .src import Toolbox -from .src.CloudSync.SyncOrchestrator import SyncOrchestrator - - -def getMetaData(): - return {} - - -def register(app): - return { - "extension": [Toolbox.Toolbox(app), SyncOrchestrator(app)] - } diff --git a/plugins/Toolbox/plugin.json b/plugins/Toolbox/plugin.json deleted file mode 100644 index ed4a3eae97..0000000000 --- a/plugins/Toolbox/plugin.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "Toolbox", - "author": "Ultimaker B.V.", - "version": "1.0.1", - "api": 7, - "description": "Find, manage and install new Cura packages." -} diff --git a/plugins/Toolbox/resources/images/Shop.svg b/plugins/Toolbox/resources/images/Shop.svg deleted file mode 100755 index 5056a25c51..0000000000 --- a/plugins/Toolbox/resources/images/Shop.svg +++ /dev/null @@ -1,6 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> -<path d="M19,3H5C3.3,3,2,4.3,2,6v3c0,1.5,0.8,2.7,2,3.4V22h16v-9.6c1.2-0.7,2-2,2-3.4V6C22,4.3,20.7,3,19,3z - M10,5h4v4c0,1.1-0.9,2-2,2s-2-0.9-2-2V5z M4,9V5h4v4c0,1.1-0.9,2-2,2S4,10.1,4,9z M18,20h-4v-5h-4v5H6v-7c1.2,0,2.3-0.5,3-1.4 - c0.7,0.8,1.8,1.4,3,1.4s2.3-0.5,3-1.4c0.7,0.8,1.8,1.4,3,1.4V20z M20,9c0,1.1-0.9,2-2,2s-2-0.9-2-2V5h4V9z" /> -</svg> diff --git a/plugins/Toolbox/resources/images/installed_check.svg b/plugins/Toolbox/resources/images/installed_check.svg deleted file mode 100644 index 1f1302770b..0000000000 --- a/plugins/Toolbox/resources/images/installed_check.svg +++ /dev/null @@ -1,8 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Generator: Adobe Illustrator 22.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> -<svg version="1.1" id="Layer_3" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" - viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve"> -<path d="M0,512h512V0L0,512z M440.4,318.3L331.2,431.6c-1.4,1.4-2.7,2-4.8,2c-2,0-3.4-0.7-4.8-2l-53.3-57.3l-1.4-2 - c-1.4-1.4-2-3.4-2-4.8c0-1.4,0.7-3.4,2-4.8l9.6-9.6c2.7-2.7,6.8-2.7,9.6,0l0.7,0.7l37.6,40.2c1.4,1.4,3.4,1.4,4.8,0l91.4-94.9h0.7 - c2.7-2.7,6.8-2.7,9.6,0l9.5,9.6C443.1,311.5,443.1,315.6,440.4,318.3z"/> -</svg> diff --git a/plugins/Toolbox/resources/qml/Toolbox.qml b/plugins/Toolbox/resources/qml/Toolbox.qml deleted file mode 100644 index b67d175194..0000000000 --- a/plugins/Toolbox/resources/qml/Toolbox.qml +++ /dev/null @@ -1,112 +0,0 @@ -// Copyright (c) 2018 Ultimaker B.V. -// Toolbox is released under the terms of the LGPLv3 or higher. - -// Main window for the Toolbox - -import QtQuick 2.2 -import QtQuick.Dialogs 1.1 -import QtQuick.Window 2.2 -import UM 1.1 as UM - -import "./pages" -import "./dialogs" -import "./components" - -Window -{ - id: base - property var selection: null - title: catalog.i18nc("@title", "Marketplace") - modality: Qt.ApplicationModal - flags: Qt.Dialog | Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowCloseButtonHint - - width: UM.Theme.getSize("large_popup_dialog").width - height: UM.Theme.getSize("large_popup_dialog").height - minimumWidth: width - maximumWidth: minimumWidth - minimumHeight: height - maximumHeight: minimumHeight - color: UM.Theme.getColor("main_background") - UM.I18nCatalog - { - id: catalog - name: "cura" - } - Item - { - anchors.fill: parent - - WelcomePage - { - visible: toolbox.viewPage === "welcome" - } - - ToolboxHeader - { - id: header - visible: toolbox.viewPage !== "welcome" - } - - Item - { - id: mainView - width: parent.width - z: parent.z - 1 - anchors - { - top: header.bottom - bottom: footer.top - } - // TODO: This could be improved using viewFilter instead of viewCategory - ToolboxLoadingPage - { - id: viewLoading - visible: toolbox.viewCategory !== "installed" && toolbox.viewPage === "loading" - } - ToolboxErrorPage - { - id: viewErrored - visible: toolbox.viewCategory !== "installed" && toolbox.viewPage === "errored" - } - ToolboxDownloadsPage - { - id: viewDownloads - visible: toolbox.viewCategory !== "installed" && toolbox.viewPage === "overview" - } - ToolboxDetailPage - { - id: viewDetail - visible: toolbox.viewCategory !== "installed" && toolbox.viewPage === "detail" - } - ToolboxAuthorPage - { - id: viewAuthor - visible: toolbox.viewCategory !== "installed" && toolbox.viewPage === "author" - } - ToolboxInstalledPage - { - id: installedPluginList - visible: toolbox.viewCategory === "installed" - } - } - - ToolboxFooter - { - id: footer - visible: toolbox.restartRequired - height: visible ? UM.Theme.getSize("toolbox_footer").height : 0 - } - - Connections - { - target: toolbox - function onShowLicenseDialog() { licenseDialog.show() } - function onCloseLicenseDialog() { licenseDialog.close() } - } - - ToolboxLicenseDialog - { - id: licenseDialog - } - } -} diff --git a/plugins/Toolbox/resources/qml/components/ToolboxActionButtonStyle.qml b/plugins/Toolbox/resources/qml/components/ToolboxActionButtonStyle.qml deleted file mode 100644 index eff74278c9..0000000000 --- a/plugins/Toolbox/resources/qml/components/ToolboxActionButtonStyle.qml +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) 2018 Ultimaker B.V. -// Toolbox is released under the terms of the LGPLv3 or higher. - -import QtQuick 2.2 -import QtQuick.Controls 1.4 -import QtQuick.Controls.Styles 1.4 -import UM 1.1 as UM - -ButtonStyle -{ - background: Rectangle - { - implicitWidth: UM.Theme.getSize("toolbox_action_button").width - implicitHeight: UM.Theme.getSize("toolbox_action_button").height - color: "transparent" - border - { - width: UM.Theme.getSize("default_lining").width - color: UM.Theme.getColor("lining") - } - } - label: Label - { - text: control.text - color: UM.Theme.getColor("text") - verticalAlignment: Text.AlignVCenter - horizontalAlignment: Text.AlignHCenter - } -}
\ No newline at end of file diff --git a/plugins/Toolbox/resources/qml/components/ToolboxBackColumn.qml b/plugins/Toolbox/resources/qml/components/ToolboxBackColumn.qml deleted file mode 100644 index 9874a977f5..0000000000 --- a/plugins/Toolbox/resources/qml/components/ToolboxBackColumn.qml +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright (c) 2018 Ultimaker B.V. -// Toolbox is released under the terms of the LGPLv3 or higher. - -import QtQuick 2.10 -import QtQuick.Controls 1.4 -import QtQuick.Controls.Styles 1.4 -import UM 1.1 as UM - -Item -{ - id: sidebar - height: parent.height - width: UM.Theme.getSize("toolbox_back_column").width - anchors - { - top: parent.top - left: parent.left - topMargin: UM.Theme.getSize("wide_margin").height - leftMargin: UM.Theme.getSize("default_margin").width - rightMargin: UM.Theme.getSize("default_margin").width - } - Button - { - id: button - text: catalog.i18nc("@action:button", "Back") - enabled: !toolbox.isDownloading - UM.RecolorImage - { - id: backArrow - anchors - { - verticalCenter: parent.verticalCenter - left: parent.left - rightMargin: UM.Theme.getSize("default_margin").width - } - width: UM.Theme.getSize("standard_arrow").width - height: UM.Theme.getSize("standard_arrow").height - sourceSize - { - width: width - height: height - } - color: button.enabled ? (button.hovered ? UM.Theme.getColor("primary") : UM.Theme.getColor("text")) : UM.Theme.getColor("text_inactive") - source: UM.Theme.getIcon("ChevronSingleLeft") - } - width: UM.Theme.getSize("toolbox_back_button").width - height: UM.Theme.getSize("toolbox_back_button").height - onClicked: - { - toolbox.viewPage = "overview" - if (toolbox.viewCategory == "material") - { - toolbox.filterModelByProp("authors", "package_types", "material") - } - else if (toolbox.viewCategory == "plugin") - { - toolbox.filterModelByProp("packages", "type", "plugin") - } - - } - style: ButtonStyle - { - background: Rectangle - { - color: "transparent" - } - label: Label - { - id: labelStyle - text: control.text - color: control.enabled ? (control.hovered ? UM.Theme.getColor("primary") : UM.Theme.getColor("text")) : UM.Theme.getColor("text_inactive") - font: UM.Theme.getFont("medium_bold") - horizontalAlignment: Text.AlignLeft - anchors - { - left: parent.left - leftMargin: UM.Theme.getSize("default_margin").width - } - width: control.width - renderType: Text.NativeRendering - } - } - } -} diff --git a/plugins/Toolbox/resources/qml/components/ToolboxCompatibilityChart.qml b/plugins/Toolbox/resources/qml/components/ToolboxCompatibilityChart.qml deleted file mode 100644 index e1f88a473f..0000000000 --- a/plugins/Toolbox/resources/qml/components/ToolboxCompatibilityChart.qml +++ /dev/null @@ -1,209 +0,0 @@ -// Copyright (c) 2019 Ultimaker B.V. -// Toolbox is released under the terms of the LGPLv3 or higher. - -import QtQuick 2.10 -import QtQuick.Controls 1.4 - -import UM 1.5 as UM - -Item -{ - id: base - - property var packageData - property var technicalDataSheetUrl: packageData.links.technicalDataSheet - property var safetyDataSheetUrl: packageData.links.safetyDataSheet - property var printingGuidelinesUrl: packageData.links.printingGuidelines - property var materialWebsiteUrl: packageData.links.website - - height: childrenRect.height - onVisibleChanged: packageData.type === "material" && (compatibilityItem.visible || dataSheetLinks.visible) - - Column - { - id: compatibilityItem - visible: packageData.has_configs - width: parent.width - // This is a bit of a hack, but the whole QML is pretty messy right now. This needs a big overhaul. - height: visible ? heading.height + table.height: 0 - - Label - { - id: heading - width: parent.width - text: catalog.i18nc("@label", "Compatibility") - wrapMode: Text.WordWrap - color: UM.Theme.getColor("text_medium") - font: UM.Theme.getFont("medium") - renderType: Text.NativeRendering - } - - TableView - { - id: table - width: parent.width - frameVisible: false - - // Workaround for scroll issues (QTBUG-49652) - flickableItem.interactive: false - Component.onCompleted: - { - for (var i = 0; i < flickableItem.children.length; ++i) - { - flickableItem.children[i].enabled = false - } - } - selectionMode: 0 - model: packageData.supported_configs - headerDelegate: Rectangle - { - color: UM.Theme.getColor("main_background") - height: UM.Theme.getSize("toolbox_chart_row").height - Label - { - anchors.verticalCenter: parent.verticalCenter - elide: Text.ElideRight - text: styleData.value || "" - color: UM.Theme.getColor("text") - font: UM.Theme.getFont("default_bold") - renderType: Text.NativeRendering - } - Rectangle - { - anchors.bottom: parent.bottom - height: UM.Theme.getSize("default_lining").height - width: parent.width - color: "black" - } - } - rowDelegate: Item - { - height: UM.Theme.getSize("toolbox_chart_row").height - Label - { - anchors.verticalCenter: parent.verticalCenter - elide: Text.ElideRight - text: styleData.value || "" - color: UM.Theme.getColor("text_medium") - font: UM.Theme.getFont("default") - renderType: Text.NativeRendering - } - } - itemDelegate: Item - { - height: UM.Theme.getSize("toolbox_chart_row").height - Label - { - anchors.verticalCenter: parent.verticalCenter - elide: Text.ElideRight - text: styleData.value || "" - color: UM.Theme.getColor("text_medium") - font: UM.Theme.getFont("default") - renderType: Text.NativeRendering - } - } - - Component - { - id: columnTextDelegate - Label - { - anchors.fill: parent - verticalAlignment: Text.AlignVCenter - text: styleData.value || "" - elide: Text.ElideRight - color: UM.Theme.getColor("text_medium") - font: UM.Theme.getFont("default") - renderType: Text.NativeRendering - } - } - - TableViewColumn - { - role: "machine" - title: catalog.i18nc("@label:table_header", "Machine") - width: Math.floor(table.width * 0.25) - delegate: columnTextDelegate - } - TableViewColumn - { - role: "print_core" - title: "Print Core" //This term should not be translated. - width: Math.floor(table.width * 0.2) - } - TableViewColumn - { - role: "build_plate" - title: catalog.i18nc("@label:table_header", "Build Plate") - width: Math.floor(table.width * 0.225) - } - TableViewColumn - { - role: "support_material" - title: catalog.i18nc("@label:table_header", "Support") - width: Math.floor(table.width * 0.225) - } - TableViewColumn - { - role: "quality" - title: catalog.i18nc("@label:table_header", "Quality") - width: Math.floor(table.width * 0.1) - } - } - } - - Label - { - id: dataSheetLinks - anchors.top: compatibilityItem.bottom - anchors.topMargin: UM.Theme.getSize("narrow_margin").height - visible: base.technicalDataSheetUrl !== undefined || - base.safetyDataSheetUrl !== undefined || - base.printingGuidelinesUrl !== undefined || - base.materialWebsiteUrl !== undefined - - text: - { - var result = "" - if (base.technicalDataSheetUrl !== undefined) - { - var tds_name = catalog.i18nc("@action:label", "Technical Data Sheet") - result += "<a href='%1'>%2</a>".arg(base.technicalDataSheetUrl).arg(tds_name) - } - if (base.safetyDataSheetUrl !== undefined) - { - if (result.length > 0) - { - result += "<br/>" - } - var sds_name = catalog.i18nc("@action:label", "Safety Data Sheet") - result += "<a href='%1'>%2</a>".arg(base.safetyDataSheetUrl).arg(sds_name) - } - if (base.printingGuidelinesUrl !== undefined) - { - if (result.length > 0) - { - result += "<br/>" - } - var pg_name = catalog.i18nc("@action:label", "Printing Guidelines") - result += "<a href='%1'>%2</a>".arg(base.printingGuidelinesUrl).arg(pg_name) - } - if (base.materialWebsiteUrl !== undefined) - { - if (result.length > 0) - { - result += "<br/>" - } - var pg_name = catalog.i18nc("@action:label", "Website") - result += "<a href='%1'>%2</a>".arg(base.materialWebsiteUrl).arg(pg_name) - } - - return result - } - font: UM.Theme.getFont("default") - color: UM.Theme.getColor("text") - linkColor: UM.Theme.getColor("text_link") - onLinkActivated: UM.UrlUtil.openUrl(link, ["http", "https"]) - renderType: Text.NativeRendering - } -} diff --git a/plugins/Toolbox/resources/qml/components/ToolboxDetailList.qml b/plugins/Toolbox/resources/qml/components/ToolboxDetailList.qml deleted file mode 100644 index 22c6b6045f..0000000000 --- a/plugins/Toolbox/resources/qml/components/ToolboxDetailList.qml +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (c) 2019 Ultimaker B.V. -// Toolbox is released under the terms of the LGPLv3 or higher. - -import QtQuick 2.10 -import QtQuick.Controls 2.3 -import UM 1.1 as UM - -Item -{ - id: detailList - ScrollView - { - clip: true - anchors.fill: detailList - - Column - { - anchors - { - right: parent.right - topMargin: UM.Theme.getSize("wide_margin").height - bottomMargin: UM.Theme.getSize("wide_margin").height - top: parent.top - } - height: childrenRect.height + 2 * UM.Theme.getSize("wide_margin").height - spacing: UM.Theme.getSize("default_margin").height - - Repeater - { - model: toolbox.packagesModel - delegate: Loader - { - // FIXME: When using asynchronous loading, on Mac and Windows, the tile may fail to load complete, - // leaving an empty space below the title part. We turn it off for now to make it work on Mac and - // Windows. - // Can be related to this QT bug: https://bugreports.qt.io/browse/QTBUG-50992 - asynchronous: false - source: "ToolboxDetailTile.qml" - } - } - } - } -} diff --git a/plugins/Toolbox/resources/qml/components/ToolboxDetailTile.qml b/plugins/Toolbox/resources/qml/components/ToolboxDetailTile.qml deleted file mode 100644 index 5badc6b66d..0000000000 --- a/plugins/Toolbox/resources/qml/components/ToolboxDetailTile.qml +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright (c) 2019 Ultimaker B.V. -// Toolbox is released under the terms of the LGPLv3 or higher. - -import QtQuick 2.10 -import QtQuick.Controls 2.3 - -import UM 1.1 as UM - -Item -{ - id: tile - width: detailList.width - UM.Theme.getSize("wide_margin").width - height: normalData.height + 2 * UM.Theme.getSize("wide_margin").height - Column - { - id: normalData - - anchors - { - top: parent.top - left: parent.left - right: controls.left - rightMargin: UM.Theme.getSize("wide_margin").width - } - - Label - { - width: parent.width - height: UM.Theme.getSize("toolbox_property_label").height - text: model.name - wrapMode: Text.WordWrap - color: UM.Theme.getColor("text") - font: UM.Theme.getFont("medium_bold") - renderType: Text.NativeRendering - } - - Label - { - width: parent.width - text: model.description - maximumLineCount: 25 - elide: Text.ElideRight - wrapMode: Text.WordWrap - color: UM.Theme.getColor("text") - font: UM.Theme.getFont("default") - renderType: Text.NativeRendering - } - - ToolboxCompatibilityChart - { - width: parent.width - packageData: model - } - } - - ToolboxDetailTileActions - { - id: controls - anchors.right: tile.right - anchors.top: tile.top - width: childrenRect.width - height: childrenRect.height - packageData: model - } - - Rectangle - { - color: UM.Theme.getColor("lining") - width: tile.width - height: UM.Theme.getSize("default_lining").height - anchors.top: normalData.bottom - anchors.topMargin: UM.Theme.getSize("default_margin").height + UM.Theme.getSize("wide_margin").height //Normal margin for spacing after chart, wide margin between items. - } -} diff --git a/plugins/Toolbox/resources/qml/components/ToolboxDetailTileActions.qml b/plugins/Toolbox/resources/qml/components/ToolboxDetailTileActions.qml deleted file mode 100644 index d683877605..0000000000 --- a/plugins/Toolbox/resources/qml/components/ToolboxDetailTileActions.qml +++ /dev/null @@ -1,121 +0,0 @@ -// Copyright (c) 2018 Ultimaker B.V. -// Toolbox is released under the terms of the LGPLv3 or higher. - -import QtQuick 2.10 -import QtQuick.Controls 1.4 -import QtQuick.Controls.Styles 1.4 -import UM 1.5 as UM -import Cura 1.1 as Cura - -Column -{ - property bool installed: toolbox.isInstalled(model.id) - property bool canUpdate: CuraApplication.getPackageManager().packagesWithUpdate.indexOf(model.id) != -1 - property bool loginRequired: model.login_required && !Cura.API.account.isLoggedIn - property var packageData - - width: UM.Theme.getSize("toolbox_action_button").width - spacing: UM.Theme.getSize("narrow_margin").height - - Item - { - width: installButton.width - height: installButton.height - ToolboxProgressButton - { - id: installButton - active: toolbox.isDownloading && toolbox.activePackage == model - onReadyAction: - { - toolbox.activePackage = model - toolbox.startDownload(model.download_url) - } - onActiveAction: toolbox.cancelDownload() - - // Don't allow installing while another download is running - enabled: installed || (!(toolbox.isDownloading && toolbox.activePackage != model) && !loginRequired) - opacity: enabled ? 1.0 : 0.5 - visible: !updateButton.visible && !installed // Don't show when the update button is visible - } - - Cura.SecondaryButton - { - id: installedButton - visible: installed - onClicked: toolbox.viewCategory = "installed" - text: catalog.i18nc("@action:button", "Installed") - fixedWidthMode: true - width: installButton.width - height: installButton.height - } - } - - Label - { - wrapMode: Text.WordWrap - text: catalog.i18nc("@label:The string between <a href=> and </a> is the highlighted link", "<a href='%1'>Log in</a> is required to install or update") - font: UM.Theme.getFont("default") - color: UM.Theme.getColor("text") - linkColor: UM.Theme.getColor("text_link") - visible: loginRequired - width: installButton.width - renderType: Text.NativeRendering - - MouseArea - { - anchors.fill: parent - onClicked: Cura.API.account.login() - } - } - - Label - { - property var whereToBuyUrl: - { - var pg_name = "whereToBuy" - return (pg_name in packageData.links) ? packageData.links[pg_name] : undefined - } - - renderType: Text.NativeRendering - text: catalog.i18nc("@label:The string between <a href=> and </a> is the highlighted link", "<a href='%1'>Buy material spools</a>") - linkColor: UM.Theme.getColor("text_link") - visible: whereToBuyUrl != undefined - font: UM.Theme.getFont("default") - color: UM.Theme.getColor("text") - MouseArea - { - anchors.fill: parent - onClicked: UM.UrlUtil.openUrl(parent.whereToBuyUrl, ["https", "http"]) - } - } - - ToolboxProgressButton - { - id: updateButton - active: toolbox.isDownloading && toolbox.activePackage == model - readyLabel: catalog.i18nc("@action:button", "Update") - activeLabel: catalog.i18nc("@action:button", "Updating") - completeLabel: catalog.i18nc("@action:button", "Updated") - - onReadyAction: - { - toolbox.activePackage = model - toolbox.update(model.id) - } - onActiveAction: toolbox.cancelDownload() - // Don't allow installing while another download is running - enabled: !(toolbox.isDownloading && toolbox.activePackage != model) && !loginRequired - opacity: enabled ? 1.0 : 0.5 - visible: canUpdate - } - - Connections - { - target: toolbox - function onInstallChanged() { installed = toolbox.isInstalled(model.id) } - function onFilterChanged() - { - installed = toolbox.isInstalled(model.id) - } - } -} diff --git a/plugins/Toolbox/resources/qml/components/ToolboxDownloadsGrid.qml b/plugins/Toolbox/resources/qml/components/ToolboxDownloadsGrid.qml deleted file mode 100644 index 6682281a31..0000000000 --- a/plugins/Toolbox/resources/qml/components/ToolboxDownloadsGrid.qml +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright (c) 2018 Ultimaker B.V. -// Toolbox is released under the terms of the LGPLv3 or higher. - -import QtQuick 2.10 -import QtQuick.Controls 2.3 -import UM 1.1 as UM - -Column -{ - property var heading: "" - property var model - id: gridArea - height: childrenRect.height + 2 * padding - width: parent.width - spacing: UM.Theme.getSize("default_margin").height - padding: UM.Theme.getSize("wide_margin").height - Label - { - id: heading - text: gridArea.heading - width: parent.width - color: UM.Theme.getColor("text_medium") - font: UM.Theme.getFont("large") - renderType: Text.NativeRendering - } - Grid - { - id: grid - width: parent.width - 2 * parent.padding - columns: 2 - columnSpacing: UM.Theme.getSize("default_margin").height - rowSpacing: UM.Theme.getSize("default_margin").width - Repeater - { - model: gridArea.model - delegate: Loader - { - asynchronous: true - width: Math.round((grid.width - (grid.columns - 1) * grid.columnSpacing) / grid.columns) - height: UM.Theme.getSize("toolbox_thumbnail_small").height - source: "ToolboxDownloadsGridTile.qml" - } - } - } -} diff --git a/plugins/Toolbox/resources/qml/components/ToolboxDownloadsGridTile.qml b/plugins/Toolbox/resources/qml/components/ToolboxDownloadsGridTile.qml deleted file mode 100644 index c310bd7121..0000000000 --- a/plugins/Toolbox/resources/qml/components/ToolboxDownloadsGridTile.qml +++ /dev/null @@ -1,125 +0,0 @@ -// Copyright (c) 2018 Ultimaker B.V. -// Toolbox is released under the terms of the LGPLv3 or higher. - -import QtQuick 2.10 -import QtQuick.Controls 1.4 -import QtQuick.Controls.Styles 1.4 -import QtQuick.Layouts 1.3 -import UM 1.1 as UM -import Cura 1.1 as Cura - -Item -{ - id: toolboxDownloadsGridTile - property int packageCount: (toolbox.viewCategory == "material" && model.type === undefined) ? toolbox.getTotalNumberOfMaterialPackagesByAuthor(model.id) : 1 - property int installedPackages: (toolbox.viewCategory == "material" && model.type === undefined) ? toolbox.getNumberOfInstalledPackagesByAuthor(model.id) : (toolbox.isInstalled(model.id) ? 1 : 0) - height: childrenRect.height - Layout.alignment: Qt.AlignTop | Qt.AlignLeft - - MouseArea - { - anchors.fill: parent - hoverEnabled: true - onEntered: thumbnail.border.color = UM.Theme.getColor("primary") - onExited: thumbnail.border.color = UM.Theme.getColor("lining") - onClicked: - { - base.selection = model - switch(toolbox.viewCategory) - { - case "material": - - // If model has a type, it must be a package - if (model.type !== undefined) - { - toolbox.viewPage = "detail" - toolbox.filterModelByProp("packages", "id", model.id) - } - else - { - toolbox.viewPage = "author" - toolbox.setFilters("packages", { - "author_id": model.id, - "type": "material" - }) - } - break - default: - toolbox.viewPage = "detail" - toolbox.filterModelByProp("packages", "id", model.id) - break - } - } - } - - Rectangle - { - id: thumbnail - width: UM.Theme.getSize("toolbox_thumbnail_small").width - height: UM.Theme.getSize("toolbox_thumbnail_small").height - color: UM.Theme.getColor("main_background") - border.width: UM.Theme.getSize("default_lining").width - border.color: UM.Theme.getColor("lining") - - Image - { - anchors.centerIn: parent - width: UM.Theme.getSize("toolbox_thumbnail_small").width - UM.Theme.getSize("wide_margin").width - height: UM.Theme.getSize("toolbox_thumbnail_small").height - UM.Theme.getSize("wide_margin").width - sourceSize.width: width - sourceSize.height: height - fillMode: Image.PreserveAspectFit - source: model.icon_url || "../../images/placeholder.svg" - mipmap: true - } - UM.RecolorImage - { - width: (parent.width * 0.4) | 0 - height: (parent.height * 0.4) | 0 - anchors - { - bottom: parent.bottom - right: parent.right - } - sourceSize.height: height - visible: installedPackages != 0 - color: (installedPackages >= packageCount) ? UM.Theme.getColor("primary") : UM.Theme.getColor("border") - source: "../../images/installed_check.svg" - } - } - Item - { - anchors - { - left: thumbnail.right - leftMargin: Math.floor(UM.Theme.getSize("narrow_margin").width) - right: parent.right - top: parent.top - bottom: parent.bottom - } - - Label - { - id: name - text: model.name - width: parent.width - elide: Text.ElideRight - color: UM.Theme.getColor("text") - font: UM.Theme.getFont("default_bold") - } - Label - { - id: info - text: model.description - elide: Text.ElideRight - width: parent.width - wrapMode: Text.WordWrap - color: UM.Theme.getColor("text") - font: UM.Theme.getFont("default") - anchors.top: name.bottom - anchors.bottom: parent.bottom - verticalAlignment: Text.AlignVCenter - maximumLineCount: 2 - } - } -} diff --git a/plugins/Toolbox/resources/qml/components/ToolboxDownloadsShowcase.qml b/plugins/Toolbox/resources/qml/components/ToolboxDownloadsShowcase.qml deleted file mode 100644 index a42a10aa29..0000000000 --- a/plugins/Toolbox/resources/qml/components/ToolboxDownloadsShowcase.qml +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright (c) 2018 Ultimaker B.V. -// Toolbox is released under the terms of the LGPLv3 or higher. - -import QtQuick 2.10 -import QtQuick.Controls 1.4 -import QtQuick.Controls.Styles 1.4 -import UM 1.1 as UM - -Rectangle -{ - color: UM.Theme.getColor("toolbox_premium_packages_background") - height: childrenRect.height - width: parent.width - Column - { - height: childrenRect.height + 2 * padding - spacing: UM.Theme.getSize("default_margin").height - width: parent.width - padding: UM.Theme.getSize("wide_margin").height - Item - { - width: parent.width - parent.padding * 2 - height: childrenRect.height - Label - { - id: heading - text: catalog.i18nc("@label", "Premium") - width: contentWidth - height: contentHeight - color: UM.Theme.getColor("text_medium") - font: UM.Theme.getFont("large") - renderType: Text.NativeRendering - } - UM.TooltipArea - { - width: childrenRect.width - height: childrenRect.height - anchors.right: parent.right - text: catalog.i18nc("@info:tooltip", "Go to Web Marketplace") - Label - { - text: "<a href='%2'>".arg(toolbox.getWebMarketplaceUrl("materials") + "?utm_source=cura&utm_medium=software&utm_campaign=marketplace-search") + catalog.i18nc("@label", "Search materials") + "</a>" - width: contentWidth - height: contentHeight - horizontalAlignment: Text.AlignRight - font: UM.Theme.getFont("default") - renderType: Text.NativeRendering - - linkColor: UM.Theme.getColor("text_link") - onLinkActivated: Qt.openUrlExternally(link) - - visible: toolbox.viewCategory === "material" - } - } - } - Grid - { - height: childrenRect.height - spacing: UM.Theme.getSize("wide_margin").width - columns: 3 - anchors.horizontalCenter: parent.horizontalCenter - - Repeater - { - model: - { - if (toolbox.viewCategory == "plugin") - { - return toolbox.pluginsShowcaseModel - } - if (toolbox.viewCategory == "material") - { - return toolbox.materialsShowcaseModel - } - } - delegate: Loader - { - asynchronous: true - source: "ToolboxDownloadsShowcaseTile.qml" - } - } - } - } -} diff --git a/plugins/Toolbox/resources/qml/components/ToolboxDownloadsShowcaseTile.qml b/plugins/Toolbox/resources/qml/components/ToolboxDownloadsShowcaseTile.qml deleted file mode 100644 index 6695921126..0000000000 --- a/plugins/Toolbox/resources/qml/components/ToolboxDownloadsShowcaseTile.qml +++ /dev/null @@ -1,114 +0,0 @@ -// Copyright (c) 2018 Ultimaker B.V. -// Cura is released under the terms of the LGPLv3 or higher. - -import QtQuick 2.10 -import QtQuick.Controls 1.4 -import QtQuick.Controls.Styles 1.4 - -import UM 1.1 as UM - -Rectangle -{ - property int packageCount: toolbox.viewCategory == "material" ? toolbox.getTotalNumberOfMaterialPackagesByAuthor(model.id) : 1 - property int installedPackages: toolbox.viewCategory == "material" ? toolbox.getNumberOfInstalledPackagesByAuthor(model.id) : (toolbox.isInstalled(model.id) ? 1 : 0) - id: tileBase - width: UM.Theme.getSize("toolbox_thumbnail_large").width + (2 * UM.Theme.getSize("default_lining").width) - height: thumbnail.height + packageName.height + UM.Theme.getSize("default_margin").width - border.width: UM.Theme.getSize("default_lining").width - border.color: UM.Theme.getColor("lining") - color: UM.Theme.getColor("main_background") - Image - { - id: thumbnail - height: UM.Theme.getSize("toolbox_thumbnail_large").height - 4 * UM.Theme.getSize("default_margin").height - width: UM.Theme.getSize("toolbox_thumbnail_large").height - 4 * UM.Theme.getSize("default_margin").height - sourceSize.height: height - sourceSize.width: width - fillMode: Image.PreserveAspectFit - source: model.icon_url || "../../images/placeholder.svg" - mipmap: true - anchors - { - top: parent.top - topMargin: UM.Theme.getSize("default_margin").height - horizontalCenter: parent.horizontalCenter - } - } - Label - { - id: packageName - text: model.name - anchors - { - horizontalCenter: parent.horizontalCenter - top: thumbnail.bottom - } - verticalAlignment: Text.AlignVCenter - horizontalAlignment: Text.AlignHCenter - renderType: Text.NativeRendering - height: UM.Theme.getSize("toolbox_heading_label").height - width: parent.width - UM.Theme.getSize("default_margin").width - wrapMode: Text.WordWrap - elide: Text.ElideRight - font: UM.Theme.getFont("medium_bold") - color: UM.Theme.getColor("text") - } - UM.RecolorImage - { - width: (parent.width * 0.20) | 0 - height: width - anchors - { - bottom: bottomBorder.top - right: parent.right - } - visible: installedPackages != 0 - color: (installedPackages >= packageCount) ? UM.Theme.getColor("primary") : UM.Theme.getColor("border") - source: "../../images/installed_check.svg" - } - - Rectangle - { - id: bottomBorder - color: UM.Theme.getColor("primary") - anchors.bottom: parent.bottom - width: parent.width - height: UM.Theme.getSize("toolbox_header_highlight").height - } - - MouseArea - { - anchors.fill: parent - hoverEnabled: true - onEntered: tileBase.border.color = UM.Theme.getColor("primary") - onExited: tileBase.border.color = UM.Theme.getColor("lining") - onClicked: - { - base.selection = model - switch(toolbox.viewCategory) - { - case "material": - - // If model has a type, it must be a package - if (model.type !== undefined) - { - toolbox.viewPage = "detail" - toolbox.filterModelByProp("packages", "id", model.id) - } - else - { - toolbox.viewPage = "author" - toolbox.setFilters("packages", { - "author_id": model.id, - "type": "material" - }) - } - break - default: - toolbox.viewPage = "detail" - toolbox.filterModelByProp("packages", "id", model.id) - break - } - } - } -} diff --git a/plugins/Toolbox/resources/qml/components/ToolboxFooter.qml b/plugins/Toolbox/resources/qml/components/ToolboxFooter.qml deleted file mode 100644 index 9863bd9a93..0000000000 --- a/plugins/Toolbox/resources/qml/components/ToolboxFooter.qml +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright (c) 2018 Ultimaker B.V. -// Toolbox is released under the terms of the LGPLv3 or higher. - -import QtQuick 2.10 -import QtQuick.Controls 2.3 - -import UM 1.1 as UM -import Cura 1.0 as Cura - -Item -{ - id: footer - width: parent.width - anchors.bottom: parent.bottom - height: visible ? UM.Theme.getSize("toolbox_footer").height : 0 - - Label - { - text: catalog.i18nc("@info", "You will need to restart Cura before changes in packages have effect.") - color: UM.Theme.getColor("text") - height: UM.Theme.getSize("toolbox_footer_button").height - verticalAlignment: Text.AlignVCenter - wrapMode: Text.WordWrap - anchors - { - top: restartButton.top - left: parent.left - leftMargin: UM.Theme.getSize("wide_margin").width - right: restartButton.left - rightMargin: UM.Theme.getSize("default_margin").width - } - renderType: Text.NativeRendering - } - - Cura.PrimaryButton - { - id: restartButton - anchors - { - top: parent.top - topMargin: UM.Theme.getSize("default_margin").height - right: parent.right - rightMargin: UM.Theme.getSize("wide_margin").width - } - height: UM.Theme.getSize("toolbox_footer_button").height - text: catalog.i18nc("@info:button, %1 is the application name", "Quit %1").arg(CuraApplication.applicationDisplayName) - onClicked: - { - base.hide() - toolbox.restart() - } - } - - ToolboxShadow - { - visible: footer.visible - anchors.bottom: footer.top - reversed: true - } -} diff --git a/plugins/Toolbox/resources/qml/components/ToolboxHeader.qml b/plugins/Toolbox/resources/qml/components/ToolboxHeader.qml deleted file mode 100644 index 2c43110af9..0000000000 --- a/plugins/Toolbox/resources/qml/components/ToolboxHeader.qml +++ /dev/null @@ -1,112 +0,0 @@ -// Copyright (c) 2020 Ultimaker B.V. -// Toolbox is released under the terms of the LGPLv3 or higher. - -import QtQuick 2.10 -import QtQuick.Controls 1.4 - -import UM 1.4 as UM -import Cura 1.0 as Cura - -Item -{ - id: header - width: parent.width - height: UM.Theme.getSize("toolbox_header").height - Row - { - id: bar - spacing: UM.Theme.getSize("default_margin").width - height: childrenRect.height - width: childrenRect.width - anchors - { - left: parent.left - leftMargin: UM.Theme.getSize("default_margin").width - } - - ToolboxTabButton - { - id: pluginsTabButton - text: catalog.i18nc("@title:tab", "Plugins") - active: toolbox.viewCategory == "plugin" && enabled - enabled: !toolbox.isDownloading && toolbox.viewPage != "loading" && toolbox.viewPage != "errored" - onClicked: - { - toolbox.filterModelByProp("packages", "type", "plugin") - toolbox.viewCategory = "plugin" - toolbox.viewPage = "overview" - } - } - - ToolboxTabButton - { - id: materialsTabButton - text: catalog.i18nc("@title:tab", "Materials") - active: toolbox.viewCategory == "material" && enabled - enabled: !toolbox.isDownloading && toolbox.viewPage != "loading" && toolbox.viewPage != "errored" - onClicked: - { - toolbox.filterModelByProp("authors", "package_types", "material") - toolbox.viewCategory = "material" - toolbox.viewPage = "overview" - } - } - - ToolboxTabButton - { - id: installedTabButton - text: catalog.i18nc("@title:tab", "Installed") - active: toolbox.viewCategory == "installed" - enabled: !toolbox.isDownloading - onClicked: toolbox.viewCategory = "installed" - width: UM.Theme.getSize("toolbox_header_tab").width + marketplaceNotificationIcon.width - UM.Theme.getSize("default_margin").width - } - - - } - - Cura.NotificationIcon - { - id: marketplaceNotificationIcon - visible: CuraApplication.getPackageManager().packagesWithUpdate.length > 0 - anchors.right: bar.right - labelText: - { - const itemCount = CuraApplication.getPackageManager().packagesWithUpdate.length - return itemCount > 9 ? "9+" : itemCount - } - } - - - UM.TooltipArea - { - id: webMarketplaceButtonTooltipArea - width: childrenRect.width - height: parent.height - text: catalog.i18nc("@info:tooltip", "Go to Web Marketplace") - anchors - { - right: parent.right - rightMargin: UM.Theme.getSize("default_margin").width - verticalCenter: parent.verticalCenter - } - acceptedButtons: Qt.LeftButton - onClicked: Qt.openUrlExternally(toolbox.getWebMarketplaceUrl("plugins") + "?utm_source=cura&utm_medium=software&utm_campaign=marketplace-button") - UM.RecolorImage - { - id: cloudMarketplaceButton - source: "../../images/Shop.svg" - color: UM.Theme.getColor(webMarketplaceButtonTooltipArea.containsMouse ? "primary" : "text") - height: parent.height / 2 - width: height - anchors.verticalCenter: parent.verticalCenter - sourceSize.width: width - sourceSize.height: height - } - } - - ToolboxShadow - { - anchors.top: bar.bottom - } -} diff --git a/plugins/Toolbox/resources/qml/components/ToolboxInstalledTile.qml b/plugins/Toolbox/resources/qml/components/ToolboxInstalledTile.qml deleted file mode 100644 index e5c94fc996..0000000000 --- a/plugins/Toolbox/resources/qml/components/ToolboxInstalledTile.qml +++ /dev/null @@ -1,123 +0,0 @@ -// Copyright (c) 2018 Ultimaker B.V. -// Toolbox is released under the terms of the LGPLv3 or higher. - -import QtQuick 2.10 -import QtQuick.Controls 1.4 -import QtQuick.Controls.Styles 1.4 -import UM 1.1 as UM - -Item -{ - height: UM.Theme.getSize("toolbox_installed_tile").height - width: parent.width - property bool isEnabled: true - - Rectangle - { - color: UM.Theme.getColor("lining") - width: parent.width - height: Math.floor(UM.Theme.getSize("default_lining").height) - anchors.bottom: parent.top - visible: index != 0 - } - Row - { - id: tileRow - height: parent.height - width: parent.width - spacing: UM.Theme.getSize("default_margin").width - topPadding: UM.Theme.getSize("default_margin").height - - CheckBox - { - id: disableButton - anchors.verticalCenter: pluginInfo.verticalCenter - checked: isEnabled - visible: model.type == "plugin" - width: visible ? UM.Theme.getSize("checkbox").width : 0 - enabled: !toolbox.isDownloading - style: UM.Theme.styles.checkbox - onClicked: toolbox.isEnabled(model.id) ? toolbox.disable(model.id) : toolbox.enable(model.id) - } - Column - { - id: pluginInfo - topPadding: UM.Theme.getSize("narrow_margin").height - property var color: model.type === "plugin" && !isEnabled ? UM.Theme.getColor("lining") : UM.Theme.getColor("text") - width: Math.floor(tileRow.width - (authorInfo.width + pluginActions.width + 2 * tileRow.spacing + ((disableButton.visible) ? disableButton.width + tileRow.spacing : 0))) - Label - { - text: model.name - width: parent.width - maximumLineCount: 1 - elide: Text.ElideRight - wrapMode: Text.WordWrap - font: UM.Theme.getFont("large_bold") - color: pluginInfo.color - renderType: Text.NativeRendering - } - Label - { - text: model.description - font: UM.Theme.getFont("default") - maximumLineCount: 3 - elide: Text.ElideRight - width: parent.width - wrapMode: Text.WordWrap - color: pluginInfo.color - renderType: Text.NativeRendering - } - } - Column - { - id: authorInfo - width: Math.floor(UM.Theme.getSize("toolbox_action_button").width * 1.25) - - Label - { - text: - { - if (model.author_email) - { - return "<a href=\"mailto:" + model.author_email + "?Subject=Cura: " + model.name + "\">" + model.author_name + "</a>" - } - else - { - return model.author_name - } - } - font: UM.Theme.getFont("medium") - width: parent.width - height: Math.floor(UM.Theme.getSize("toolbox_property_label").height) - wrapMode: Text.WordWrap - verticalAlignment: Text.AlignVCenter - horizontalAlignment: Text.AlignLeft - onLinkActivated: Qt.openUrlExternally("mailto:" + model.author_email + "?Subject=Cura: " + model.name + " Plugin") - color: model.enabled ? UM.Theme.getColor("text") : UM.Theme.getColor("lining") - linkColor: UM.Theme.getColor("text_link") - renderType: Text.NativeRendering - } - - Label - { - text: model.version - font: UM.Theme.getFont("default") - width: parent.width - height: UM.Theme.getSize("toolbox_property_label").height - color: UM.Theme.getColor("text") - verticalAlignment: Text.AlignVCenter - horizontalAlignment: Text.AlignLeft - renderType: Text.NativeRendering - } - } - ToolboxInstalledTileActions - { - id: pluginActions - } - Connections - { - target: toolbox - function onToolboxEnabledChanged() { isEnabled = toolbox.isEnabled(model.id) } - } - } -} diff --git a/plugins/Toolbox/resources/qml/components/ToolboxInstalledTileActions.qml b/plugins/Toolbox/resources/qml/components/ToolboxInstalledTileActions.qml deleted file mode 100644 index 1726497c00..0000000000 --- a/plugins/Toolbox/resources/qml/components/ToolboxInstalledTileActions.qml +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright (c) 2018 Ultimaker B.V. -// Toolbox is released under the terms of the LGPLv3 or higher. - -import QtQuick 2.10 -import QtQuick.Controls 1.4 -import QtQuick.Controls.Styles 1.4 -import UM 1.1 as UM - -import Cura 1.1 as Cura - -Column -{ - property bool canUpdate: CuraApplication.getPackageManager().packagesWithUpdate.indexOf(model.id) != -1 - property bool canDowngrade: false - property bool loginRequired: model.login_required && !Cura.API.account.isLoggedIn - width: UM.Theme.getSize("toolbox_action_button").width - spacing: UM.Theme.getSize("narrow_margin").height - - Label - { - visible: !model.is_installed - text: catalog.i18nc("@label", "Will install upon restarting") - color: UM.Theme.getColor("lining") - font: UM.Theme.getFont("default") - wrapMode: Text.WordWrap - width: parent.width - renderType: Text.NativeRendering - } - - ToolboxProgressButton - { - id: updateButton - active: toolbox.isDownloading && toolbox.activePackage == model - readyLabel: catalog.i18nc("@action:button", "Update") - activeLabel: catalog.i18nc("@action:button", "Updating") - completeLabel: catalog.i18nc("@action:button", "Updated") - onReadyAction: - { - toolbox.activePackage = model - toolbox.update(model.id) - } - onActiveAction: toolbox.cancelDownload() - - // Don't allow installing while another download is running - enabled: !(toolbox.isDownloading && toolbox.activePackage != model) && !loginRequired - opacity: enabled ? 1.0 : 0.5 - visible: canUpdate - } - - Label - { - wrapMode: Text.WordWrap - text: catalog.i18nc("@label:The string between <a href=> and </a> is the highlighted link", "<a href='%1'>Log in</a> is required to update") - font: UM.Theme.getFont("default") - color: UM.Theme.getColor("text") - linkColor: UM.Theme.getColor("text_link") - visible: loginRequired - width: updateButton.width - renderType: Text.NativeRendering - - MouseArea - { - anchors.fill: parent - onClicked: Cura.API.account.login() - } - } - - Cura.SecondaryButton - { - id: removeButton - text: canDowngrade ? catalog.i18nc("@action:button", "Downgrade") : catalog.i18nc("@action:button", "Uninstall") - visible: !model.is_bundled && model.is_installed - enabled: !toolbox.isDownloading - - width: UM.Theme.getSize("toolbox_action_button").width - height: UM.Theme.getSize("toolbox_action_button").height - - fixedWidthMode: true - - onClicked: toolbox.checkPackageUsageAndUninstall(model.id) - Connections - { - target: toolbox - function onMetadataChanged() - { - canDowngrade = toolbox.canDowngrade(model.id) - } - } - } -} diff --git a/plugins/Toolbox/resources/qml/components/ToolboxProgressButton.qml b/plugins/Toolbox/resources/qml/components/ToolboxProgressButton.qml deleted file mode 100644 index 40d6c1af47..0000000000 --- a/plugins/Toolbox/resources/qml/components/ToolboxProgressButton.qml +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright (c) 2019 Ultimaker B.V. -// Toolbox is released under the terms of the LGPLv3 or higher. - -import QtQuick 2.10 -import QtQuick.Controls 2.3 - -import UM 1.1 as UM -import Cura 1.0 as Cura - - -Cura.PrimaryButton -{ - id: button - - property var active: false - property var complete: false - - property var readyLabel: catalog.i18nc("@action:button", "Install") - property var activeLabel: catalog.i18nc("@action:button", "Cancel") - property var completeLabel: catalog.i18nc("@action:button", "Installed") - - signal readyAction() // Action when button is ready and clicked (likely install) - signal activeAction() // Action when button is active and clicked (likely cancel) - signal completeAction() // Action when button is complete and clicked (likely go to installed) - - width: UM.Theme.getSize("toolbox_action_button").width - height: UM.Theme.getSize("toolbox_action_button").height - fixedWidthMode: true - text: - { - if (complete) - { - return completeLabel - } - else if (active) - { - return activeLabel - } - else - { - return readyLabel - } - } - onClicked: - { - if (complete) - { - completeAction() - } - else if (active) - { - activeAction() - } - else - { - readyAction() - } - } - busy: active -} diff --git a/plugins/Toolbox/resources/qml/components/ToolboxShadow.qml b/plugins/Toolbox/resources/qml/components/ToolboxShadow.qml deleted file mode 100644 index 0f2f98beb9..0000000000 --- a/plugins/Toolbox/resources/qml/components/ToolboxShadow.qml +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) 2018 Ultimaker B.V. -// Toolbox is released under the terms of the LGPLv3 or higher. - -import QtQuick 2.2 - -Rectangle -{ - property bool reversed: false - width: parent.width - height: 8 - gradient: Gradient - { - GradientStop - { - position: reversed ? 1.0 : 0.0 - color: reversed ? Qt.rgba(0,0,0,0.05) : Qt.rgba(0,0,0,0.2) - } - GradientStop - { - position: reversed ? 0.0 : 1.0 - color: Qt.rgba(0,0,0,0) - } - } -} diff --git a/plugins/Toolbox/resources/qml/components/ToolboxTabButton.qml b/plugins/Toolbox/resources/qml/components/ToolboxTabButton.qml deleted file mode 100644 index 7a7d2be48a..0000000000 --- a/plugins/Toolbox/resources/qml/components/ToolboxTabButton.qml +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright (c) 2018 Ultimaker B.V. -// Toolbox is released under the terms of the LGPLv3 or higher. - -import QtQuick 2.10 -import QtQuick.Controls 2.3 -import UM 1.1 as UM - -Button -{ - id: control - property bool active: false - - implicitWidth: UM.Theme.getSize("toolbox_header_tab").width - implicitHeight: UM.Theme.getSize("toolbox_header_tab").height - - background: Item - { - id: backgroundItem - Rectangle - { - id: highlight - - visible: control.active - color: UM.Theme.getColor("primary") - anchors.bottom: parent.bottom - width: parent.width - height: UM.Theme.getSize("toolbox_header_highlight").height - } - } - - contentItem: Label - { - id: label - text: control.text - color: UM.Theme.getColor("toolbox_header_button_text_inactive") - font: UM.Theme.getFont("medium") - - verticalAlignment: Text.AlignVCenter - horizontalAlignment: Text.AlignHCenter - - renderType: Text.NativeRendering - } - - states: - [ - State - { - name: "disabled" - when: !control.enabled - PropertyChanges - { - target: label - font: UM.Theme.getFont("default_italic") - } - }, - State - { - name: "active" - when: control.active - PropertyChanges - { - target: label - font: UM.Theme.getFont("medium_bold") - color: UM.Theme.getColor("action_button_text") - } - } - ] -}
\ No newline at end of file diff --git a/plugins/Toolbox/resources/qml/dialogs/CompatibilityDialog.qml b/plugins/Toolbox/resources/qml/dialogs/CompatibilityDialog.qml deleted file mode 100644 index b33036847b..0000000000 --- a/plugins/Toolbox/resources/qml/dialogs/CompatibilityDialog.qml +++ /dev/null @@ -1,153 +0,0 @@ -// Copyright (c) 2020 Ultimaker B.V. -// Toolbox is released under the terms of the LGPLv3 or higher. - -import QtQuick 2.10 -import QtQuick.Window 2.2 -import QtQuick.Controls 2.3 - -import UM 1.1 as UM -import Cura 1.6 as Cura - - -UM.Dialog{ - visible: true - title: catalog.i18nc("@title", "Changes from your account") - width: UM.Theme.getSize("popup_dialog").width - height: UM.Theme.getSize("popup_dialog").height - minimumWidth: width - maximumWidth: minimumWidth - minimumHeight: height - maximumHeight: minimumHeight - margin: 0 - - property string actionButtonText: subscribedPackagesModel.hasIncompatiblePackages && !subscribedPackagesModel.hasCompatiblePackages ? catalog.i18nc("@button", "Dismiss") : catalog.i18nc("@button", "Next") - - Rectangle - { - id: root - anchors.fill: parent - color: UM.Theme.getColor("main_background") - - UM.I18nCatalog - { - id: catalog - name: "cura" - } - - ScrollView - { - width: parent.width - height: parent.height - nextButton.height - nextButton.anchors.margins * 2 // We want some leftover space for the button at the bottom - clip: true - - Column - { - anchors.fill: parent - anchors.margins: UM.Theme.getSize("default_margin").width - - // Compatible packages - Label - { - font: UM.Theme.getFont("default") - text: catalog.i18nc("@label", "The following packages will be added:") - visible: subscribedPackagesModel.hasCompatiblePackages - color: UM.Theme.getColor("text") - height: contentHeight + UM.Theme.getSize("default_margin").height - } - Repeater - { - model: subscribedPackagesModel - Component - { - Item - { - width: parent.width - property int lineHeight: 60 - visible: model.is_compatible - height: visible ? (lineHeight + UM.Theme.getSize("default_margin").height) : 0 // We only show the compatible packages here - Image - { - id: packageIcon - source: model.icon_url || "../../images/placeholder.svg" - height: lineHeight - width: height - sourceSize.height: height - sourceSize.width: width - mipmap: true - fillMode: Image.PreserveAspectFit - } - Label - { - text: model.display_name - font: UM.Theme.getFont("medium_bold") - anchors.left: packageIcon.right - anchors.leftMargin: UM.Theme.getSize("default_margin").width - anchors.verticalCenter: packageIcon.verticalCenter - color: UM.Theme.getColor("text") - elide: Text.ElideRight - } - } - } - } - - // Incompatible packages - Label - { - font: UM.Theme.getFont("default") - text: catalog.i18nc("@label", "The following packages can not be installed because of an incompatible Cura version:") - visible: subscribedPackagesModel.hasIncompatiblePackages - color: UM.Theme.getColor("text") - height: contentHeight + UM.Theme.getSize("default_margin").height - } - Repeater - { - model: subscribedPackagesModel - Component - { - Item - { - width: parent.width - property int lineHeight: 60 - visible: !model.is_compatible && !model.is_dismissed - height: visible ? (lineHeight + UM.Theme.getSize("default_margin").height) : 0 // We only show the incompatible packages here - Image - { - id: packageIcon - source: model.icon_url || "../../images/placeholder.svg" - height: lineHeight - width: height - sourceSize.height: height - sourceSize.width: width - mipmap: true - fillMode: Image.PreserveAspectFit - } - Label - { - text: model.display_name - font: UM.Theme.getFont("medium_bold") - anchors.left: packageIcon.right - anchors.leftMargin: UM.Theme.getSize("default_margin").width - anchors.verticalCenter: packageIcon.verticalCenter - color: UM.Theme.getColor("text") - elide: Text.ElideRight - } - } - } - } - } - - } // End of ScrollView - - Cura.PrimaryButton - { - id: nextButton - anchors.bottom: parent.bottom - anchors.right: parent.right - anchors.margins: UM.Theme.getSize("default_margin").height - text: actionButtonText - onClicked: accept() - leftPadding: UM.Theme.getSize("dialog_primary_button_padding").width - rightPadding: UM.Theme.getSize("dialog_primary_button_padding").width - } - } -} diff --git a/plugins/Toolbox/resources/qml/dialogs/ToolboxConfirmUninstallResetDialog.qml b/plugins/Toolbox/resources/qml/dialogs/ToolboxConfirmUninstallResetDialog.qml deleted file mode 100644 index 1b5e4d1d46..0000000000 --- a/plugins/Toolbox/resources/qml/dialogs/ToolboxConfirmUninstallResetDialog.qml +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright (c) 2018 Ultimaker B.V. -// Cura is released under the terms of the LGPLv3 or higher. - -import QtQuick 2.10 -import QtQuick.Controls 1.1 -import QtQuick.Controls.Styles 1.1 -import QtQuick.Layouts 1.1 -import QtQuick.Dialogs 1.1 -import QtQuick.Window 2.1 - -import UM 1.3 as UM -import Cura 1.0 as Cura - - -UM.Dialog -{ - // This dialog asks the user to confirm he/she wants to uninstall materials/pprofiles which are currently in use - id: base - - title: catalog.i18nc("@title:window", "Confirm uninstall") + toolbox.pluginToUninstall - width: 450 * screenScaleFactor - height: 50 * screenScaleFactor + dialogText.height + buttonBar.height - - maximumWidth: 450 * screenScaleFactor - maximumHeight: 450 * screenScaleFactor - minimumWidth: 450 * screenScaleFactor - minimumHeight: 150 * screenScaleFactor - - modality: Qt.WindowModal - - Column - { - UM.I18nCatalog { id: catalog; name: "cura" } - - anchors - { - fill: parent - leftMargin: Math.round(20 * screenScaleFactor) - rightMargin: Math.round(20 * screenScaleFactor) - topMargin: Math.round(10 * screenScaleFactor) - bottomMargin: Math.round(10 * screenScaleFactor) - } - spacing: Math.round(15 * screenScaleFactor) - - Label - { - id: dialogText - text: - { - var base_text = catalog.i18nc("@text:window", "You are uninstalling materials and/or profiles that are still in use. Confirming will reset the following materials/profiles to their defaults.") - var materials_text = catalog.i18nc("@text:window", "Materials") - var qualities_text = catalog.i18nc("@text:window", "Profiles") - var machines_with_materials = toolbox.uninstallUsedMaterials - var machines_with_qualities = toolbox.uninstallUsedQualities - if (machines_with_materials != "") - { - base_text += "\n\n" + materials_text +": \n" + machines_with_materials - } - if (machines_with_qualities != "") - { - base_text += "\n\n" + qualities_text + ": \n" + machines_with_qualities - } - return base_text - } - anchors.left: parent.left - anchors.right: parent.right - font: UM.Theme.getFont("default") - wrapMode: Text.WordWrap - renderType: Text.NativeRendering - } - - // Buttons - Item { - id: buttonBar - anchors.right: parent.right - anchors.left: parent.left - height: childrenRect.height - - Button { - id: cancelButton - text: catalog.i18nc("@action:button", "Cancel") - anchors.right: confirmButton.left - anchors.rightMargin: UM.Theme.getSize("default_margin").width - isDefault: true - onClicked: toolbox.closeConfirmResetDialog() - } - - Button { - id: confirmButton - text: catalog.i18nc("@action:button", "Confirm") - anchors.right: parent.right - onClicked: toolbox.resetMaterialsQualitiesAndUninstall() - } - } - } -} diff --git a/plugins/Toolbox/resources/qml/dialogs/ToolboxLicenseDialog.qml b/plugins/Toolbox/resources/qml/dialogs/ToolboxLicenseDialog.qml deleted file mode 100644 index 9219f4ed32..0000000000 --- a/plugins/Toolbox/resources/qml/dialogs/ToolboxLicenseDialog.qml +++ /dev/null @@ -1,110 +0,0 @@ -// Copyright (c) 2018 Ultimaker B.V. -// Toolbox is released under the terms of the LGPLv3 or higher. - -import QtQuick 2.10 -import QtQuick.Dialogs 1.1 -import QtQuick.Window 2.2 -import QtQuick.Controls 2.3 -import QtQuick.Layouts 1.3 -import QtQuick.Controls.Styles 1.4 - -import UM 1.1 as UM -import Cura 1.6 as Cura - -UM.Dialog -{ - id: licenseDialog - title: licenseModel.dialogTitle - minimumWidth: UM.Theme.getSize("license_window_minimum").width - minimumHeight: UM.Theme.getSize("license_window_minimum").height - width: minimumWidth - height: minimumHeight - backgroundColor: UM.Theme.getColor("main_background") - margin: screenScaleFactor * 10 - - ColumnLayout - { - anchors.fill: parent - spacing: UM.Theme.getSize("thick_margin").height - - UM.I18nCatalog{id: catalog; name: "cura"} - - Label - { - id: licenseHeader - Layout.fillWidth: true - text: catalog.i18nc("@label", "You need to accept the license to install the package") - color: UM.Theme.getColor("text") - wrapMode: Text.Wrap - renderType: Text.NativeRendering - } - - Row { - id: packageRow - - Layout.fillWidth: true - height: childrenRect.height - spacing: UM.Theme.getSize("default_margin").width - leftPadding: UM.Theme.getSize("narrow_margin").width - - Image - { - id: icon - width: 30 * screenScaleFactor - height: width - sourceSize.width: width - sourceSize.height: height - fillMode: Image.PreserveAspectFit - source: licenseModel.iconUrl || "../../images/placeholder.svg" - mipmap: true - } - - Label - { - id: packageName - text: licenseModel.packageName - color: UM.Theme.getColor("text") - font.bold: true - anchors.verticalCenter: icon.verticalCenter - height: contentHeight - wrapMode: Text.Wrap - renderType: Text.NativeRendering - } - - - } - - Cura.ScrollableTextArea - { - - Layout.fillWidth: true - Layout.fillHeight: true - anchors.topMargin: UM.Theme.getSize("default_margin").height - - textArea.text: licenseModel.licenseText - textArea.readOnly: true - } - - } - rightButtons: - [ - Cura.PrimaryButton - { - leftPadding: UM.Theme.getSize("dialog_primary_button_padding").width - rightPadding: UM.Theme.getSize("dialog_primary_button_padding").width - - text: licenseModel.acceptButtonText - onClicked: { handler.onLicenseAccepted() } - } - ] - - leftButtons: - [ - Cura.SecondaryButton - { - id: declineButton - text: licenseModel.declineButtonText - onClicked: { handler.onLicenseDeclined() } - } - ] -} diff --git a/plugins/Toolbox/resources/qml/pages/ToolboxAuthorPage.qml b/plugins/Toolbox/resources/qml/pages/ToolboxAuthorPage.qml deleted file mode 100644 index 2fa4224388..0000000000 --- a/plugins/Toolbox/resources/qml/pages/ToolboxAuthorPage.qml +++ /dev/null @@ -1,176 +0,0 @@ -// Copyright (c) 2018 Ultimaker B.V. -// Toolbox is released under the terms of the LGPLv3 or higher. - -import QtQuick 2.10 -import QtQuick.Controls 1.4 -import QtQuick.Controls.Styles 1.4 -import UM 1.5 as UM - -import "../components" - -Item -{ - id: page - property var details: base.selection || {} - anchors.fill: parent - ToolboxBackColumn - { - id: sidebar - } - Item - { - id: header - anchors - { - left: sidebar.right - right: parent.right - rightMargin: UM.Theme.getSize("wide_margin").width - } - height: UM.Theme.getSize("toolbox_detail_header").height - Image - { - id: thumbnail - width: UM.Theme.getSize("toolbox_thumbnail_medium").width - height: UM.Theme.getSize("toolbox_thumbnail_medium").height - fillMode: Image.PreserveAspectFit - source: details && details.icon_url ? details.icon_url : "../../images/placeholder.svg" - mipmap: true - anchors - { - top: parent.top - left: parent.left - leftMargin: UM.Theme.getSize("wide_margin").width - topMargin: UM.Theme.getSize("wide_margin").height - } - } - - Label - { - id: title - anchors - { - top: thumbnail.top - left: thumbnail.right - leftMargin: UM.Theme.getSize("default_margin").width - right: parent.right - rightMargin: UM.Theme.getSize("wide_margin").width - bottomMargin: UM.Theme.getSize("default_margin").height - } - text: details && details.name ? details.name : "" - font: UM.Theme.getFont("large_bold") - color: UM.Theme.getColor("text_medium") - wrapMode: Text.WordWrap - width: parent.width - height: UM.Theme.getSize("toolbox_property_label").height - renderType: Text.NativeRendering - } - Label - { - id: description - text: details && details.description ? details.description : "" - font: UM.Theme.getFont("default") - color: UM.Theme.getColor("text_medium") - anchors - { - top: title.bottom - left: title.left - topMargin: UM.Theme.getSize("default_margin").height - } - renderType: Text.NativeRendering - } - Column - { - id: properties - anchors - { - top: description.bottom - left: description.left - topMargin: UM.Theme.getSize("default_margin").height - } - spacing: Math.floor(UM.Theme.getSize("narrow_margin").height) - width: childrenRect.width - - Label - { - text: catalog.i18nc("@label", "Website") + ":" - font: UM.Theme.getFont("default") - color: UM.Theme.getColor("text_medium") - renderType: Text.NativeRendering - } - Label - { - text: catalog.i18nc("@label", "Email") + ":" - font: UM.Theme.getFont("default") - color: UM.Theme.getColor("text_medium") - renderType: Text.NativeRendering - } - } - Column - { - id: values - anchors - { - top: description.bottom - left: properties.right - leftMargin: UM.Theme.getSize("default_margin").width - right: parent.right - rightMargin: UM.Theme.getSize("default_margin").width - topMargin: UM.Theme.getSize("default_margin").height - } - spacing: Math.floor(UM.Theme.getSize("narrow_margin").height) - - Label - { - text: - { - if (details && details.website) - { - return "<a href=\"" + details.website + "\">" + details.website + "</a>" - } - return "" - } - width: parent.width - elide: Text.ElideRight - font: UM.Theme.getFont("default") - color: UM.Theme.getColor("text") - linkColor: UM.Theme.getColor("text_link") - onLinkActivated: UM.UrlUtil.openUrl(link, ["https", "http"]) - renderType: Text.NativeRendering - } - - Label - { - text: - { - if (details && details.email) - { - return "<a href=\"mailto:" + details.email + "\">" + details.email + "</a>" - } - return "" - } - font: UM.Theme.getFont("default") - color: UM.Theme.getColor("text") - linkColor: UM.Theme.getColor("text_link") - onLinkActivated: Qt.openUrlExternally(link) - renderType: Text.NativeRendering - } - } - Rectangle - { - color: UM.Theme.getColor("lining") - width: parent.width - height: UM.Theme.getSize("default_lining").height - anchors.bottom: parent.bottom - } - } - ToolboxDetailList - { - anchors - { - top: header.bottom - bottom: page.bottom - left: header.left - right: page.right - } - } -} diff --git a/plugins/Toolbox/resources/qml/pages/ToolboxDetailPage.qml b/plugins/Toolbox/resources/qml/pages/ToolboxDetailPage.qml deleted file mode 100644 index 645b77a8c9..0000000000 --- a/plugins/Toolbox/resources/qml/pages/ToolboxDetailPage.qml +++ /dev/null @@ -1,188 +0,0 @@ -// Copyright (c) 2018 Ultimaker B.V. -// Toolbox is released under the terms of the LGPLv3 or higher. - -import QtQuick 2.10 -import QtQuick.Controls 1.4 -import QtQuick.Controls.Styles 1.4 -import UM 1.5 as UM - -import Cura 1.1 as Cura - -import "../components" - -Item -{ - id: page - property var details: base.selection || {} - anchors.fill: parent - ToolboxBackColumn - { - id: sidebar - } - Item - { - id: header - anchors - { - left: sidebar.right - right: parent.right - rightMargin: UM.Theme.getSize("wide_margin").width - } - height: childrenRect.height + 3 * UM.Theme.getSize("default_margin").width - Rectangle - { - id: thumbnail - width: UM.Theme.getSize("toolbox_thumbnail_medium").width - height: UM.Theme.getSize("toolbox_thumbnail_medium").height - anchors - { - top: parent.top - left: parent.left - leftMargin: UM.Theme.getSize("wide_margin").width - topMargin: UM.Theme.getSize("wide_margin").height - } - color: UM.Theme.getColor("main_background") - Image - { - anchors.fill: parent - fillMode: Image.PreserveAspectFit - source: details === null ? "" : (details.icon_url || "../../images/placeholder.svg") - mipmap: true - height: UM.Theme.getSize("toolbox_thumbnail_large").height - 4 * UM.Theme.getSize("default_margin").height - width: UM.Theme.getSize("toolbox_thumbnail_large").height - 4 * UM.Theme.getSize("default_margin").height - sourceSize.height: height - sourceSize.width: width - } - } - - Label - { - id: title - anchors - { - top: thumbnail.top - left: thumbnail.right - leftMargin: UM.Theme.getSize("default_margin").width - } - text: details === null ? "" : (details.name || "") - font: UM.Theme.getFont("large_bold") - color: UM.Theme.getColor("text") - width: contentWidth - height: contentHeight - renderType: Text.NativeRendering - } - - Column - { - id: properties - anchors - { - top: title.bottom - left: title.left - topMargin: UM.Theme.getSize("default_margin").height - } - spacing: Math.floor(UM.Theme.getSize("narrow_margin").height) - width: childrenRect.width - height: childrenRect.height - Label - { - text: catalog.i18nc("@label", "Version") + ":" - font: UM.Theme.getFont("default") - color: UM.Theme.getColor("text_medium") - renderType: Text.NativeRendering - } - Label - { - text: catalog.i18nc("@label", "Last updated") + ":" - font: UM.Theme.getFont("default") - color: UM.Theme.getColor("text_medium") - renderType: Text.NativeRendering - } - Label - { - text: catalog.i18nc("@label", "Brand") + ":" - font: UM.Theme.getFont("default") - color: UM.Theme.getColor("text_medium") - renderType: Text.NativeRendering - } - Label - { - text: catalog.i18nc("@label", "Downloads") + ":" - font: UM.Theme.getFont("default") - color: UM.Theme.getColor("text_medium") - renderType: Text.NativeRendering - } - } - Column - { - id: values - anchors - { - top: title.bottom - left: properties.right - leftMargin: UM.Theme.getSize("default_margin").width - topMargin: UM.Theme.getSize("default_margin").height - } - spacing: Math.floor(UM.Theme.getSize("narrow_margin").height) - height: childrenRect.height - Label - { - text: details === null ? "" : (details.version || catalog.i18nc("@label", "Unknown")) - font: UM.Theme.getFont("default") - color: UM.Theme.getColor("text") - renderType: Text.NativeRendering - } - Label - { - text: - { - if (details === null) - { - return "" - } - var date = new Date(details.last_updated) - return date.toLocaleString(UM.Preferences.getValue("general/language")) - } - font: UM.Theme.getFont("default") - color: UM.Theme.getColor("text") - renderType: Text.NativeRendering - } - Label - { - text: - { - if (details === null) - { - return "" - } - else - { - return "<a href=\"" + details.website + "\">" + details.author_name + "</a>" - } - } - font: UM.Theme.getFont("default") - color: UM.Theme.getColor("text") - linkColor: UM.Theme.getColor("text_link") - onLinkActivated: UM.UrlUtil.openUrl(link, ["http", "https"]) - renderType: Text.NativeRendering - } - Label - { - text: details === null ? "" : (details.download_count || catalog.i18nc("@label", "Unknown")) - font: UM.Theme.getFont("default") - color: UM.Theme.getColor("text") - renderType: Text.NativeRendering - } - } - } - ToolboxDetailList - { - anchors - { - top: header.bottom - bottom: page.bottom - left: header.left - right: page.right - } - } -} diff --git a/plugins/Toolbox/resources/qml/pages/ToolboxDownloadsPage.qml b/plugins/Toolbox/resources/qml/pages/ToolboxDownloadsPage.qml deleted file mode 100644 index 9be8cbe2b9..0000000000 --- a/plugins/Toolbox/resources/qml/pages/ToolboxDownloadsPage.qml +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (c) 2019 Ultimaker B.V. -// Toolbox is released under the terms of the LGPLv3 or higher. - -import QtQuick 2.10 -import QtQuick.Controls 2.3 -import UM 1.1 as UM - -import "../components" - -ScrollView -{ - clip: true - width: parent.width - height: parent.height - contentHeight: mainColumn.height - - Column - { - id: mainColumn - width: base.width - spacing: UM.Theme.getSize("default_margin").height - - ToolboxDownloadsShowcase - { - id: showcase - width: parent.width - } - - ToolboxDownloadsGrid - { - id: allPlugins - width: parent.width - heading: toolbox.viewCategory === "material" ? catalog.i18nc("@label", "Community Contributions") : catalog.i18nc("@label", "Community Plugins") - model: toolbox.viewCategory === "material" ? toolbox.materialsAvailableModel : toolbox.pluginsAvailableModel - } - - ToolboxDownloadsGrid - { - id: genericMaterials - visible: toolbox.viewCategory === "material" - width: parent.width - heading: catalog.i18nc("@label", "Generic Materials") - model: toolbox.materialsGenericModel - } - } -} diff --git a/plugins/Toolbox/resources/qml/pages/ToolboxErrorPage.qml b/plugins/Toolbox/resources/qml/pages/ToolboxErrorPage.qml deleted file mode 100644 index e57e63dbb9..0000000000 --- a/plugins/Toolbox/resources/qml/pages/ToolboxErrorPage.qml +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) 2018 Ultimaker B.V. -// Toolbox is released under the terms of the LGPLv3 or higher. - -import QtQuick 2.10 -import QtQuick.Controls 1.4 -import QtQuick.Controls.Styles 1.4 - -Rectangle -{ - id: page - width: parent.width - height: parent.height - color: "transparent" - Label - { - text: catalog.i18nc("@info", "Could not connect to the Cura Package database. Please check your connection.") - anchors - { - centerIn: parent - } - renderType: Text.NativeRendering - } -} diff --git a/plugins/Toolbox/resources/qml/pages/ToolboxInstalledPage.qml b/plugins/Toolbox/resources/qml/pages/ToolboxInstalledPage.qml deleted file mode 100644 index fa7bd24c9d..0000000000 --- a/plugins/Toolbox/resources/qml/pages/ToolboxInstalledPage.qml +++ /dev/null @@ -1,223 +0,0 @@ -// Copyright (c) 2019 Ultimaker B.V. -// Toolbox is released under the terms of the LGPLv3 or higher. - -import QtQuick 2.10 -import QtQuick.Controls 2.3 - -import UM 1.1 as UM - -import "../components" - -ScrollView -{ - id: page - clip: true - width: parent.width - height: parent.height - - Column - { - width: page.width - spacing: UM.Theme.getSize("default_margin").height - padding: UM.Theme.getSize("wide_margin").width - height: childrenRect.height + 2 * UM.Theme.getSize("wide_margin").height - - Label - { - anchors - { - left: parent.left - right: parent.right - margins: parent.padding - } - text: catalog.i18nc("@title:tab", "Installed plugins") - color: UM.Theme.getColor("text_medium") - font: UM.Theme.getFont("medium") - renderType: Text.NativeRendering - } - - Rectangle - { - anchors - { - left: parent.left - right: parent.right - margins: parent.padding - } - id: installedPlugins - color: "transparent" - height: childrenRect.height + UM.Theme.getSize("default_margin").width - border.color: UM.Theme.getColor("lining") - border.width: UM.Theme.getSize("default_lining").width - Column - { - anchors - { - top: parent.top - right: parent.right - left: parent.left - margins: UM.Theme.getSize("default_margin").width - } - Repeater - { - id: pluginList - model: toolbox.pluginsInstalledModel - delegate: ToolboxInstalledTile { } - } - } - Label - { - visible: toolbox.pluginsInstalledModel.count < 1 - padding: UM.Theme.getSize("default_margin").width - text: catalog.i18nc("@info", "No plugin has been installed.") - font: UM.Theme.getFont("medium") - color: UM.Theme.getColor("lining") - renderType: Text.NativeRendering - } - } - - Label - { - anchors - { - left: parent.left - right: parent.right - margins: parent.padding - } - text: catalog.i18nc("@title:tab", "Installed materials") - color: UM.Theme.getColor("text_medium") - font: UM.Theme.getFont("medium") - renderType: Text.NativeRendering - } - - Rectangle - { - anchors - { - left: parent.left - right: parent.right - margins: parent.padding - } - id: installedMaterials - color: "transparent" - height: childrenRect.height + UM.Theme.getSize("default_margin").width - border.color: UM.Theme.getColor("lining") - border.width: UM.Theme.getSize("default_lining").width - Column - { - anchors - { - top: parent.top - right: parent.right - left: parent.left - margins: UM.Theme.getSize("default_margin").width - } - Repeater - { - id: installedMaterialsList - model: toolbox.materialsInstalledModel - delegate: ToolboxInstalledTile { } - } - } - Label - { - visible: toolbox.materialsInstalledModel.count < 1 - padding: UM.Theme.getSize("default_margin").width - text: catalog.i18nc("@info", "No material has been installed.") - color: UM.Theme.getColor("lining") - font: UM.Theme.getFont("medium") - renderType: Text.NativeRendering - } - } - - Label - { - anchors - { - left: parent.left - right: parent.right - margins: parent.padding - } - text: catalog.i18nc("@title:tab", "Bundled plugins") - color: UM.Theme.getColor("text_medium") - font: UM.Theme.getFont("medium") - renderType: Text.NativeRendering - } - - Rectangle - { - anchors - { - left: parent.left - right: parent.right - margins: parent.padding - } - id: bundledPlugins - color: "transparent" - height: childrenRect.height + UM.Theme.getSize("default_margin").width - border.color: UM.Theme.getColor("lining") - border.width: UM.Theme.getSize("default_lining").width - Column - { - anchors - { - top: parent.top - right: parent.right - left: parent.left - margins: UM.Theme.getSize("default_margin").width - } - Repeater - { - id: bundledPluginsList - model: toolbox.pluginsBundledModel - delegate: ToolboxInstalledTile { } - } - } - } - - Label - { - anchors - { - left: parent.left - right: parent.right - margins: parent.padding - } - text: catalog.i18nc("@title:tab", "Bundled materials") - color: UM.Theme.getColor("text_medium") - font: UM.Theme.getFont("medium") - renderType: Text.NativeRendering - } - - Rectangle - { - anchors - { - left: parent.left - right: parent.right - margins: parent.padding - } - id: bundledMaterials - color: "transparent" - height: childrenRect.height + UM.Theme.getSize("default_margin").width - border.color: UM.Theme.getColor("lining") - border.width: UM.Theme.getSize("default_lining").width - Column - { - anchors - { - top: parent.top - right: parent.right - left: parent.left - margins: UM.Theme.getSize("default_margin").width - } - Repeater - { - id: bundledMaterialsList - model: toolbox.materialsBundledModel - delegate: ToolboxInstalledTile {} - } - } - } - } -} diff --git a/plugins/Toolbox/resources/qml/pages/ToolboxLoadingPage.qml b/plugins/Toolbox/resources/qml/pages/ToolboxLoadingPage.qml deleted file mode 100644 index a30af6b335..0000000000 --- a/plugins/Toolbox/resources/qml/pages/ToolboxLoadingPage.qml +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) 2018 Ultimaker B.V. -// Toolbox is released under the terms of the LGPLv3 or higher. - -import QtQuick 2.10 -import QtQuick.Controls 1.4 -import QtQuick.Controls.Styles 1.4 -import UM 1.3 as UM - -Rectangle -{ - id: page - width: parent.width - height: parent.height - color: "transparent" - Label - { - text: catalog.i18nc("@info", "Fetching packages...") - color: UM.Theme.getColor("text") - anchors - { - centerIn: parent - } - renderType: Text.NativeRendering - } -} diff --git a/plugins/Toolbox/resources/qml/pages/WelcomePage.qml b/plugins/Toolbox/resources/qml/pages/WelcomePage.qml deleted file mode 100644 index 04110cbc0f..0000000000 --- a/plugins/Toolbox/resources/qml/pages/WelcomePage.qml +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright (c) 2018 Ultimaker B.V. -// Cura is released under the terms of the LGPLv3 or higher. - -import QtQuick 2.7 -import QtQuick.Controls 2.1 -import QtQuick.Window 2.2 - -import UM 1.3 as UM -import Cura 1.1 as Cura - -Column -{ - id: welcomePage - spacing: UM.Theme.getSize("wide_margin").height - width: parent.width - height: childrenRect.height - anchors.centerIn: parent - - Label - { - id: welcomeTextLabel - text: catalog.i18nc("@description", "Please sign in to get verified plugins and materials for Ultimaker Cura Enterprise") - width: Math.round(parent.width / 2) - font: UM.Theme.getFont("default") - color: UM.Theme.getColor("text") - verticalAlignment: Text.AlignVCenter - horizontalAlignment: Text.AlignHCenter - anchors.horizontalCenter: parent.horizontalCenter - wrapMode: Label.WordWrap - renderType: Text.NativeRendering - } - - Cura.PrimaryButton - { - id: loginButton - width: UM.Theme.getSize("account_button").width - height: UM.Theme.getSize("account_button").height - anchors.horizontalCenter: parent.horizontalCenter - text: catalog.i18nc("@button", "Sign in") - onClicked: Cura.API.account.login() - fixedWidthMode: true - } -} - diff --git a/plugins/Toolbox/src/AuthorsModel.py b/plugins/Toolbox/src/AuthorsModel.py deleted file mode 100644 index 04c8ed3a40..0000000000 --- a/plugins/Toolbox/src/AuthorsModel.py +++ /dev/null @@ -1,104 +0,0 @@ -# Copyright (c) 2018 Ultimaker B.V. -# Cura is released under the terms of the LGPLv3 or higher. - -import re -from typing import Dict, List, Optional, Union, cast - -from PyQt5.QtCore import Qt, pyqtProperty - -from UM.Qt.ListModel import ListModel - - -class AuthorsModel(ListModel): - """Model that holds cura packages. - - By setting the filter property the instances held by this model can be changed. - """ - - def __init__(self, parent = None) -> None: - super().__init__(parent) - - self._metadata = None # type: Optional[List[Dict[str, Union[str, List[str], int]]]] - - self.addRoleName(Qt.UserRole + 1, "id") - self.addRoleName(Qt.UserRole + 2, "name") - self.addRoleName(Qt.UserRole + 3, "email") - self.addRoleName(Qt.UserRole + 4, "website") - self.addRoleName(Qt.UserRole + 5, "package_count") - self.addRoleName(Qt.UserRole + 6, "package_types") - self.addRoleName(Qt.UserRole + 7, "icon_url") - self.addRoleName(Qt.UserRole + 8, "description") - - # List of filters for queries. The result is the union of the each list of results. - self._filter = {} # type: Dict[str, str] - - def setMetadata(self, data: List[Dict[str, Union[str, List[str], int]]]): - if self._metadata != data: - self._metadata = data - self._update() - - def _update(self) -> None: - items = [] # type: List[Dict[str, Union[str, List[str], int, None]]] - if not self._metadata: - self.setItems(items) - return - - for author in self._metadata: - items.append({ - "id": author.get("author_id"), - "name": author.get("display_name"), - "email": author.get("email"), - "website": author.get("website"), - "package_count": author.get("package_count", 0), - "package_types": author.get("package_types", []), - "icon_url": author.get("icon_url"), - "description": "Material and quality profiles from {author_name}".format(author_name = author.get("display_name", "")) - }) - - # Filter on all the key-word arguments. - for key, value in self._filter.items(): - if key == "package_types": - key_filter = lambda item, value = value: value in item["package_types"] # type: ignore - elif "*" in value: - key_filter = lambda item, key = key, value = value: self._matchRegExp(item, key, value) # type: ignore - else: - key_filter = lambda item, key = key, value = value: self._matchString(item, key, value) # type: ignore - items = filter(key_filter, items) # type: ignore - - # Execute all filters. - filtered_items = list(items) - - filtered_items.sort(key = lambda k: cast(str, k["name"])) - self.setItems(filtered_items) - - def setFilter(self, filter_dict: Dict[str, str]) -> None: - """Set the filter of this model based on a string. - - :param filter_dict: Dictionary to do the filtering by. - """ - if filter_dict != self._filter: - self._filter = filter_dict - self._update() - - @pyqtProperty("QVariantMap", fset = setFilter, constant = True) - def filter(self) -> Dict[str, str]: - return self._filter - - # Check to see if a container matches with a regular expression - def _matchRegExp(self, metadata, property_name, value): - if property_name not in metadata: - return False - value = re.escape(value) #Escape for regex patterns. - value = "^" + value.replace("\\*", ".*") + "$" #Instead of (now escaped) asterisks, match on any string. Also add anchors for a complete match. - if self._ignore_case: - value_pattern = re.compile(value, re.IGNORECASE) - else: - value_pattern = re.compile(value) - - return value_pattern.match(str(metadata[property_name])) - - # Check to see if a container matches with a string - def _matchString(self, metadata, property_name, value): - if property_name not in metadata: - return False - return value.lower() == str(metadata[property_name]).lower() diff --git a/plugins/Toolbox/src/CloudApiModel.py b/plugins/Toolbox/src/CloudApiModel.py deleted file mode 100644 index bef37d8173..0000000000 --- a/plugins/Toolbox/src/CloudApiModel.py +++ /dev/null @@ -1,29 +0,0 @@ -from typing import Union - -from cura import ApplicationMetadata -from cura.UltimakerCloud import UltimakerCloudConstants - - -class CloudApiModel: - sdk_version = ApplicationMetadata.CuraSDKVersion # type: Union[str, int] - cloud_api_version = UltimakerCloudConstants.CuraCloudAPIVersion # type: str - cloud_api_root = UltimakerCloudConstants.CuraCloudAPIRoot # type: str - api_url = "{cloud_api_root}/cura-packages/v{cloud_api_version}/cura/v{sdk_version}".format( - cloud_api_root = cloud_api_root, - cloud_api_version = cloud_api_version, - sdk_version = sdk_version - ) # type: str - - # https://api.ultimaker.com/cura-packages/v1/user/packages - api_url_user_packages = "{cloud_api_root}/cura-packages/v{cloud_api_version}/user/packages".format( - cloud_api_root=cloud_api_root, - cloud_api_version=cloud_api_version, - ) - - @classmethod - def userPackageUrl(cls, package_id: str) -> str: - """https://api.ultimaker.com/cura-packages/v1/user/packages/{package_id}""" - - return (CloudApiModel.api_url_user_packages + "/{package_id}").format( - package_id=package_id - ) diff --git a/plugins/Toolbox/src/CloudSync/CloudApiClient.py b/plugins/Toolbox/src/CloudSync/CloudApiClient.py deleted file mode 100644 index 21eb1bdbd2..0000000000 --- a/plugins/Toolbox/src/CloudSync/CloudApiClient.py +++ /dev/null @@ -1,52 +0,0 @@ -from UM.Logger import Logger -from UM.TaskManagement.HttpRequestManager import HttpRequestManager -from UM.TaskManagement.HttpRequestScope import JsonDecoratorScope -from cura.CuraApplication import CuraApplication -from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope -from ..CloudApiModel import CloudApiModel - - -class CloudApiClient: - """Manages Cloud subscriptions - - When a package is added to a user's account, the user is 'subscribed' to that package. - Whenever the user logs in on another instance of Cura, these subscriptions can be used to sync the user's plugins - - Singleton: use CloudApiClient.getInstance() instead of CloudApiClient() - """ - - __instance = None - - @classmethod - def getInstance(cls, app: CuraApplication): - if not cls.__instance: - cls.__instance = CloudApiClient(app) - return cls.__instance - - def __init__(self, app: CuraApplication) -> None: - if self.__instance is not None: - raise RuntimeError("This is a Singleton. use getInstance()") - - self._scope = JsonDecoratorScope(UltimakerCloudScope(app)) # type: JsonDecoratorScope - - app.getPackageManager().packageInstalled.connect(self._onPackageInstalled) - - def unsubscribe(self, package_id: str) -> None: - url = CloudApiModel.userPackageUrl(package_id) - HttpRequestManager.getInstance().delete(url = url, scope = self._scope) - - 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) - data = "{\"data\": {\"package_id\": \"%s\", \"sdk_version\": \"%s\"}}" % (package_id, CloudApiModel.sdk_version) - HttpRequestManager.getInstance().put( - url = CloudApiModel.api_url_user_packages, - data = data.encode(), - scope = self._scope - ) - - def _onPackageInstalled(self, package_id: str): - if CuraApplication.getInstance().getCuraAPI().account.isLoggedIn: - # We might already be subscribed, but checking would take one extra request. Instead, simply subscribe - self._subscribe(package_id) diff --git a/plugins/Toolbox/src/CloudSync/CloudPackageChecker.py b/plugins/Toolbox/src/CloudSync/CloudPackageChecker.py deleted file mode 100644 index 6d2ed1dcbd..0000000000 --- a/plugins/Toolbox/src/CloudSync/CloudPackageChecker.py +++ /dev/null @@ -1,164 +0,0 @@ -# Copyright (c) 2020 Ultimaker B.V. -# Cura is released under the terms of the LGPLv3 or higher. - -import json -from typing import List, Dict, Any, Set -from typing import Optional - -from PyQt5.QtCore import QObject -from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest - -from UM import i18nCatalog -from UM.Logger import Logger -from UM.Message import Message -from UM.Signal import Signal -from UM.TaskManagement.HttpRequestScope import JsonDecoratorScope -from cura.API.Account import SyncState -from cura.CuraApplication import CuraApplication, ApplicationMetadata -from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope -from .SubscribedPackagesModel import SubscribedPackagesModel -from ..CloudApiModel import CloudApiModel - - -class CloudPackageChecker(QObject): - - SYNC_SERVICE_NAME = "CloudPackageChecker" - - def __init__(self, application: CuraApplication) -> None: - super().__init__() - - self.discrepancies = Signal() # Emits SubscribedPackagesModel - self._application = application # type: CuraApplication - self._scope = JsonDecoratorScope(UltimakerCloudScope(application)) - self._model = SubscribedPackagesModel() - self._message = None # type: Optional[Message] - - self._application.initializationFinished.connect(self._onAppInitialized) - self._i18n_catalog = i18nCatalog("cura") - self._sdk_version = ApplicationMetadata.CuraSDKVersion - self._last_notified_packages = set() # type: Set[str] - """Packages for which a notification has been shown. No need to bother the user twice for equal content""" - - # This is a plugin, so most of the components required are not ready when - # this is initialized. Therefore, we wait until the application is ready. - def _onAppInitialized(self) -> None: - self._package_manager = self._application.getPackageManager() - # initial check - self._getPackagesIfLoggedIn() - - self._application.getCuraAPI().account.loginStateChanged.connect(self._onLoginStateChanged) - self._application.getCuraAPI().account.syncRequested.connect(self._getPackagesIfLoggedIn) - - def _onLoginStateChanged(self) -> None: - # reset session - self._last_notified_packages = set() - self._getPackagesIfLoggedIn() - - def _getPackagesIfLoggedIn(self) -> None: - if self._application.getCuraAPI().account.isLoggedIn: - self._getUserSubscribedPackages() - else: - self._hideSyncMessage() - - def _getUserSubscribedPackages(self) -> None: - self._application.getCuraAPI().account.setSyncState(self.SYNC_SERVICE_NAME, SyncState.SYNCING) - url = CloudApiModel.api_url_user_packages - self._application.getHttpRequestManager().get(url, - callback = self._onUserPackagesRequestFinished, - error_callback = self._onUserPackagesRequestFinished, - timeout = 10, - scope = self._scope) - - def _onUserPackagesRequestFinished(self, reply: "QNetworkReply", error: Optional["QNetworkReply.NetworkError"] = None) -> None: - if error is not None or reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) != 200: - Logger.log("w", - "Requesting user packages failed, response code %s while trying to connect to %s", - reply.attribute(QNetworkRequest.HttpStatusCodeAttribute), reply.url()) - self._application.getCuraAPI().account.setSyncState(self.SYNC_SERVICE_NAME, SyncState.ERROR) - return - - try: - json_data = json.loads(bytes(reply.readAll()).decode("utf-8")) - # Check for errors: - if "errors" in json_data: - for error in json_data["errors"]: - Logger.log("e", "%s", error["title"]) - self._application.getCuraAPI().account.setSyncState(self.SYNC_SERVICE_NAME, SyncState.ERROR) - return - self._handleCompatibilityData(json_data["data"]) - except json.decoder.JSONDecodeError: - Logger.log("w", "Received invalid JSON for user subscribed packages from the Web Marketplace") - - self._application.getCuraAPI().account.setSyncState(self.SYNC_SERVICE_NAME, SyncState.SUCCESS) - - def _handleCompatibilityData(self, subscribed_packages_payload: List[Dict[str, Any]]) -> None: - user_subscribed_packages = {plugin["package_id"] for plugin in subscribed_packages_payload} - user_installed_packages = self._package_manager.getAllInstalledPackageIDs() - - # We need to re-evaluate the dismissed packages - # (i.e. some package might got updated to the correct SDK version in the meantime, - # hence remove them from the Dismissed Incompatible list) - self._package_manager.reEvaluateDismissedPackages(subscribed_packages_payload, self._sdk_version) - user_dismissed_packages = self._package_manager.getDismissedPackages() - if user_dismissed_packages: - user_installed_packages.update(user_dismissed_packages) - - # We check if there are packages installed in Web Marketplace but not in Cura marketplace - package_discrepancy = list(user_subscribed_packages.difference(user_installed_packages)) - - if user_subscribed_packages != self._last_notified_packages: - # scenario: - # 1. user subscribes to a package - # 2. dismisses the license/unsubscribes - # 3. subscribes to the same package again - # in this scenario we want to notify the user again. To capture that there was a change during - # step 2, we clear the last_notified after step 2. This way, the user will be notified after - # step 3 even though the list of packages for step 1 and 3 are equal - self._last_notified_packages = set() - - if package_discrepancy: - account = self._application.getCuraAPI().account - account.setUpdatePackagesAction(lambda: self._onSyncButtonClicked(None, None)) - - if user_subscribed_packages == self._last_notified_packages: - # already notified user about these - return - - Logger.log("d", "Discrepancy found between Cloud subscribed packages and Cura installed packages") - self._model.addDiscrepancies(package_discrepancy) - self._model.initialize(self._package_manager, subscribed_packages_payload) - self._showSyncMessage() - self._last_notified_packages = user_subscribed_packages - - def _showSyncMessage(self) -> None: - """Show the message if it is not already shown""" - - if self._message is not None: - self._message.show() - return - - sync_message = Message(self._i18n_catalog.i18nc( - "@info:generic", - "Do you want to sync material and software packages with your account?"), - title = self._i18n_catalog.i18nc("@info:title", "Changes detected from your Ultimaker account", )) - sync_message.addAction("sync", - name = self._i18n_catalog.i18nc("@action:button", "Sync"), - icon = "", - description = "Sync your plugins and print profiles to Ultimaker Cura.", - button_align = Message.ActionButtonAlignment.ALIGN_RIGHT) - sync_message.actionTriggered.connect(self._onSyncButtonClicked) - sync_message.show() - self._message = sync_message - - def _hideSyncMessage(self) -> None: - """Hide the message if it is showing""" - - if self._message is not None: - self._message.hide() - self._message = None - - def _onSyncButtonClicked(self, sync_message: Optional[Message], sync_message_action: Optional[str]) -> None: - if sync_message is not None: - sync_message.hide() - self._hideSyncMessage() # Should be the same message, but also sets _message to None - self.discrepancies.emit(self._model) diff --git a/plugins/Toolbox/src/CloudSync/DiscrepanciesPresenter.py b/plugins/Toolbox/src/CloudSync/DiscrepanciesPresenter.py deleted file mode 100644 index cee2f6318a..0000000000 --- a/plugins/Toolbox/src/CloudSync/DiscrepanciesPresenter.py +++ /dev/null @@ -1,41 +0,0 @@ -import os -from typing import Optional - -from PyQt5.QtCore import QObject, pyqtSlot - -from UM.Qt.QtApplication import QtApplication -from UM.Signal import Signal -from .SubscribedPackagesModel import SubscribedPackagesModel - - -class DiscrepanciesPresenter(QObject): - """Shows a list of packages to be added or removed. The user can select which packages to (un)install. The user's - - choices are emitted on the `packageMutations` Signal. - """ - - def __init__(self, app: QtApplication) -> None: - super().__init__(app) - - self.packageMutations = Signal() # Emits SubscribedPackagesModel - - self._app = app - self._package_manager = app.getPackageManager() - self._dialog = None # type: Optional[QObject] - self._compatibility_dialog_path = "resources/qml/dialogs/CompatibilityDialog.qml" - - def present(self, plugin_path: str, model: SubscribedPackagesModel) -> None: - path = os.path.join(plugin_path, self._compatibility_dialog_path) - self._dialog = self._app.createQmlComponent(path, {"subscribedPackagesModel": model, "handler": self}) - assert self._dialog - self._dialog.accepted.connect(lambda: self._onConfirmClicked(model)) - - def _onConfirmClicked(self, model: SubscribedPackagesModel) -> None: - # If there are incompatible packages - automatically dismiss them - if model.getIncompatiblePackages(): - self._package_manager.dismissAllIncompatiblePackages(model.getIncompatiblePackages()) - # For now, all compatible packages presented to the user should be installed. - # Later, we might remove items for which the user unselected the package - if model.getCompatiblePackages(): - model.setItems(model.getCompatiblePackages()) - self.packageMutations.emit(model) diff --git a/plugins/Toolbox/src/CloudSync/DownloadPresenter.py b/plugins/Toolbox/src/CloudSync/DownloadPresenter.py deleted file mode 100644 index 8a5e763f3c..0000000000 --- a/plugins/Toolbox/src/CloudSync/DownloadPresenter.py +++ /dev/null @@ -1,153 +0,0 @@ -# Copyright (c) 2020 Ultimaker B.V. -# Cura is released under the terms of the LGPLv3 or higher. - -import tempfile -from typing import Dict, List, Any - -from PyQt5.QtNetwork import QNetworkReply - -from UM.i18n import i18nCatalog -from UM.Logger import Logger -from UM.Message import Message -from UM.Signal import Signal -from UM.TaskManagement.HttpRequestManager import HttpRequestManager -from cura.CuraApplication import CuraApplication -from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope -from .SubscribedPackagesModel import SubscribedPackagesModel - -i18n_catalog = i18nCatalog("cura") - - -class DownloadPresenter: - """Downloads a set of packages from the Ultimaker Cloud Marketplace - - use download() exactly once: should not be used for multiple sets of downloads since this class contains state - """ - - DISK_WRITE_BUFFER_SIZE = 256 * 1024 # 256 KB - - def __init__(self, app: CuraApplication) -> None: - # Emits (Dict[str, str], List[str]) # (success_items, error_items) - # Dict{success_package_id, temp_file_path} - # List[errored_package_id] - self.done = Signal() - - self._app = app - self._scope = UltimakerCloudScope(app) - - self._started = False - self._progress_message = self._createProgressMessage() - self._progress = {} # type: Dict[str, Dict[str, Any]] # package_id, Dict - self._error = [] # type: List[str] # package_id - - def download(self, model: SubscribedPackagesModel) -> None: - if self._started: - Logger.error("Download already started. Create a new %s instead", self.__class__.__name__) - return - - manager = HttpRequestManager.getInstance() - for item in model.items: - package_id = item["package_id"] - - def finishedCallback(reply: QNetworkReply, pid = package_id) -> None: - self._onFinished(pid, reply) - - def progressCallback(rx: int, rt: int, pid = package_id) -> None: - self._onProgress(pid, rx, rt) - - def errorCallback(reply: QNetworkReply, error: QNetworkReply.NetworkError, pid = package_id) -> None: - self._onError(pid) - - request_data = manager.get( - item["download_url"], - callback = finishedCallback, - download_progress_callback = progressCallback, - error_callback = errorCallback, - scope = self._scope) - - self._progress[package_id] = { - "received": 0, - "total": 1, # make sure this is not considered done yet. Also divByZero-safe - "file_written": None, - "request_data": request_data, - "package_model": item - } - - self._started = True - self._progress_message.show() - - def abort(self) -> None: - manager = HttpRequestManager.getInstance() - for item in self._progress.values(): - manager.abortRequest(item["request_data"]) - - # Aborts all current operations and returns a copy with the same settings such as app and scope - def resetCopy(self) -> "DownloadPresenter": - self.abort() - self.done.disconnectAll() - return DownloadPresenter(self._app) - - def _createProgressMessage(self) -> Message: - return Message(i18n_catalog.i18nc("@info:generic", "Syncing..."), - lifetime = 0, - use_inactivity_timer = False, - progress = 0.0, - title = i18n_catalog.i18nc("@info:title", "Changes detected from your Ultimaker account")) - - def _onFinished(self, package_id: str, reply: QNetworkReply) -> None: - self._progress[package_id]["received"] = self._progress[package_id]["total"] - - try: - with tempfile.NamedTemporaryFile(mode = "wb+", suffix = ".curapackage", delete = False) as temp_file: - bytes_read = reply.read(self.DISK_WRITE_BUFFER_SIZE) - while bytes_read: - temp_file.write(bytes_read) - bytes_read = reply.read(self.DISK_WRITE_BUFFER_SIZE) - self._app.processEvents() - self._progress[package_id]["file_written"] = temp_file.name - except IOError as e: - Logger.logException("e", "Failed to write downloaded package to temp file", e) - self._onError(package_id) - temp_file.close() - - self._checkDone() - - def _onProgress(self, package_id: str, rx: int, rt: int) -> None: - self._progress[package_id]["received"] = rx - self._progress[package_id]["total"] = rt - - received = 0 - total = 0 - for item in self._progress.values(): - received += item["received"] - total += item["total"] - - if total == 0: # Total download size is 0, or unknown, or there are no progress items at all. - self._progress_message.setProgress(100.0) - return - - self._progress_message.setProgress(100.0 * (received / total)) # [0 .. 100] % - - def _onError(self, package_id: str) -> None: - self._progress.pop(package_id) - self._error.append(package_id) - self._checkDone() - - def _checkDone(self) -> bool: - for item in self._progress.values(): - if not item["file_written"]: - return False - - success_items = { - package_id: - { - "package_path": value["file_written"], - "icon_url": value["package_model"]["icon_url"] - } - for package_id, value in self._progress.items() - } - error_items = [package_id for package_id in self._error] - - self._progress_message.hide() - self.done.emit(success_items, error_items) - return True diff --git a/plugins/Toolbox/src/CloudSync/LicenseModel.py b/plugins/Toolbox/src/CloudSync/LicenseModel.py deleted file mode 100644 index 335a91ef84..0000000000 --- a/plugins/Toolbox/src/CloudSync/LicenseModel.py +++ /dev/null @@ -1,77 +0,0 @@ -from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal -from UM.i18n import i18nCatalog - -catalog = i18nCatalog("cura") - - -# Model for the ToolboxLicenseDialog -class LicenseModel(QObject): - DEFAULT_DECLINE_BUTTON_TEXT = catalog.i18nc("@button", "Decline") - ACCEPT_BUTTON_TEXT = catalog.i18nc("@button", "Agree") - - dialogTitleChanged = pyqtSignal() - packageNameChanged = pyqtSignal() - licenseTextChanged = pyqtSignal() - iconChanged = pyqtSignal() - - def __init__(self, decline_button_text: str = DEFAULT_DECLINE_BUTTON_TEXT) -> None: - super().__init__() - - self._current_page_idx = 0 - self._page_count = 1 - self._dialogTitle = "" - self._license_text = "" - self._package_name = "" - self._icon_url = "" - self._decline_button_text = decline_button_text - - @pyqtProperty(str, constant = True) - def acceptButtonText(self): - return self.ACCEPT_BUTTON_TEXT - - @pyqtProperty(str, constant = True) - def declineButtonText(self): - return self._decline_button_text - - @pyqtProperty(str, notify=dialogTitleChanged) - def dialogTitle(self) -> str: - return self._dialogTitle - - @pyqtProperty(str, notify=packageNameChanged) - def packageName(self) -> str: - return self._package_name - - def setPackageName(self, name: str) -> None: - self._package_name = name - self.packageNameChanged.emit() - - @pyqtProperty(str, notify=iconChanged) - def iconUrl(self) -> str: - return self._icon_url - - def setIconUrl(self, url: str): - self._icon_url = url - self.iconChanged.emit() - - @pyqtProperty(str, notify=licenseTextChanged) - def licenseText(self) -> str: - return self._license_text - - def setLicenseText(self, license_text: str) -> None: - if self._license_text != license_text: - self._license_text = license_text - self.licenseTextChanged.emit() - - def setCurrentPageIdx(self, idx: int) -> None: - self._current_page_idx = idx - self._updateDialogTitle() - - def setPageCount(self, count: int) -> None: - self._page_count = count - self._updateDialogTitle() - - def _updateDialogTitle(self): - self._dialogTitle = catalog.i18nc("@title:window", "Plugin License Agreement") - if self._page_count > 1: - self._dialogTitle = self._dialogTitle + " ({}/{})".format(self._current_page_idx + 1, self._page_count) - self.dialogTitleChanged.emit() diff --git a/plugins/Toolbox/src/CloudSync/LicensePresenter.py b/plugins/Toolbox/src/CloudSync/LicensePresenter.py deleted file mode 100644 index 39ce11c8d3..0000000000 --- a/plugins/Toolbox/src/CloudSync/LicensePresenter.py +++ /dev/null @@ -1,142 +0,0 @@ -# Copyright (c) 2021 Ultimaker B.V. -# Cura is released under the terms of the LGPLv3 or higher. - -import os -from collections import OrderedDict -from typing import Dict, Optional, List, Any - -from PyQt5.QtCore import QObject, pyqtSlot - -from UM.Logger import Logger -from UM.PackageManager import PackageManager -from UM.Signal import Signal -from cura.CuraApplication import CuraApplication -from UM.i18n import i18nCatalog - -from .LicenseModel import LicenseModel - - -class LicensePresenter(QObject): - """Presents licenses for a set of packages for the user to accept or reject. - - Call present() exactly once to show a licenseDialog for a set of packages - Before presenting another set of licenses, create a new instance using resetCopy(). - - licenseAnswers emits a list of Dicts containing answers when the user has made a choice for all provided packages. - """ - - def __init__(self, app: CuraApplication) -> None: - super().__init__() - self._presented = False - """Whether present() has been called and state is expected to be initialized""" - self._catalog = i18nCatalog("cura") - self._dialog = None # type: Optional[QObject] - self._package_manager = app.getPackageManager() # type: PackageManager - # Emits List[Dict[str, [Any]] containing for example - # [{ "package_id": "BarbarianPlugin", "package_path" : "/tmp/dg345as", "accepted" : True }] - self.licenseAnswers = Signal() - - self._current_package_idx = 0 - self._package_models = [] # type: List[Dict] - decline_button_text = self._catalog.i18nc("@button", "Decline and remove from account") - self._license_model = LicenseModel(decline_button_text=decline_button_text) # type: LicenseModel - self._page_count = 0 - - self._app = app - - self._compatibility_dialog_path = "resources/qml/dialogs/ToolboxLicenseDialog.qml" - - def present(self, plugin_path: str, packages: Dict[str, Dict[str, str]]) -> None: - """Show a license dialog for multiple packages where users can read a license and accept or decline them - - :param plugin_path: Root directory of the Toolbox plugin - :param packages: Dict[package id, file path] - """ - if self._presented: - Logger.error("{clazz} is single-use. Create a new {clazz} instead", clazz=self.__class__.__name__) - return - - path = os.path.join(plugin_path, self._compatibility_dialog_path) - - self._initState(packages) - - if self._page_count == 0: - self.licenseAnswers.emit(self._package_models) - return - - if self._dialog is None: - - context_properties = { - "catalog": self._catalog, - "licenseModel": self._license_model, - "handler": self - } - self._dialog = self._app.createQmlComponent(path, context_properties) - self._presentCurrentPackage() - self._presented = True - - def resetCopy(self) -> "LicensePresenter": - """Clean up and return a new copy with the same settings such as app""" - if self._dialog: - self._dialog.close() - self.licenseAnswers.disconnectAll() - return LicensePresenter(self._app) - - @pyqtSlot() - def onLicenseAccepted(self) -> None: - self._package_models[self._current_package_idx]["accepted"] = True - self._checkNextPage() - - @pyqtSlot() - def onLicenseDeclined(self) -> None: - self._package_models[self._current_package_idx]["accepted"] = False - self._checkNextPage() - - def _initState(self, packages: Dict[str, Dict[str, Any]]) -> None: - - implicitly_accepted_count = 0 - - for package_id, item in packages.items(): - item["package_id"] = package_id - try: - item["licence_content"] = self._package_manager.getPackageLicense(item["package_path"]) - except EnvironmentError as e: - Logger.error(f"Could not open downloaded package {package_id} to read license file! {type(e)} - {e}") - continue # Skip this package. - if item["licence_content"] is None: - # Implicitly accept when there is no license - item["accepted"] = True - implicitly_accepted_count = implicitly_accepted_count + 1 - self._package_models.append(item) - else: - item["accepted"] = None #: None: no answer yet - # When presenting the packages, we want to show packages which have a license first. - # In fact, we don't want to show the others at all because they are implicitly accepted - self._package_models.insert(0, item) - CuraApplication.getInstance().processEvents() - self._page_count = len(self._package_models) - implicitly_accepted_count - self._license_model.setPageCount(self._page_count) - - - def _presentCurrentPackage(self) -> None: - package_model = self._package_models[self._current_package_idx] - package_info = self._package_manager.getPackageInfo(package_model["package_path"]) - - self._license_model.setCurrentPageIdx(self._current_package_idx) - self._license_model.setPackageName(package_info["display_name"]) - self._license_model.setIconUrl(package_model["icon_url"]) - self._license_model.setLicenseText(package_model["licence_content"]) - if self._dialog: - self._dialog.open() # Does nothing if already open - - def _checkNextPage(self) -> None: - if self._current_package_idx + 1 < self._page_count: - self._current_package_idx += 1 - self._presentCurrentPackage() - else: - if self._dialog: - self._dialog.close() - self.licenseAnswers.emit(self._package_models) - - - diff --git a/plugins/Toolbox/src/CloudSync/RestartApplicationPresenter.py b/plugins/Toolbox/src/CloudSync/RestartApplicationPresenter.py deleted file mode 100644 index 8776d1782a..0000000000 --- a/plugins/Toolbox/src/CloudSync/RestartApplicationPresenter.py +++ /dev/null @@ -1,32 +0,0 @@ -from UM import i18nCatalog -from UM.Message import Message -from cura.CuraApplication import CuraApplication - - -class RestartApplicationPresenter: - """Presents a dialog telling the user that a restart is required to apply changes - - Since we cannot restart Cura, the app is closed instead when the button is clicked - """ - def __init__(self, app: CuraApplication) -> None: - self._app = app - self._i18n_catalog = i18nCatalog("cura") - - def present(self) -> None: - app_name = self._app.getApplicationDisplayName() - - message = Message(self._i18n_catalog.i18nc("@info:generic", - "You need to quit and restart {} before changes have effect.", - app_name)) - - message.addAction("quit", - name="Quit " + app_name, - icon = "", - description="Close the application", - button_align=Message.ActionButtonAlignment.ALIGN_RIGHT) - - message.actionTriggered.connect(self._quitClicked) - message.show() - - def _quitClicked(self, *_): - self._app.windowClosed() diff --git a/plugins/Toolbox/src/CloudSync/SubscribedPackagesModel.py b/plugins/Toolbox/src/CloudSync/SubscribedPackagesModel.py deleted file mode 100644 index db16c5ea84..0000000000 --- a/plugins/Toolbox/src/CloudSync/SubscribedPackagesModel.py +++ /dev/null @@ -1,74 +0,0 @@ -# Copyright (c) 2020 Ultimaker B.V. -# Cura is released under the terms of the LGPLv3 or higher. - -from PyQt5.QtCore import Qt, pyqtProperty, pyqtSlot - -from UM.PackageManager import PackageManager -from UM.Qt.ListModel import ListModel -from UM.Version import Version - -from cura import ApplicationMetadata -from typing import List, Dict, Any - - -class SubscribedPackagesModel(ListModel): - def __init__(self, parent = None): - super().__init__(parent) - - self._items = [] - self._metadata = None - self._discrepancies = None - self._sdk_version = ApplicationMetadata.CuraSDKVersion - - self.addRoleName(Qt.UserRole + 1, "package_id") - self.addRoleName(Qt.UserRole + 2, "display_name") - self.addRoleName(Qt.UserRole + 3, "icon_url") - self.addRoleName(Qt.UserRole + 4, "is_compatible") - self.addRoleName(Qt.UserRole + 5, "is_dismissed") - - @pyqtProperty(bool, constant=True) - def hasCompatiblePackages(self) -> bool: - for item in self._items: - if item['is_compatible']: - return True - return False - - @pyqtProperty(bool, constant=True) - def hasIncompatiblePackages(self) -> bool: - for item in self._items: - if not item['is_compatible']: - return True - return False - - def addDiscrepancies(self, discrepancy: List[str]) -> None: - self._discrepancies = discrepancy - - def getCompatiblePackages(self) -> List[Dict[str, Any]]: - return [package for package in self._items if package["is_compatible"]] - - def getIncompatiblePackages(self) -> List[str]: - return [package["package_id"] for package in self._items if not package["is_compatible"]] - - def initialize(self, package_manager: PackageManager, subscribed_packages_payload: List[Dict[str, Any]]) -> None: - self._items.clear() - for item in subscribed_packages_payload: - if item["package_id"] not in self._discrepancies: - continue - package = { - "package_id": item["package_id"], - "display_name": item["display_name"], - "sdk_versions": item["sdk_versions"], - "download_url": item["download_url"], - "md5_hash": item["md5_hash"], - "is_dismissed": False, - } - - compatible = any(package_manager.isPackageCompatible(Version(version)) for version in item["sdk_versions"]) - package.update({"is_compatible": compatible}) - - try: - package.update({"icon_url": item["icon_url"]}) - except KeyError: # There is no 'icon_url" in the response payload for this package - package.update({"icon_url": ""}) - self._items.append(package) - self.setItems(self._items) diff --git a/plugins/Toolbox/src/CloudSync/SyncOrchestrator.py b/plugins/Toolbox/src/CloudSync/SyncOrchestrator.py deleted file mode 100644 index bb37c6d4a9..0000000000 --- a/plugins/Toolbox/src/CloudSync/SyncOrchestrator.py +++ /dev/null @@ -1,114 +0,0 @@ -# Copyright (c) 2021 Ultimaker B.V. -# Cura is released under the terms of the LGPLv3 or higher. - -import os -from typing import List, Dict, Any, cast - -from UM import i18n_catalog -from UM.Extension import Extension -from UM.Logger import Logger -from UM.Message import Message -from UM.PluginRegistry import PluginRegistry -from cura.CuraApplication import CuraApplication -from .CloudPackageChecker import CloudPackageChecker -from .CloudApiClient import CloudApiClient -from .DiscrepanciesPresenter import DiscrepanciesPresenter -from .DownloadPresenter import DownloadPresenter -from .LicensePresenter import LicensePresenter -from .RestartApplicationPresenter import RestartApplicationPresenter -from .SubscribedPackagesModel import SubscribedPackagesModel - - -class SyncOrchestrator(Extension): - """Orchestrates the synchronizing of packages from the user account to the installed packages - - Example flow: - - - CloudPackageChecker compares a list of packages the user `subscribed` to in their account - If there are `discrepancies` between the account and locally installed packages, they are emitted - - DiscrepanciesPresenter shows a list of packages to be added or removed to the user. It emits the `packageMutations` - the user selected to be performed - - The SyncOrchestrator uses PackageManager to remove local packages the users wants to see removed - - The DownloadPresenter shows a download progress dialog. It emits A tuple of succeeded and failed downloads - - The LicensePresenter extracts licenses from the downloaded packages and presents a license for each package to - be installed. It emits the `licenseAnswers` signal for accept or declines - - The CloudApiClient removes the declined packages from the account - - The SyncOrchestrator uses PackageManager to install the downloaded packages and delete temp files. - - The RestartApplicationPresenter notifies the user that a restart is required for changes to take effect - """ - - def __init__(self, app: CuraApplication) -> None: - super().__init__() - # Differentiate This PluginObject from the Toolbox. self.getId() includes _name. - # getPluginId() will return the same value for The toolbox extension and this one - self._name = "SyncOrchestrator" - - self._package_manager = app.getPackageManager() - # Keep a reference to the CloudApiClient. it watches for installed packages and subscribes to them - self._cloud_api = CloudApiClient.getInstance(app) # type: CloudApiClient - - self._checker = CloudPackageChecker(app) # type: CloudPackageChecker - self._checker.discrepancies.connect(self._onDiscrepancies) - - self._discrepancies_presenter = DiscrepanciesPresenter(app) # type: DiscrepanciesPresenter - self._discrepancies_presenter.packageMutations.connect(self._onPackageMutations) - - self._download_presenter = DownloadPresenter(app) # type: DownloadPresenter - - self._license_presenter = LicensePresenter(app) # type: LicensePresenter - self._license_presenter.licenseAnswers.connect(self._onLicenseAnswers) - - self._restart_presenter = RestartApplicationPresenter(app) - - def _onDiscrepancies(self, model: SubscribedPackagesModel) -> None: - plugin_path = cast(str, PluginRegistry.getInstance().getPluginPath(self.getPluginId())) - self._discrepancies_presenter.present(plugin_path, model) - - def _onPackageMutations(self, mutations: SubscribedPackagesModel) -> None: - self._download_presenter = self._download_presenter.resetCopy() - self._download_presenter.done.connect(self._onDownloadFinished) - self._download_presenter.download(mutations) - - def _onDownloadFinished(self, success_items: Dict[str, Dict[str, str]], error_items: List[str]) -> None: - """Called when a set of packages have finished downloading - - :param success_items:: Dict[package_id, Dict[str, str]] - :param error_items:: List[package_id] - """ - if error_items: - message = i18n_catalog.i18nc("@info:generic", "{} plugins failed to download".format(len(error_items))) - self._showErrorMessage(message) - - plugin_path = cast(str, PluginRegistry.getInstance().getPluginPath(self.getPluginId())) - self._license_presenter = self._license_presenter.resetCopy() - self._license_presenter.licenseAnswers.connect(self._onLicenseAnswers) - self._license_presenter.present(plugin_path, success_items) - - # Called when user has accepted / declined all licenses for the downloaded packages - def _onLicenseAnswers(self, answers: List[Dict[str, Any]]) -> None: - has_changes = False # True when at least one package is installed - - for item in answers: - if item["accepted"]: - # install and subscribe packages - if not self._package_manager.installPackage(item["package_path"]): - message = "Could not install {}".format(item["package_id"]) - self._showErrorMessage(message) - continue - has_changes = True - else: - self._cloud_api.unsubscribe(item["package_id"]) - # delete temp file - try: - os.remove(item["package_path"]) - except EnvironmentError as e: # File was already removed, no access rights, etc. - Logger.error("Can't delete temporary package file: {err}".format(err = str(e))) - - if has_changes: - self._restart_presenter.present() - - def _showErrorMessage(self, text: str): - """Logs an error and shows it to the user""" - - Logger.error(text) - Message(text, lifetime = 0, message_type = Message.MessageType.ERROR).show() diff --git a/plugins/Toolbox/src/CloudSync/__init__.py b/plugins/Toolbox/src/CloudSync/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 --- a/plugins/Toolbox/src/CloudSync/__init__.py +++ /dev/null diff --git a/plugins/Toolbox/src/ConfigsModel.py b/plugins/Toolbox/src/ConfigsModel.py deleted file mode 100644 index a53817653f..0000000000 --- a/plugins/Toolbox/src/ConfigsModel.py +++ /dev/null @@ -1,38 +0,0 @@ -# Copyright (c) 2018 Ultimaker B.V. -# Cura is released under the terms of the LGPLv3 or higher. - -from PyQt5.QtCore import Qt - -from UM.Qt.ListModel import ListModel - - -class ConfigsModel(ListModel): - """Model that holds supported configurations (for material/quality packages).""" - - def __init__(self, parent = None): - super().__init__(parent) - - self._configs = None - - self.addRoleName(Qt.UserRole + 1, "machine") - self.addRoleName(Qt.UserRole + 2, "print_core") - self.addRoleName(Qt.UserRole + 3, "build_plate") - self.addRoleName(Qt.UserRole + 4, "support_material") - self.addRoleName(Qt.UserRole + 5, "quality") - - def setConfigs(self, configs): - self._configs = configs - self._update() - - def _update(self): - items = [] - for item in self._configs: - items.append({ - "machine": item["machine"], - "print_core": item["print_core"], - "build_plate": item["build_plate"], - "support_material": item["support_material"], - "quality": item["quality"] - }) - - self.setItems(items) diff --git a/plugins/Toolbox/src/PackagesModel.py b/plugins/Toolbox/src/PackagesModel.py deleted file mode 100644 index 97645ae466..0000000000 --- a/plugins/Toolbox/src/PackagesModel.py +++ /dev/null @@ -1,161 +0,0 @@ -# Copyright (c) 2021 Ultimaker B.V. -# Cura is released under the terms of the LGPLv3 or higher. - -import re -from typing import Dict - -from PyQt5.QtCore import Qt, pyqtProperty - -from UM.Logger import Logger -from UM.Qt.ListModel import ListModel - -from .ConfigsModel import ConfigsModel - - -class PackagesModel(ListModel): - """Model that holds Cura packages. - - By setting the filter property the instances held by this model can be changed. - """ - - def __init__(self, parent = None): - super().__init__(parent) - - self._metadata = None - - self.addRoleName(Qt.UserRole + 1, "id") - self.addRoleName(Qt.UserRole + 2, "type") - self.addRoleName(Qt.UserRole + 3, "name") - self.addRoleName(Qt.UserRole + 4, "version") - self.addRoleName(Qt.UserRole + 5, "author_id") - self.addRoleName(Qt.UserRole + 6, "author_name") - self.addRoleName(Qt.UserRole + 7, "author_email") - self.addRoleName(Qt.UserRole + 8, "description") - self.addRoleName(Qt.UserRole + 9, "icon_url") - self.addRoleName(Qt.UserRole + 10, "image_urls") - self.addRoleName(Qt.UserRole + 11, "download_url") - self.addRoleName(Qt.UserRole + 12, "last_updated") - self.addRoleName(Qt.UserRole + 13, "is_bundled") - self.addRoleName(Qt.UserRole + 14, "is_active") - self.addRoleName(Qt.UserRole + 15, "is_installed") # Scheduled pkgs are included in the model but should not be marked as actually installed - self.addRoleName(Qt.UserRole + 16, "has_configs") - self.addRoleName(Qt.UserRole + 17, "supported_configs") - self.addRoleName(Qt.UserRole + 18, "download_count") - self.addRoleName(Qt.UserRole + 19, "tags") - self.addRoleName(Qt.UserRole + 20, "links") - self.addRoleName(Qt.UserRole + 21, "website") - self.addRoleName(Qt.UserRole + 22, "login_required") - - # List of filters for queries. The result is the union of the each list of results. - self._filter = {} # type: Dict[str, str] - - def setMetadata(self, data): - if self._metadata != data: - self._metadata = data - self._update() - - def _update(self): - items = [] - - if self._metadata is None: - self.setItems(items) - return - - for package in self._metadata: - has_configs = False - configs_model = None - - links_dict = {} - if "data" in package: - # Links is a list of dictionaries with "title" and "url". Convert this list into a dict so it's easier - # to process. - link_list = package["data"]["links"] if "links" in package["data"] else [] - links_dict = {d["title"]: d["url"] for d in link_list} - - # This code never gets executed because the API response does not contain "supported_configs" in it - # It is so because 2y ago when this was created - it did contain it. But it was a prototype only - # and never got to production. As agreed with the team, it'll stay here for now, in case we decide to rework and use it - # The response payload has been changed. Please see: - # https://github.com/Ultimaker/Cura/compare/CURA-7072-temp?expand=1 - if "supported_configs" in package["data"]: - if len(package["data"]["supported_configs"]) > 0: - has_configs = True - configs_model = ConfigsModel() - configs_model.setConfigs(package["data"]["supported_configs"]) - - if "author_id" not in package["author"] or "display_name" not in package["author"]: - package["author"]["author_id"] = "" - package["author"]["display_name"] = "" - - items.append({ - "id": package["package_id"], - "type": package["package_type"], - "name": package["display_name"].strip(), - "version": package["package_version"], - "author_id": package["author"]["author_id"], - "author_name": package["author"]["display_name"], - "author_email": package["author"]["email"] if "email" in package["author"] else None, - "description": package["description"] if "description" in package else None, - "icon_url": package["icon_url"] if "icon_url" in package else None, - "image_urls": package["image_urls"] if "image_urls" in package else None, - "download_url": package["download_url"] if "download_url" in package else None, - "last_updated": package["last_updated"] if "last_updated" in package else None, - "is_bundled": package["is_bundled"] if "is_bundled" in package else False, - "is_active": package["is_active"] if "is_active" in package else False, - "is_installed": package["is_installed"] if "is_installed" in package else False, - "has_configs": has_configs, - "supported_configs": configs_model, - "download_count": package["download_count"] if "download_count" in package else 0, - "tags": package["tags"] if "tags" in package else [], - "links": links_dict, - "website": package["website"] if "website" in package else None, - "login_required": "login-required" in package.get("tags", []), - }) - - # Filter on all the key-word arguments. - for key, value in self._filter.items(): - if key == "tags": - key_filter = lambda item, v = value: v in item["tags"] - elif "*" in value: - key_filter = lambda candidate, k = key, v = value: self._matchRegExp(candidate, k, v) - else: - key_filter = lambda candidate, k = key, v = value: self._matchString(candidate, k, v) - items = filter(key_filter, items) - - # Execute all filters. - filtered_items = list(items) - - filtered_items.sort(key = lambda k: k["name"]) - self.setItems(filtered_items) - - def setFilter(self, filter_dict: Dict[str, str]) -> None: - """Set the filter of this model based on a string. - - :param filter_dict: Dictionary to do the filtering by. - """ - if filter_dict != self._filter: - self._filter = filter_dict - self._update() - - @pyqtProperty("QVariantMap", fset = setFilter, constant = True) - def filter(self) -> Dict[str, str]: - return self._filter - - # Check to see if a container matches with a regular expression - def _matchRegExp(self, metadata, property_name, value): - if property_name not in metadata: - return False - value = re.escape(value) #Escape for regex patterns. - value = "^" + value.replace("\\*", ".*") + "$" #Instead of (now escaped) asterisks, match on any string. Also add anchors for a complete match. - if self._ignore_case: - value_pattern = re.compile(value, re.IGNORECASE) - else: - value_pattern = re.compile(value) - - return value_pattern.match(str(metadata[property_name])) - - # Check to see if a container matches with a string - def _matchString(self, metadata, property_name, value): - if property_name not in metadata: - return False - return value.lower() == str(metadata[property_name]).lower() diff --git a/plugins/Toolbox/src/Toolbox.py b/plugins/Toolbox/src/Toolbox.py deleted file mode 100644 index e525a88d89..0000000000 --- a/plugins/Toolbox/src/Toolbox.py +++ /dev/null @@ -1,878 +0,0 @@ -# Copyright (c) 2021 Ultimaker B.V. -# Toolbox is released under the terms of the LGPLv3 or higher. - -import json -import os -import tempfile -from typing import cast, Any, Dict, List, Set, TYPE_CHECKING, Tuple, Optional, Union - -from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal, pyqtSlot -from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply - -from UM.Extension import Extension -from UM.Logger import Logger -from UM.PluginRegistry import PluginRegistry -from UM.TaskManagement.HttpRequestScope import JsonDecoratorScope -from UM.Version import Version -from UM.i18n import i18nCatalog -from cura import ApplicationMetadata -from cura.CuraApplication import CuraApplication -from cura.Machines.ContainerTree import ContainerTree -from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope -from .AuthorsModel import AuthorsModel -from .CloudApiModel import CloudApiModel -from .CloudSync.LicenseModel import LicenseModel -from .PackagesModel import PackagesModel - -if TYPE_CHECKING: - from UM.TaskManagement.HttpRequestData import HttpRequestData - from cura.Settings.GlobalStack import GlobalStack - -i18n_catalog = i18nCatalog("cura") - -DEFAULT_MARKETPLACE_ROOT = "https://marketplace.ultimaker.com" # type: str - -try: - from cura.CuraVersion import CuraMarketplaceRoot -except ImportError: - CuraMarketplaceRoot = DEFAULT_MARKETPLACE_ROOT - - -class Toolbox(QObject, Extension): - """Provides a marketplace for users to download plugins an materials""" - - def __init__(self, application: CuraApplication) -> None: - super().__init__() - - self._application = application # type: CuraApplication - - # Network: - self._download_request_data = None # type: Optional[HttpRequestData] - self._download_progress = 0 # type: float - self._is_downloading = False # type: bool - self._cloud_scope = UltimakerCloudScope(application) # type: UltimakerCloudScope - self._json_scope = JsonDecoratorScope(self._cloud_scope) # type: JsonDecoratorScope - - self._request_urls = {} # type: Dict[str, str] - self._to_update = [] # type: List[str] # Package_ids that are waiting to be updated - self._old_plugin_ids = set() # type: Set[str] - self._old_plugin_metadata = dict() # type: Dict[str, Dict[str, Any]] - - # The responses as given by the server parsed to a list. - self._server_response_data = { - "authors": [], - "packages": [], - "updates": [] - } # type: Dict[str, List[Any]] - - # Models: - self._models = { - "authors": AuthorsModel(self), - "packages": PackagesModel(self), - "updates": PackagesModel(self) - } # type: Dict[str, Union[AuthorsModel, PackagesModel]] - - self._plugins_showcase_model = PackagesModel(self) - self._plugins_available_model = PackagesModel(self) - self._plugins_installed_model = PackagesModel(self) - self._plugins_installed_model.setFilter({"is_bundled": "False"}) - self._plugins_bundled_model = PackagesModel(self) - self._plugins_bundled_model.setFilter({"is_bundled": "True"}) - self._materials_showcase_model = AuthorsModel(self) - self._materials_available_model = AuthorsModel(self) - self._materials_installed_model = PackagesModel(self) - self._materials_installed_model.setFilter({"is_bundled": "False"}) - self._materials_bundled_model = PackagesModel(self) - self._materials_bundled_model.setFilter({"is_bundled": "True"}) - self._materials_generic_model = PackagesModel(self) - - self._license_model = LicenseModel() - - # These properties are for keeping track of the UI state: - # ---------------------------------------------------------------------- - # View category defines which filter to use, and therefore effectively - # which category is currently being displayed. For example, possible - # values include "plugin" or "material", but also "installed". - self._view_category = "plugin" # type: str - - # View page defines which type of page layout to use. For example, - # possible values include "overview", "detail" or "author". - self._view_page = "welcome" # type: str - - # Active package refers to which package is currently being downloaded, - # installed, or otherwise modified. - self._active_package = None # type: Optional[Dict[str, Any]] - - self._dialog = None # type: Optional[QObject] - self._confirm_reset_dialog = None # type: Optional[QObject] - self._resetUninstallVariables() - - self._restart_required = False # type: bool - - # variables for the license agreement dialog - self._license_dialog_plugin_file_location = "" # type: str - - self._application.initializationFinished.connect(self._onAppInitialized) - - # Signals: - # -------------------------------------------------------------------------- - # Downloading changes - activePackageChanged = pyqtSignal() - onDownloadProgressChanged = pyqtSignal() - onIsDownloadingChanged = pyqtSignal() - restartRequiredChanged = pyqtSignal() - installChanged = pyqtSignal() - toolboxEnabledChanged = pyqtSignal() - - # UI changes - viewChanged = pyqtSignal() - detailViewChanged = pyqtSignal() - filterChanged = pyqtSignal() - metadataChanged = pyqtSignal() - showLicenseDialog = pyqtSignal() - closeLicenseDialog = pyqtSignal() - uninstallVariablesChanged = pyqtSignal() - - def _restart(self): - """Go back to the start state (welcome screen or loading if no login required)""" - - # For an Essentials build, login is mandatory - if not self._application.getCuraAPI().account.isLoggedIn and ApplicationMetadata.IsEnterpriseVersion: - self.setViewPage("welcome") - else: - self.setViewPage("loading") - self._fetchPackageData() - - def _resetUninstallVariables(self) -> None: - self._package_id_to_uninstall = None # type: Optional[str] - self._package_name_to_uninstall = "" - self._package_used_materials = [] # type: List[Tuple[GlobalStack, str, str]] - self._package_used_qualities = [] # type: List[Tuple[GlobalStack, str, str]] - - def getLicenseDialogPluginFileLocation(self) -> str: - return self._license_dialog_plugin_file_location - - def openLicenseDialog(self, plugin_name: str, license_content: str, plugin_file_location: str, icon_url: str) -> None: - # Set page 1/1 when opening the dialog for a single package - self._license_model.setCurrentPageIdx(0) - self._license_model.setPageCount(1) - self._license_model.setIconUrl(icon_url) - - self._license_model.setPackageName(plugin_name) - self._license_model.setLicenseText(license_content) - self._license_dialog_plugin_file_location = plugin_file_location - self.showLicenseDialog.emit() - - # This is a plugin, so most of the components required are not ready when - # this is initialized. Therefore, we wait until the application is ready. - def _onAppInitialized(self) -> None: - self._plugin_registry = self._application.getPluginRegistry() - self._package_manager = self._application.getPackageManager() - - # We need to construct a query like installed_packages=ID:VERSION&installed_packages=ID:VERSION, etc. - installed_package_ids_with_versions = [":".join(items) for items in - self._package_manager.getAllInstalledPackageIdsAndVersions()] - installed_packages_query = "&installed_packages=".join(installed_package_ids_with_versions) - - self._request_urls = { - "authors": "{base_url}/authors".format(base_url = CloudApiModel.api_url), - "packages": "{base_url}/packages".format(base_url = CloudApiModel.api_url), - "updates": "{base_url}/packages/package-updates?installed_packages={query}".format( - base_url = CloudApiModel.api_url, query = installed_packages_query) - } - - self._application.getCuraAPI().account.loginStateChanged.connect(self._restart) - - preferences = CuraApplication.getInstance().getPreferences() - - preferences.addPreference("info/automatic_plugin_update_check", True) - - # On boot we check which packages have updates. - if preferences.getValue("info/automatic_plugin_update_check") and len(installed_package_ids_with_versions) > 0: - # Request the latest and greatest! - self._makeRequestByType("updates") - - def _fetchPackageData(self) -> None: - self._makeRequestByType("packages") - self._makeRequestByType("authors") - self._updateInstalledModels() - - # Displays the toolbox - @pyqtSlot() - def launch(self) -> None: - if not self._dialog: - self._dialog = self._createDialog("Toolbox.qml") - - if not self._dialog: - Logger.log("e", "Unexpected error trying to create the 'Marketplace' dialog.") - return - - self._restart() - - self._dialog.show() - # Apply enabled/disabled state to installed plugins - self.toolboxEnabledChanged.emit() - - def _createDialog(self, qml_name: str) -> Optional[QObject]: - Logger.log("d", "Marketplace: Creating dialog [%s].", qml_name) - plugin_path = PluginRegistry.getInstance().getPluginPath(self.getPluginId()) - if not plugin_path: - return None - path = os.path.join(plugin_path, "resources", "qml", qml_name) - - dialog = self._application.createQmlComponent(path, { - "toolbox": self, - "handler": self, - "licenseModel": self._license_model - }) - if not dialog: - return None - return dialog - - def _convertPluginMetadata(self, plugin_data: Dict[str, Any]) -> Optional[Dict[str, Any]]: - try: - highest_sdk_version_supported = Version(0) - for supported_version in plugin_data["plugin"]["supported_sdk_versions"]: - if supported_version > highest_sdk_version_supported: - highest_sdk_version_supported = supported_version - - formatted = { - "package_id": plugin_data["id"], - "package_type": "plugin", - "display_name": plugin_data["plugin"]["name"], - "package_version": plugin_data["plugin"]["version"], - "sdk_version": highest_sdk_version_supported, - "author": { - "author_id": plugin_data["plugin"]["author"], - "display_name": plugin_data["plugin"]["author"] - }, - "is_installed": True, - "description": plugin_data["plugin"]["description"] - } - return formatted - except KeyError: - Logger.log("w", "Unable to convert plugin meta data %s", str(plugin_data)) - return None - - @pyqtSlot() - def _updateInstalledModels(self) -> None: - # This is moved here to avoid code duplication and so that after installing plugins they get removed from the - # list of old plugins - old_plugin_ids = self._plugin_registry.getInstalledPlugins() - installed_package_ids = self._package_manager.getAllInstalledPackageIDs() - scheduled_to_remove_package_ids = self._package_manager.getToRemovePackageIDs() - - self._old_plugin_ids = set() - self._old_plugin_metadata = dict() - - for plugin_id in old_plugin_ids: - # Neither the installed packages nor the packages that are scheduled to remove are old plugins - if plugin_id not in installed_package_ids and plugin_id not in scheduled_to_remove_package_ids: - Logger.log("d", "Found a plugin that was installed with the old plugin browser: %s", plugin_id) - - old_metadata = self._plugin_registry.getMetaData(plugin_id) - new_metadata = self._convertPluginMetadata(old_metadata) - if new_metadata is None: - # Something went wrong converting it. - continue - self._old_plugin_ids.add(plugin_id) - self._old_plugin_metadata[new_metadata["package_id"]] = new_metadata - - all_packages = self._package_manager.getAllInstalledPackagesInfo() - if "plugin" in all_packages: - # For old plugins, we only want to include the old custom plugin that were installed via the old toolbox. - # The bundled plugins will be included in JSON files in the "bundled_packages" folder, so the bundled - # plugins should be excluded from the old plugins list/dict. - all_plugin_package_ids = set(package["package_id"] for package in all_packages["plugin"]) - self._old_plugin_ids = set(plugin_id for plugin_id in self._old_plugin_ids - if plugin_id not in all_plugin_package_ids) - self._old_plugin_metadata = {k: v for k, v in self._old_plugin_metadata.items() if k in self._old_plugin_ids} - - self._plugins_installed_model.setMetadata(all_packages["plugin"] + list(self._old_plugin_metadata.values())) - self._plugins_bundled_model.setMetadata(all_packages["plugin"] + list(self._old_plugin_metadata.values())) - self.metadataChanged.emit() - if "material" in all_packages: - self._materials_installed_model.setMetadata(all_packages["material"]) - self._materials_bundled_model.setMetadata(all_packages["material"]) - self.metadataChanged.emit() - - @pyqtSlot(str) - def install(self, file_path: str) -> Optional[str]: - package_id = self._package_manager.installPackage(file_path) - self.installChanged.emit() - self._updateInstalledModels() - self.metadataChanged.emit() - self._restart_required = True - self.restartRequiredChanged.emit() - return package_id - - @pyqtSlot(str) - def checkPackageUsageAndUninstall(self, package_id: str) -> None: - """Check package usage and uninstall - - If the package is in use, you'll get a confirmation dialog to set everything to default - """ - - package_used_materials, package_used_qualities = self._package_manager.getMachinesUsingPackage(package_id) - if package_used_materials or package_used_qualities: - # Set up "uninstall variables" for resetMaterialsQualitiesAndUninstall - self._package_id_to_uninstall = package_id - package_info = self._package_manager.getInstalledPackageInfo(package_id) - self._package_name_to_uninstall = package_info.get("display_name", package_info.get("package_id")) - self._package_used_materials = package_used_materials - self._package_used_qualities = package_used_qualities - # Ask change to default material / profile - if self._confirm_reset_dialog is None: - self._confirm_reset_dialog = self._createDialog("dialogs/ToolboxConfirmUninstallResetDialog.qml") - self.uninstallVariablesChanged.emit() - if self._confirm_reset_dialog is None: - Logger.log("e", "ToolboxConfirmUninstallResetDialog should have been initialized, but it is not. Not showing dialog and not uninstalling package.") - else: - self._confirm_reset_dialog.show() - else: - # Plain uninstall - self.uninstall(package_id) - - @pyqtProperty(str, notify = uninstallVariablesChanged) - def pluginToUninstall(self) -> str: - return self._package_name_to_uninstall - - @pyqtProperty(str, notify = uninstallVariablesChanged) - def uninstallUsedMaterials(self) -> str: - return "\n".join(["%s (%s)" % (str(global_stack.getName()), material) for global_stack, extruder_nr, material in self._package_used_materials]) - - @pyqtProperty(str, notify = uninstallVariablesChanged) - def uninstallUsedQualities(self) -> str: - return "\n".join(["%s (%s)" % (str(global_stack.getName()), quality) for global_stack, extruder_nr, quality in self._package_used_qualities]) - - @pyqtSlot() - def closeConfirmResetDialog(self) -> None: - if self._confirm_reset_dialog is not None: - self._confirm_reset_dialog.close() - - @pyqtSlot() - def resetMaterialsQualitiesAndUninstall(self) -> None: - """Uses "uninstall variables" to reset qualities and materials, then uninstall - - It's used as an action on Confirm reset on Uninstall - """ - - application = CuraApplication.getInstance() - machine_manager = application.getMachineManager() - container_tree = ContainerTree.getInstance() - - for global_stack, extruder_nr, container_id in self._package_used_materials: - extruder = global_stack.extruderList[int(extruder_nr)] - approximate_diameter = extruder.getApproximateMaterialDiameter() - variant_node = container_tree.machines[global_stack.definition.getId()].variants[extruder.variant.getName()] - default_material_node = variant_node.preferredMaterial(approximate_diameter) - machine_manager.setMaterial(extruder_nr, default_material_node, global_stack = global_stack) - for global_stack, extruder_nr, container_id in self._package_used_qualities: - variant_names = [extruder.variant.getName() for extruder in global_stack.extruderList] - material_bases = [extruder.material.getMetaDataEntry("base_file") for extruder in global_stack.extruderList] - extruder_enabled = [extruder.isEnabled for extruder in global_stack.extruderList] - definition_id = global_stack.definition.getId() - machine_node = container_tree.machines[definition_id] - default_quality_group = machine_node.getQualityGroups(variant_names, material_bases, extruder_enabled)[machine_node.preferred_quality_type] - machine_manager.setQualityGroup(default_quality_group, global_stack = global_stack) - - if self._package_id_to_uninstall is not None: - self._markPackageMaterialsAsToBeUninstalled(self._package_id_to_uninstall) - self.uninstall(self._package_id_to_uninstall) - self._resetUninstallVariables() - self.closeConfirmResetDialog() - - @pyqtSlot() - def onLicenseAccepted(self): - self.closeLicenseDialog.emit() - package_id = self.install(self.getLicenseDialogPluginFileLocation()) - - - @pyqtSlot() - def onLicenseDeclined(self): - self.closeLicenseDialog.emit() - - def _markPackageMaterialsAsToBeUninstalled(self, package_id: str) -> None: - container_registry = self._application.getContainerRegistry() - - all_containers = self._package_manager.getPackageContainerIds(package_id) - for container_id in all_containers: - containers = container_registry.findInstanceContainers(id = container_id) - if not containers: - continue - container = containers[0] - if container.getMetaDataEntry("type") != "material": - continue - root_material_id = container.getMetaDataEntry("base_file") - root_material_containers = container_registry.findInstanceContainers(id = root_material_id) - if not root_material_containers: - continue - root_material_container = root_material_containers[0] - root_material_container.setMetaDataEntry("removed", True) - - @pyqtSlot(str) - def uninstall(self, package_id: str) -> None: - self._package_manager.removePackage(package_id, force_add = True) - self.installChanged.emit() - self._updateInstalledModels() - self.metadataChanged.emit() - self._restart_required = True - self.restartRequiredChanged.emit() - - def _update(self) -> None: - """Actual update packages that are in self._to_update""" - - if self._to_update: - plugin_id = self._to_update.pop(0) - remote_package = self.getRemotePackage(plugin_id) - if remote_package: - download_url = remote_package["download_url"] - Logger.log("d", "Updating package [%s]..." % plugin_id) - self.startDownload(download_url) - else: - Logger.log("e", "Could not update package [%s] because there is no remote package info available.", plugin_id) - - if self._to_update: - self._application.callLater(self._update) - - @pyqtSlot(str) - def update(self, plugin_id: str) -> None: - """Update a plugin by plugin_id""" - - self._to_update.append(plugin_id) - self._application.callLater(self._update) - - @pyqtSlot(str) - def enable(self, plugin_id: str) -> None: - self._plugin_registry.enablePlugin(plugin_id) - self.toolboxEnabledChanged.emit() - Logger.log("i", "%s was set as 'active'.", plugin_id) - self._restart_required = True - self.restartRequiredChanged.emit() - - @pyqtSlot(str) - def disable(self, plugin_id: str) -> None: - self._plugin_registry.disablePlugin(plugin_id) - self.toolboxEnabledChanged.emit() - Logger.log("i", "%s was set as 'deactive'.", plugin_id) - self._restart_required = True - self.restartRequiredChanged.emit() - - @pyqtProperty(bool, notify = metadataChanged) - def dataReady(self) -> bool: - return self._packages_model is not None - - @pyqtProperty(bool, notify = restartRequiredChanged) - def restartRequired(self) -> bool: - return self._restart_required - - @pyqtSlot() - def restart(self) -> None: - self._application.windowClosed() - - def getRemotePackage(self, package_id: str) -> Optional[Dict]: - # TODO: make the lookup in a dict, not a loop. canUpdate is called for every item. - remote_package = None - for package in self._server_response_data["packages"]: - if package["package_id"] == package_id: - remote_package = package - break - return remote_package - - @pyqtSlot(str, result = bool) - def canDowngrade(self, package_id: str) -> bool: - # If the currently installed version is higher than the bundled version (if present), the we can downgrade - # this package. - local_package = self._package_manager.getInstalledPackageInfo(package_id) - if local_package is None: - return False - - bundled_package = self._package_manager.getBundledPackageInfo(package_id) - if bundled_package is None: - return False - - local_version = Version(local_package["package_version"]) - bundled_version = Version(bundled_package["package_version"]) - return bundled_version < local_version - - @pyqtSlot(str, result = bool) - def isInstalled(self, package_id: str) -> bool: - result = self._package_manager.isPackageInstalled(package_id) - # Also check the old plugins list if it's not found in the package manager. - if not result: - result = self.isOldPlugin(package_id) - return result - - @pyqtSlot(str, result = int) - def getNumberOfInstalledPackagesByAuthor(self, author_id: str) -> int: - count = 0 - for package in self._materials_installed_model.items: - if package["author_id"] == author_id: - count += 1 - return count - - # This slot is only used to get the number of material packages by author, not any other type of packages. - @pyqtSlot(str, result = int) - def getTotalNumberOfMaterialPackagesByAuthor(self, author_id: str) -> int: - count = 0 - for package in self._server_response_data["packages"]: - if package["package_type"] == "material": - if package["author"]["author_id"] == author_id: - count += 1 - return count - - @pyqtSlot(str, result = bool) - def isEnabled(self, package_id: str) -> bool: - return package_id in self._plugin_registry.getActivePlugins() - - # Check for plugins that were installed with the old plugin browser - def isOldPlugin(self, plugin_id: str) -> bool: - return plugin_id in self._old_plugin_ids - - def getOldPluginPackageMetadata(self, plugin_id: str) -> Optional[Dict[str, Any]]: - return self._old_plugin_metadata.get(plugin_id) - - def isLoadingComplete(self) -> bool: - populated = 0 - for metadata_list in self._server_response_data.items(): - if metadata_list: - populated += 1 - return populated == len(self._server_response_data.items()) - - # Make API Calls - # -------------------------------------------------------------------------- - def _makeRequestByType(self, request_type: str) -> None: - Logger.debug(f"Requesting {request_type} metadata from server.") - url = self._request_urls[request_type] - - callback = lambda r, rt = request_type: self._onDataRequestFinished(rt, r) - error_callback = lambda r, e, rt = request_type: self._onDataRequestError(rt, r, e) - self._application.getHttpRequestManager().get(url, - callback = callback, - error_callback = error_callback, - scope=self._json_scope) - - @pyqtSlot(str) - def startDownload(self, url: str) -> None: - Logger.info(f"Attempting to download & install package from {url}.") - - callback = lambda r: self._onDownloadFinished(r) - error_callback = lambda r, e: self._onDownloadFailed(r, e) - download_progress_callback = self._onDownloadProgress - request_data = self._application.getHttpRequestManager().get(url, - callback = callback, - error_callback = error_callback, - download_progress_callback = download_progress_callback, - scope=self._cloud_scope - ) - - self._download_request_data = request_data - self.setDownloadProgress(0) - self.setIsDownloading(True) - - @pyqtSlot() - def cancelDownload(self) -> None: - Logger.info(f"User cancelled the download of a package. request {self._download_request_data}") - if self._download_request_data is not None: - self._application.getHttpRequestManager().abortRequest(self._download_request_data) - self._download_request_data = None - self.resetDownload() - - def resetDownload(self) -> None: - self.setDownloadProgress(0) - self.setIsDownloading(False) - - # Handlers for Network Events - # -------------------------------------------------------------------------- - def _onDataRequestError(self, request_type: str, reply: "QNetworkReply", error: "QNetworkReply.NetworkError") -> None: - Logger.error(f"Request {request_type} failed due to error {error}: {reply.errorString()}") - self.setViewPage("errored") - - def _onDataRequestFinished(self, request_type: str, reply: "QNetworkReply") -> None: - if reply.operation() != QNetworkAccessManager.GetOperation: - Logger.log("e", "_onDataRequestFinished() only handles GET requests but got [%s] instead", reply.operation()) - return - - http_status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) - if http_status_code != 200: - Logger.log("e", "Request type [%s] got non-200 HTTP response: [%s]", http_status_code) - self.setViewPage("errored") - return - - data = bytes(reply.readAll()) - try: - json_data = json.loads(data.decode("utf-8")) - except json.decoder.JSONDecodeError: - Logger.log("e", "Failed to decode response data as JSON for request type [%s], response data [%s]", - request_type, data) - self.setViewPage("errored") - return - - # Check for errors: - if "errors" in json_data: - for error in json_data["errors"]: - Logger.log("e", "Request type [%s] got response showing error: %s", error.get("title", "No error title found")) - self.setViewPage("errored") - return - - # Create model and apply metadata: - if not self._models[request_type]: - Logger.log("e", "Could not find the model for request type [%s].", request_type) - self.setViewPage("errored") - return - - self._server_response_data[request_type] = json_data["data"] - self._models[request_type].setMetadata(self._server_response_data[request_type]) - - if request_type == "packages": - self._models[request_type].setFilter({"type": "plugin"}) - self.reBuildMaterialsModels() - self.reBuildPluginsModels() - self._notifyPackageManager() - elif request_type == "authors": - self._models[request_type].setFilter({"package_types": "material"}) - 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) - - self.metadataChanged.emit() - - if self.isLoadingComplete(): - self.setViewPage("overview") - - # This function goes through all known remote versions of a package and notifies the package manager of this change - def _notifyPackageManager(self): - for package in self._server_response_data["packages"]: - self._package_manager.addAvailablePackageVersion(package["package_id"], Version(package["package_version"])) - - def _onDownloadFinished(self, reply: "QNetworkReply") -> None: - self.resetDownload() - - if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) != 200: - try: - reply_error = json.loads(reply.readAll().data().decode("utf-8")) - except Exception as e: - reply_error = str(e) - Logger.log("w", "Failed to download package. The following error was returned: %s", reply_error) - return - # Must not delete the temporary file on Windows - self._temp_plugin_file = tempfile.NamedTemporaryFile(mode = "w+b", suffix = ".curapackage", delete = False) - file_path = self._temp_plugin_file.name - # Write first and close, otherwise on Windows, it cannot read the file - self._temp_plugin_file.write(reply.readAll()) - self._temp_plugin_file.close() - self._onDownloadComplete(file_path) - - def _onDownloadFailed(self, reply: "QNetworkReply", error: "QNetworkReply.NetworkError") -> None: - Logger.log("w", "Failed to download package. The following error was returned: %s", error) - - self.resetDownload() - - def _onDownloadProgress(self, bytes_sent: int, bytes_total: int) -> None: - if bytes_total > 0: - new_progress = bytes_sent / bytes_total * 100 - self.setDownloadProgress(new_progress) - Logger.log("d", "new download progress %s / %s : %s%%", bytes_sent, bytes_total, new_progress) - - def _onDownloadComplete(self, file_path: str) -> None: - Logger.log("i", "Download complete.") - package_info = self._package_manager.getPackageInfo(file_path) - if not package_info: - Logger.log("w", "Package file [%s] was not a valid CuraPackage.", file_path) - return - package_id = package_info["package_id"] - - try: - license_content = self._package_manager.getPackageLicense(file_path) - except EnvironmentError as e: - Logger.error(f"Could not open downloaded package {package_id} to read license file! {type(e)} - {e}") - return - if license_content is not None: - # get the icon url for package_id, make sure the result is a string, never None - icon_url = next((x["icon_url"] for x in self.packagesModel.items if x["id"] == package_id), None) or "" - self.openLicenseDialog(package_info["display_name"], license_content, file_path, icon_url) - return - - installed_id = self.install(file_path) - if installed_id != package_id: - Logger.error("Installed package {} does not match {}".format(installed_id, package_id)) - - # Getter & Setters for Properties: - # -------------------------------------------------------------------------- - def setDownloadProgress(self, progress: float) -> None: - if progress != self._download_progress: - self._download_progress = progress - self.onDownloadProgressChanged.emit() - - @pyqtProperty(int, fset = setDownloadProgress, notify = onDownloadProgressChanged) - def downloadProgress(self) -> float: - return self._download_progress - - def setIsDownloading(self, is_downloading: bool) -> None: - if self._is_downloading != is_downloading: - self._is_downloading = is_downloading - self.onIsDownloadingChanged.emit() - - @pyqtProperty(bool, fset = setIsDownloading, notify = onIsDownloadingChanged) - def isDownloading(self) -> bool: - return self._is_downloading - - def setActivePackage(self, package: QObject) -> None: - if self._active_package != package: - self._active_package = package - self.activePackageChanged.emit() - - @pyqtProperty(QObject, fset = setActivePackage, notify = activePackageChanged) - def activePackage(self) -> Optional[QObject]: - """The active package is the package that is currently being downloaded""" - - return self._active_package - - def setViewCategory(self, category: str = "plugin") -> None: - if self._view_category != category: - self._view_category = category - self.viewChanged.emit() - - # Function explicitly defined so that it can be called through the callExtensionsMethod - # which cannot receive arguments. - def setViewCategoryToMaterials(self) -> None: - self.setViewCategory("material") - - @pyqtProperty(str, fset = setViewCategory, notify = viewChanged) - def viewCategory(self) -> str: - return self._view_category - - def setViewPage(self, page: str = "overview") -> None: - if self._view_page != page: - self._view_page = page - self.viewChanged.emit() - - @pyqtProperty(str, fset = setViewPage, notify = viewChanged) - def viewPage(self) -> str: - return self._view_page - - # Exposed Models: - # -------------------------------------------------------------------------- - @pyqtProperty(QObject, constant = True) - def authorsModel(self) -> AuthorsModel: - return cast(AuthorsModel, self._models["authors"]) - - @pyqtProperty(QObject, constant = True) - def packagesModel(self) -> PackagesModel: - return cast(PackagesModel, self._models["packages"]) - - @pyqtProperty(QObject, constant = True) - def pluginsShowcaseModel(self) -> PackagesModel: - return self._plugins_showcase_model - - @pyqtProperty(QObject, constant = True) - def pluginsAvailableModel(self) -> PackagesModel: - return self._plugins_available_model - - @pyqtProperty(QObject, constant = True) - def pluginsInstalledModel(self) -> PackagesModel: - return self._plugins_installed_model - - @pyqtProperty(QObject, constant = True) - def pluginsBundledModel(self) -> PackagesModel: - return self._plugins_bundled_model - - @pyqtProperty(QObject, constant = True) - def materialsShowcaseModel(self) -> AuthorsModel: - return self._materials_showcase_model - - @pyqtProperty(QObject, constant = True) - def materialsAvailableModel(self) -> AuthorsModel: - return self._materials_available_model - - @pyqtProperty(QObject, constant = True) - def materialsInstalledModel(self) -> PackagesModel: - return self._materials_installed_model - - @pyqtProperty(QObject, constant = True) - def materialsBundledModel(self) -> PackagesModel: - return self._materials_bundled_model - - @pyqtProperty(QObject, constant = True) - def materialsGenericModel(self) -> PackagesModel: - return self._materials_generic_model - - @pyqtSlot(str, result = str) - def getWebMarketplaceUrl(self, page: str) -> str: - root = CuraMarketplaceRoot - if root == "": - root = DEFAULT_MARKETPLACE_ROOT - return root + "/app/cura/" + page - - # Filter Models: - # -------------------------------------------------------------------------- - @pyqtSlot(str, str, str) - def filterModelByProp(self, model_type: str, filter_type: str, parameter: str) -> None: - if not self._models[model_type]: - Logger.log("w", "Couldn't filter %s model because it doesn't exist.", model_type) - return - self._models[model_type].setFilter({filter_type: parameter}) - self.filterChanged.emit() - - @pyqtSlot(str, "QVariantMap") - def setFilters(self, model_type: str, filter_dict: dict) -> None: - if not self._models[model_type]: - Logger.log("w", "Couldn't filter %s model because it doesn't exist.", model_type) - return - self._models[model_type].setFilter(filter_dict) - self.filterChanged.emit() - - @pyqtSlot(str) - def removeFilters(self, model_type: str) -> None: - if not self._models[model_type]: - Logger.log("w", "Couldn't remove filters on %s model because it doesn't exist.", model_type) - return - self._models[model_type].setFilter({}) - self.filterChanged.emit() - - # HACK(S): - # -------------------------------------------------------------------------- - def reBuildMaterialsModels(self) -> None: - materials_showcase_metadata = [] - materials_available_metadata = [] - materials_generic_metadata = [] - - processed_authors = [] # type: List[str] - - for item in self._server_response_data["packages"]: - if item["package_type"] == "material": - - author = item["author"] - if author["author_id"] in processed_authors: - continue - - # Generic materials to be in the same section - if "generic" in item["tags"]: - materials_generic_metadata.append(item) - else: - if "showcase" in item["tags"]: - materials_showcase_metadata.append(author) - else: - materials_available_metadata.append(author) - - processed_authors.append(author["author_id"]) - - self._materials_showcase_model.setMetadata(materials_showcase_metadata) - self._materials_available_model.setMetadata(materials_available_metadata) - self._materials_generic_model.setMetadata(materials_generic_metadata) - - def reBuildPluginsModels(self) -> None: - plugins_showcase_metadata = [] - plugins_available_metadata = [] - - for item in self._server_response_data["packages"]: - if item["package_type"] == "plugin": - if "showcase" in item["tags"]: - plugins_showcase_metadata.append(item) - else: - plugins_available_metadata.append(item) - - self._plugins_showcase_model.setMetadata(plugins_showcase_metadata) - self._plugins_available_model.setMetadata(plugins_available_metadata) diff --git a/plugins/Toolbox/src/__init__.py b/plugins/Toolbox/src/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 --- a/plugins/Toolbox/src/__init__.py +++ /dev/null diff --git a/resources/bundled_packages/cura.json b/resources/bundled_packages/cura.json index d1914c68ce..501445f9d8 100644 --- a/resources/bundled_packages/cura.json +++ b/resources/bundled_packages/cura.json @@ -526,13 +526,13 @@ } } }, - "Toolbox": { + "Marketplace": { "package_info": { - "package_id": "Toolbox", + "package_id": "Marketplace", "package_type": "plugin", - "display_name": "Toolbox", + "display_name": "Marketplace", "description": "Find, manage and install new Cura packages.", - "package_version": "1.0.1", + "package_version": "1.0.0", "sdk_version": "7.9.0", "website": "https://ultimaker.com", "author": { diff --git a/resources/qml/ActionButton.qml b/resources/qml/ActionButton.qml index c84018fb07..1c231e5ef3 100644 --- a/resources/qml/ActionButton.qml +++ b/resources/qml/ActionButton.qml @@ -15,6 +15,7 @@ Button property bool isIconOnRightSide: false property alias iconSource: buttonIconLeft.source + property real iconSize: UM.Theme.getSize("action_button_icon").height property alias textFont: buttonText.font property alias cornerRadius: backgroundRect.radius property alias tooltip: tooltip.tooltipText @@ -109,7 +110,7 @@ Button { id: buttonIconLeft source: "" - height: visible ? UM.Theme.getSize("action_button_icon").height : 0 + height: visible ? button.iconSize : 0 width: visible ? height : 0 sourceSize.width: width sourceSize.height: height @@ -158,7 +159,7 @@ Button { id: buttonIconRight source: buttonIconLeft.source - height: visible ? UM.Theme.getSize("action_button_icon").height : 0 + height: visible ? button.iconSize : 0 width: visible ? height : 0 sourceSize.width: width sourceSize.height: height diff --git a/resources/qml/Actions.qml b/resources/qml/Actions.qml index 4ec2f03260..e3f3947bfe 100644 --- a/resources/qml/Actions.qml +++ b/resources/qml/Actions.qml @@ -72,6 +72,7 @@ Item property alias configureSettingVisibility: configureSettingVisibilityAction property alias browsePackages: browsePackagesAction + property alias openMarketplace: openMarketplaceAction UM.I18nCatalog{id: catalog; name: "cura"} @@ -483,4 +484,11 @@ Item text: "&Marketplace" iconName: "plugins_browse" } + + Action + { + id: openMarketplaceAction + text: catalog.i18nc("@action:menu", "&Marketplace") + iconName: "plugins_browse" + } } diff --git a/resources/qml/MainWindow/ApplicationMenu.qml b/resources/qml/MainWindow/ApplicationMenu.qml index 62b3a71ee8..497c5e1541 100644 --- a/resources/qml/MainWindow/ApplicationMenu.qml +++ b/resources/qml/MainWindow/ApplicationMenu.qml @@ -196,13 +196,13 @@ Item } } - // show the Toolbox + // show the Marketplace Connections { - target: Cura.Actions.browsePackages + target: Cura.Actions.openMarketplace function onTriggered() { - curaExtensions.callExtensionMethod("Toolbox", "launch") + curaExtensions.callExtensionMethod("Marketplace", "show") } } @@ -212,8 +212,8 @@ Item target: Cura.Actions.marketplaceMaterials function onTriggered() { - curaExtensions.callExtensionMethod("Toolbox", "launch") - curaExtensions.callExtensionMethod("Toolbox", "setViewCategoryToMaterials") + curaExtensions.callExtensionMethod("Marketplace", "show") + curaExtensions.callExtensionMethod("Marketplace", "setVisibleTabToMaterials") } } -}
\ No newline at end of file +} diff --git a/resources/qml/MainWindow/MainWindowHeader.qml b/resources/qml/MainWindow/MainWindowHeader.qml index 815ddff732..16d7d69062 100644 --- a/resources/qml/MainWindow/MainWindowHeader.qml +++ b/resources/qml/MainWindow/MainWindowHeader.qml @@ -83,19 +83,31 @@ Item ExclusiveGroup { id: mainWindowHeaderMenuGroup } } - // Shortcut button to quick access the Toolbox Controls2.Button { id: marketplaceButton - text: catalog.i18nc("@action:button", "Marketplace") height: Math.round(0.5 * UM.Theme.getSize("main_window_header").height) - onClicked: Cura.Actions.browsePackages.trigger() + anchors + { + verticalCenter: parent.verticalCenter + right: applicationSwitcher.left + rightMargin: UM.Theme.getSize("default_margin").width + } hoverEnabled: true + onClicked: Cura.Actions.openMarketplace.trigger() + + contentItem: Label + { + text: "Marketplace" //Ultimaker considers this a product name, so it shouldn't be translated. + font: UM.Theme.getFont("default") + color: UM.Theme.getColor("primary_text") + width: contentWidth + verticalAlignment: Text.AlignVCenter + } background: Rectangle { - id: marketplaceButtonBorder radius: UM.Theme.getSize("action_button_radius").width color: UM.Theme.getColor("main_window_header_background") border.width: UM.Theme.getSize("default_lining").width @@ -103,7 +115,6 @@ Item Rectangle { - id: marketplaceButtonFill anchors.fill: parent radius: parent.radius color: UM.Theme.getColor("primary_text") @@ -112,33 +123,12 @@ Item } } - contentItem: Label - { - id: label - text: marketplaceButton.text - font: UM.Theme.getFont("default") - color: UM.Theme.getColor("primary_text") - width: contentWidth - verticalAlignment: Text.AlignVCenter - renderType: Text.NativeRendering - } - - anchors - { - right: applicationSwitcher.left - rightMargin: UM.Theme.getSize("default_margin").width - verticalCenter: parent.verticalCenter - } - Cura.NotificationIcon { - id: marketplaceNotificationIcon anchors { - top: parent.top - right: parent.right - rightMargin: (-0.5 * width) | 0 - topMargin: (-0.5 * height) | 0 + horizontalCenter: parent.right + verticalCenter: parent.top } visible: CuraApplication.getPackageManager().packagesWithUpdate.length > 0 diff --git a/resources/qml/SearchBar.qml b/resources/qml/SearchBar.qml new file mode 100644 index 0000000000..4d9c003653 --- /dev/null +++ b/resources/qml/SearchBar.qml @@ -0,0 +1,35 @@ +// 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.7 as Cura + +Cura.TextField +{ + UM.I18nCatalog { id: catalog; name: "cura" } + + leftPadding: searchIcon.width + UM.Theme.getSize("default_margin").width * 2 + + placeholderText: catalog.i18nc("@placeholder", "Search") + font.italic: true + + UM.RecolorImage + { + id: searchIcon + + anchors + { + verticalCenter: parent.verticalCenter + left: parent.left + leftMargin: UM.Theme.getSize("default_margin").width + } + source: UM.Theme.getIcon("Magnifier") + height: UM.Theme.getSize("small_button_icon").height + width: height + color: UM.Theme.getColor("text") + } +} diff --git a/resources/qml/Settings/SettingView.qml b/resources/qml/Settings/SettingView.qml index cb96728973..657edd8259 100644 --- a/resources/qml/Settings/SettingView.qml +++ b/resources/qml/Settings/SettingView.qml @@ -41,39 +41,19 @@ Item repeat: false } - Cura.TextField + Cura.SearchBar { id: filter height: parent.height anchors.left: parent.left anchors.right: parent.right - leftPadding: searchIcon.width + UM.Theme.getSize("default_margin").width * 2 - placeholderText: catalog.i18nc("@label:textbox", "Search settings") - font.italic: true + + placeholderText: catalog.i18nc("@label:textbox", "Search settings") // Overwrite property var expandedCategories property bool lastFindingSettings: false - UM.RecolorImage - { - id: searchIcon - - anchors - { - verticalCenter: parent.verticalCenter - left: parent.left - leftMargin: UM.Theme.getSize("default_margin").width - } - source: UM.Theme.getIcon("search") - height: UM.Theme.getSize("small_button_icon").height - width: height - color: UM.Theme.getColor("text") - } - - onTextChanged: - { - settingsSearchTimer.restart() - } + onTextChanged: settingsSearchTimer.restart() onEditingFinished: { @@ -86,10 +66,7 @@ Item } } - Keys.onEscapePressed: - { - filter.text = "" - } + Keys.onEscapePressed: filter.text = "" function updateDefinitionModel() { diff --git a/resources/qml/TertiaryButton.qml b/resources/qml/TertiaryButton.qml index 76684b6ef2..8171188232 100644 --- a/resources/qml/TertiaryButton.qml +++ b/resources/qml/TertiaryButton.qml @@ -16,4 +16,5 @@ Cura.ActionButton textDisabledColor: UM.Theme.getColor("action_button_disabled_text") hoverColor: "transparent" underlineTextOnHover: true + iconSize: UM.Theme.getSize("action_button_icon_small").height } diff --git a/resources/qml/ToolTip.qml b/resources/qml/ToolTip.qml index 3157f81d89..c4edc5a361 100644 --- a/resources/qml/ToolTip.qml +++ b/resources/qml/ToolTip.qml @@ -38,7 +38,7 @@ ToolTip onAboutToHide: hide() // If the text is not set, just set the height to 0 to prevent it from showing - height: text != "" ? label.contentHeight + 2 * UM.Theme.getSize("thin_margin").width: 0 + height: label.contentHeight + 2 * UM.Theme.getSize("thin_margin").width x: { @@ -74,7 +74,7 @@ ToolTip } function show() { - opacity = 1 + opacity = text != "" ? 1 : 0 } function hide() { diff --git a/resources/qml/Widgets/ScrollView.qml b/resources/qml/Widgets/ScrollView.qml index 9e7531994c..deaecb5dfb 100644 --- a/resources/qml/Widgets/ScrollView.qml +++ b/resources/qml/Widgets/ScrollView.qml @@ -1,5 +1,5 @@ // Copyright (c) 2020 Ultimaker B.V. -// Toolbox is released under the terms of the LGPLv3 or higher. +// Cura is released under the terms of the LGPLv3 or higher. import QtQuick 2.10 import QtQuick.Controls 2.3 diff --git a/resources/themes/cura-dark/theme.json b/resources/themes/cura-dark/theme.json index 520f863972..a81dcadb5c 100644 --- a/resources/themes/cura-dark/theme.json +++ b/resources/themes/cura-dark/theme.json @@ -6,6 +6,7 @@ "colors": { "main_background": [39, 44, 48, 255], + "detail_background": [63, 63, 63, 255], "message_background": [39, 44, 48, 255], "wide_lining": [31, 36, 39, 255], "thick_lining": [255, 255, 255, 60], @@ -175,10 +176,6 @@ "quality_slider_available": [255, 255, 255, 255], - "toolbox_header_button_text_active": [255, 255, 255, 255], - "toolbox_header_button_text_inactive": [128, 128, 128, 255], - "toolbox_premium_packages_background": [57, 57, 57, 255], - "monitor_printer_family_tag": [86, 86, 106, 255], "monitor_text_disabled": [102, 102, 102, 255], "monitor_icon_primary": [229, 229, 229, 255], diff --git a/resources/themes/cura-light/icons/default/ArrowDown.svg b/resources/themes/cura-light/icons/default/ArrowDown.svg new file mode 100644 index 0000000000..ab5ea8e076 --- /dev/null +++ b/resources/themes/cura-light/icons/default/ArrowDown.svg @@ -0,0 +1,3 @@ +<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> +<path d="M12 21L4.293 13.293L5.707 11.8789L11 17.1719V3H13V17.1719L18.293 11.8789L19.707 13.293L12 21Z" /> +</svg> diff --git a/resources/themes/cura-light/icons/default/ArrowLeft.svg b/resources/themes/cura-light/icons/default/ArrowLeft.svg new file mode 100644 index 0000000000..d722b8ae8d --- /dev/null +++ b/resources/themes/cura-light/icons/default/ArrowLeft.svg @@ -0,0 +1,3 @@ +<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> +<path d="M10.707 4.293L12.1211 5.707L6.8281 11H21V13H6.8281L12.1211 18.293L10.707 19.707L3 12L10.707 4.293Z" /> +</svg> diff --git a/resources/themes/cura-light/icons/default/Certified.svg b/resources/themes/cura-light/icons/default/Certified.svg new file mode 100644 index 0000000000..031011213a --- /dev/null +++ b/resources/themes/cura-light/icons/default/Certified.svg @@ -0,0 +1,3 @@ +<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> +<path d="M12.0005 22.0122H12C11.07 21.9324 10.1927 21.5466 9.50539 20.915C9.21375 20.6772 8.89653 20.4726 8.55959 20.305C8.20295 20.1856 7.83416 20.1061 7.45999 20.0679C6.5274 20.0285 5.63416 19.6811 4.91999 19.0801C4.33502 18.3806 3.9948 17.509 3.95119 16.5981C3.90897 16.2037 3.82297 15.8153 3.69479 15.44C3.52022 15.0837 3.3065 14.748 3.05759 14.4391C2.44454 13.7645 2.06886 12.9079 1.98779 12C2.06757 11.07 2.45344 10.1927 3.08499 9.5054C3.32281 9.21376 3.5274 8.89653 3.69499 8.5596C3.81442 8.20295 3.89394 7.83417 3.93209 7.46C3.97149 6.52741 4.31886 5.63417 4.91989 4.92C5.61927 4.33475 6.49098 3.99449 7.40189 3.9512C7.79626 3.90907 8.1847 3.82308 8.55999 3.6948C8.91626 3.52044 9.25197 3.30689 9.56089 3.0581C10.2354 2.44481 11.092 2.06894 12 1.9878H12.001C12.9306 2.0678 13.8076 2.45365 14.4946 3.085C14.7862 3.32281 15.1035 3.52741 15.4404 3.695C15.7969 3.81446 16.1656 3.89405 16.5396 3.9323C17.4722 3.97169 18.3654 4.31907 19.0796 4.9201C19.6646 5.61964 20.0048 6.49125 20.0484 7.4021C20.0908 7.7964 20.1769 8.18476 20.3052 8.56C20.4798 8.91626 20.6935 9.25196 20.9424 9.5609C21.5557 10.2357 21.9315 11.0926 22.0124 12.0009C21.9325 12.9307 21.5466 13.8078 20.9152 14.495C20.6774 14.7866 20.4728 15.1039 20.3052 15.4408C20.1857 15.7973 20.1061 16.166 20.0679 16.54C20.0285 17.4726 19.6811 18.3658 19.0801 19.08C18.3807 19.6653 17.509 20.0055 16.5981 20.0488C16.2038 20.0912 15.8155 20.1775 15.4404 20.3061C15.0841 20.4805 14.7484 20.694 14.4395 20.9428C13.7649 21.5557 12.9084 21.9313 12.0005 22.0122ZM12 3.9878C11.5315 4.1067 11.1003 4.34102 10.7456 4.6694C10.309 5.01634 9.83223 5.30948 9.32559 5.5425C8.80239 5.73609 8.25776 5.86593 7.70349 5.9292C7.22072 5.94798 6.7503 6.08713 6.33499 6.334C6.07267 6.76174 5.9264 7.25048 5.91059 7.752C5.8537 8.28949 5.73005 8.81779 5.54249 9.3247C5.31656 9.81621 5.03019 10.2776 4.68999 10.6982C4.34665 11.0638 4.10465 11.5127 3.98789 12.0005C4.10683 12.4686 4.34097 12.8994 4.66899 13.2539C5.01645 13.6907 5.30978 14.168 5.54259 14.6753C5.73618 15.1983 5.86602 15.7428 5.92929 16.2969C5.94811 16.7796 6.08744 17.2499 6.33459 17.665C6.76211 17.9274 7.25071 18.0737 7.75209 18.0894C8.28958 18.1463 8.81789 18.2699 9.32479 18.4575C9.81631 18.6835 10.2777 18.9699 10.6983 19.3101C11.0639 19.6534 11.5128 19.8954 12.0006 20.0122C12.4691 19.8933 12.9003 19.659 13.255 19.3306C13.6916 18.9837 14.1684 18.6905 14.675 18.4575C15.1982 18.2639 15.7428 18.1341 16.2971 18.0708C16.7797 18.052 17.25 17.9128 17.6652 17.666C17.9275 17.2383 18.0738 16.7495 18.0896 16.248C18.1465 15.7105 18.2701 15.1822 18.4577 14.6753C18.6837 14.1838 18.9701 13.7224 19.3103 13.3018C19.6535 12.9363 19.8954 12.4876 20.0122 12C19.8933 11.5319 19.6591 11.1011 19.3311 10.7466C18.9836 10.3098 18.6903 9.83249 18.4575 9.3252C18.2639 8.80217 18.1341 8.25771 18.0708 7.7036C18.052 7.22089 17.9126 6.75057 17.6655 6.3355C17.238 6.0731 16.7494 5.92681 16.248 5.9111C15.7105 5.85421 15.1822 5.73055 14.6753 5.543C14.1838 5.31692 13.7224 5.03037 13.3018 4.69C12.9363 4.3467 12.4876 4.10467 12 3.9878ZM15.5361 15.5361L14.1221 14.1211C13.7026 14.5407 13.168 14.8265 12.5861 14.9423C12.0041 15.0581 11.4009 14.9987 10.8526 14.7717C10.3044 14.5447 9.83586 14.1601 9.50619 13.6668C9.17652 13.1734 9.00055 12.5934 9.00055 12C9.00055 11.4066 9.17652 10.8266 9.50619 10.3332C9.83586 9.83986 10.3044 9.45535 10.8526 9.2283C11.4009 9.00126 12.0041 8.94188 12.5861 9.05769C13.168 9.1735 13.7026 9.45928 14.1221 9.8789L15.5361 8.4639C14.8368 7.7648 13.9458 7.28875 12.976 7.09596C12.0061 6.90316 11.0009 7.00227 10.0873 7.38076C9.1738 7.75924 8.393 8.4001 7.84367 9.22232C7.29433 10.0445 7.00113 11.0112 7.00113 12C7.00113 12.9888 7.29433 13.9555 7.84367 14.7777C8.393 15.5999 9.1738 16.2408 10.0873 16.6192C11.0009 16.9977 12.0061 17.0968 12.976 16.904C13.9458 16.7112 14.8368 16.2352 15.5361 15.5361Z" /> +</svg> diff --git a/resources/themes/cura-light/icons/default/DocumentFilled.svg b/resources/themes/cura-light/icons/default/DocumentFilled.svg new file mode 100644 index 0000000000..bb654fea33 --- /dev/null +++ b/resources/themes/cura-light/icons/default/DocumentFilled.svg @@ -0,0 +1,3 @@ +<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> +<path d="M14 2H7C6.20435 2 5.44129 2.31607 4.87868 2.87868C4.31607 3.44129 4 4.20435 4 5V19C4 19.7956 4.31607 20.5587 4.87868 21.1213C5.44129 21.6839 6.20435 22 7 22H17C17.7956 22 18.5587 21.6839 19.1213 21.1213C19.6839 20.5587 20 19.7956 20 19V8L14 2ZM14 4.8284L17.1716 8H14V4.8284ZM18 20H6V4H12V10H18V20ZM16 14H8V12H16V14ZM13 18H8V16H13V18Z" /> +</svg> diff --git a/resources/themes/cura-light/icons/default/Download.svg b/resources/themes/cura-light/icons/default/Download.svg new file mode 100644 index 0000000000..cbe0da2a99 --- /dev/null +++ b/resources/themes/cura-light/icons/default/Download.svg @@ -0,0 +1,3 @@ +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M18.707 10.293L12 17L5.293 10.293L6.707 8.8789L11 13.1719V3H13V13.1719L17.293 8.8789L18.707 10.293ZM21 19H3V21H21V19Z" fill="#000E1A"/> +</svg> diff --git a/resources/themes/cura-light/icons/default/Globe.svg b/resources/themes/cura-light/icons/default/Globe.svg new file mode 100644 index 0000000000..4d955e9615 --- /dev/null +++ b/resources/themes/cura-light/icons/default/Globe.svg @@ -0,0 +1,3 @@ +<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> +<path d="M12 2C10.0222 2 8.08879 2.58649 6.4443 3.6853C4.79981 4.78412 3.51809 6.3459 2.76121 8.17317C2.00433 10.0004 1.8063 12.0111 2.19215 13.9509C2.578 15.8907 3.53041 17.6725 4.92894 19.0711C6.32746 20.4696 8.10929 21.422 10.0491 21.8079C11.9889 22.1937 13.9996 21.9957 15.8268 21.2388C17.6541 20.4819 19.2159 19.2002 20.3147 17.5557C21.4135 15.9112 22 13.9778 22 12C22 10.6868 21.7413 9.38642 21.2388 8.17317C20.7363 6.95991 19.9997 5.85752 19.0711 4.92893C18.1425 4.00035 17.0401 3.26375 15.8268 2.7612C14.6136 2.25866 13.3132 2 12 2ZM19.9309 11H16.9594C16.8101 8.655 16.0204 6.39588 14.6763 4.4685C16.0708 4.96434 17.3003 5.83742 18.228 6.99057C19.1557 8.14372 19.7452 9.5317 19.9309 11ZM9.06 13H14.94C14.7622 15.4921 13.7228 17.8448 12 19.6543C10.2772 17.8448 9.2378 15.4921 9.06 13ZM9.06 11C9.23776 8.50785 10.2772 6.15522 12 4.3457C13.7226 6.15537 14.762 8.50792 14.94 11H9.06ZM9.3241 4.4685C7.9799 6.39584 7.19008 8.65496 7.0406 11H4.0691C4.25479 9.5317 4.8443 8.14372 5.77201 6.99057C6.69972 5.83742 7.92924 4.96434 9.3237 4.4685H9.3241ZM4.0691 13H7.0406C7.18996 15.345 7.97964 17.6041 9.3237 19.5315C7.92924 19.0357 6.69972 18.1626 5.77201 17.0094C4.8443 15.8563 4.25479 14.4683 4.0691 13ZM14.6763 19.5315C16.0204 17.6041 16.8101 15.345 16.9594 13H19.9309C19.7452 14.4683 19.1557 15.8563 18.228 17.0094C17.3003 18.1626 16.0708 19.0357 14.6763 19.5315Z" /> +</svg>
\ No newline at end of file diff --git a/resources/themes/cura-light/icons/default/Settings.svg b/resources/themes/cura-light/icons/default/Settings.svg new file mode 100644 index 0000000000..feb0ab0cc8 --- /dev/null +++ b/resources/themes/cura-light/icons/default/Settings.svg @@ -0,0 +1,3 @@ +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M19 12C19 11.895 18.9888 11.7928 18.9843 11.6889L21.2354 9.9966L18.3525 5.0034L15.7525 6.1005C15.5837 5.9927 15.4112 5.8905 15.2325 5.7972L14.8828 3H9.1172L8.7672 5.7972C8.5855 5.8923 8.4096 5.9964 8.2372 6.1066L5.6472 5.0034L2.7646 9.9966L5.0152 11.7C5.0109 11.8 5 11.8988 5 12C5 12.105 5.0112 12.2072 5.0157 12.3111L2.7646 14.0034L5.6475 18.9966L8.2475 17.8994C8.4163 18.0072 8.5888 18.1094 8.7675 18.2028L9.1172 21H14.8828L15.2328 18.2028C15.4145 18.1077 15.5904 18.0036 15.7628 17.8934L18.3528 18.9966L21.2357 14.0034L18.9848 12.3C18.9891 12.2 19 12.1012 19 12ZM18.62 14.5327L17.5028 16.4673L15.4513 15.6018C14.8702 16.1531 14.1648 16.5564 13.3949 16.7773L13.1172 19H10.8828L10.605 16.7773C9.83505 16.5564 9.12968 16.1531 8.5486 15.6018L6.4971 16.4673L5.38 14.5327L7.1579 13.1865C6.94752 12.4095 6.94752 11.5905 7.1579 10.8135L5.38 9.4673L6.4971 7.5327L8.5486 8.3982C9.12968 7.84686 9.83505 7.44364 10.605 7.2227L10.8828 5H13.1172L13.395 7.2227C14.1649 7.44364 14.8703 7.84686 15.4514 8.3982L17.5029 7.5327L18.62 9.4673L16.8421 10.8135C17.0525 11.5905 17.0525 12.4095 16.8421 13.1865L18.62 14.5327ZM12 9C11.4067 9 10.8266 9.17595 10.3333 9.50559C9.83994 9.83524 9.45542 10.3038 9.22836 10.8519C9.0013 11.4001 8.94189 12.0033 9.05764 12.5853C9.1734 13.1672 9.45912 13.7018 9.87868 14.1213C10.2982 14.5409 10.8328 14.8266 11.4147 14.9424C11.9967 15.0581 12.5999 14.9987 13.148 14.7716C13.6962 14.5446 14.1648 14.1601 14.4944 13.6667C14.8241 13.1734 15 12.5933 15 12C14.9991 11.2046 14.6828 10.4421 14.1204 9.87964C13.5579 9.31722 12.7954 9.00087 12 9ZM12 13C11.8022 13 11.6089 12.9414 11.4444 12.8315C11.28 12.7216 11.1518 12.5654 11.0761 12.3827C11.0004 12.2 10.9806 11.9989 11.0192 11.8049C11.0578 11.6109 11.153 11.4327 11.2929 11.2929C11.4327 11.153 11.6109 11.0578 11.8049 11.0192C11.9989 10.9806 12.2 11.0004 12.3827 11.0761C12.5654 11.1518 12.7216 11.28 12.8315 11.4444C12.9413 11.6089 13 11.8022 13 12C12.9998 12.2651 12.8943 12.5194 12.7068 12.7068C12.5194 12.8943 12.2651 12.9998 12 13Z" fill="#000E1A"/> +</svg> diff --git a/resources/themes/cura-light/icons/default/ShoppingCart.svg b/resources/themes/cura-light/icons/default/ShoppingCart.svg new file mode 100644 index 0000000000..b3fece3fab --- /dev/null +++ b/resources/themes/cura-light/icons/default/ShoppingCart.svg @@ -0,0 +1,3 @@ +<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> +<path d="M5.99631 13C5.99631 13.7956 6.31238 14.5587 6.87499 15.1213C7.4376 15.6839 8.20066 16 8.99631 16H17.9963C18.792 16 19.555 15.6839 20.1176 15.1213C20.6802 14.5587 20.9963 13.7956 20.9963 13V7H7.99631V4H1.99631V6H5.99631V13ZM18.9963 9V14H7.99631V9H18.9963ZM21.9963 19C21.9963 19.3956 21.879 19.7822 21.6592 20.1111C21.4395 20.44 21.1271 20.6964 20.7617 20.8478C20.3962 20.9991 19.9941 21.0387 19.6061 20.9616C19.2182 20.8844 18.8618 20.6939 18.5821 20.4142C18.3024 20.1345 18.1119 19.7781 18.0347 19.3902C17.9576 19.0022 17.9972 18.6001 18.1485 18.2346C18.2999 17.8692 18.5563 17.5568 18.8852 17.3371C19.2141 17.1173 19.6007 17 19.9963 17C20.5266 17.0006 21.0349 17.2115 21.4098 17.5865C21.7848 17.9614 21.9957 18.4698 21.9963 19ZM9.99631 19C9.99631 19.3956 9.87901 19.7822 9.65925 20.1111C9.43948 20.44 9.12713 20.6964 8.76167 20.8478C8.39622 20.9991 7.99409 21.0387 7.60613 20.9616C7.21816 20.8844 6.8618 20.6939 6.58209 20.4142C6.30239 20.1345 6.11191 19.7781 6.03474 19.3902C5.95757 19.0022 5.99717 18.6001 6.14855 18.2346C6.29992 17.8692 6.55627 17.5568 6.88517 17.3371C7.21407 17.1173 7.60074 17 7.99631 17C8.52655 17.0006 9.03491 17.2115 9.40985 17.5865C9.78479 17.9614 9.9957 18.4698 9.99631 19Z" /> +</svg> diff --git a/resources/themes/cura-light/icons/default/Spinner.svg b/resources/themes/cura-light/icons/default/Spinner.svg new file mode 100644 index 0000000000..22a8f4dfd9 --- /dev/null +++ b/resources/themes/cura-light/icons/default/Spinner.svg @@ -0,0 +1,3 @@ +<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> diff --git a/resources/themes/cura-light/icons/high/Certificate.svg b/resources/themes/cura-light/icons/high/Certificate.svg new file mode 100644 index 0000000000..b588bddd8b --- /dev/null +++ b/resources/themes/cura-light/icons/high/Certificate.svg @@ -0,0 +1,3 @@ +<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> diff --git a/resources/themes/cura-light/icons/high/Settings.svg b/resources/themes/cura-light/icons/high/Settings.svg new file mode 100644 index 0000000000..1cd2ff324e --- /dev/null +++ b/resources/themes/cura-light/icons/high/Settings.svg @@ -0,0 +1,3 @@ +<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M31.6058 21.5314C31.4745 21.131 31.3116 20.7417 31.1185 20.3671C30.3501 18.8691 29.131 17.6501 27.6331 16.8816C26.5097 16.3023 25.2641 16.0001 24.0001 16.0001C22.7361 16.0001 21.4905 16.3023 20.3671 16.8816C18.8691 17.6501 17.65 18.8692 16.8815 20.3672C16.3021 21.4906 15.9998 22.7362 15.9998 24.0002C15.9998 25.2642 16.3021 26.5098 16.8815 27.6332C17.65 29.1311 18.869 30.3502 20.367 31.1186C21.4904 31.698 22.736 32.0003 24 32.0003C25.264 32.0003 26.5096 31.698 27.633 31.1186C29.1309 30.3502 30.3499 29.1312 31.1183 27.6333C31.5999 26.6993 31.8911 25.6789 31.9747 24.6314C32.0584 23.5839 31.933 22.5302 31.6057 21.5316L31.6058 21.5314ZM28.3781 28.09C28.2852 28.1894 28.189 28.2855 28.0897 28.3784C27.0333 29.3689 25.6529 29.9417 24.2056 29.99C24.1367 29.9923 24.0694 30 24 30C23.9306 30 23.8633 29.9919 23.7944 29.99C22.3469 29.9416 20.9664 29.3688 19.91 28.3781C19.8106 28.2852 19.7145 28.189 19.6216 28.0897C18.6312 27.0335 18.0585 25.6533 18.01 24.2062C18.0081 24.1371 18 24.07 18 24C18 23.93 18.0081 23.8629 18.01 23.7938C18.0585 22.3466 18.6314 20.9663 19.6219 19.91C19.7148 19.8106 19.811 19.7145 19.9103 19.6216C20.9665 18.6312 22.3467 18.0585 23.7938 18.01C23.8629 18.0077 23.9304 18 24 18C24.0696 18 24.1371 18.0081 24.2062 18.01C25.6534 18.0585 27.0337 18.6314 28.09 19.6219C28.1894 19.7148 28.2855 19.811 28.3784 19.9103C29.3689 20.9667 29.9417 22.3471 29.99 23.7944C29.9923 23.8633 30 23.9306 30 24C30 24.0694 29.9919 24.1367 29.99 24.2056C29.9416 25.6531 29.3688 27.0336 28.3781 28.09ZM42 27.8467V20.1528L37.193 19.3516C37.0257 18.8773 36.8327 18.4124 36.6149 17.9591L39.4482 13.9927L34.0082 8.5518L30.0413 11.3851C29.5879 11.1673 29.1229 10.9742 28.6485 10.8069L27.8467 6H20.1528L19.3516 10.807C18.8774 10.9743 18.4126 11.1673 17.9593 11.385L13.9927 8.5518L8.5518 13.9927L11.385 17.9593C11.1673 18.4126 10.9743 18.8774 10.807 19.3516L6 20.1528V27.8467L10.8069 28.6481C10.9742 29.1225 11.1673 29.5875 11.3851 30.0409L8.5518 34.0078L13.9927 39.4478L17.9591 36.6145C18.4124 36.8323 18.8773 37.0253 19.3516 37.1926L20.1528 42H27.8467L28.6481 37.1931C29.1226 37.0257 29.5876 36.8327 30.0411 36.6149L34.0078 39.4482L39.4478 34.0082L36.6145 30.0415C36.8323 29.588 37.0253 29.123 37.1927 28.6485L42 27.8467ZM35.6375 26.88C35.5326 27.305 35.4043 27.7238 35.2532 28.1346C35.1409 28.4401 35.0184 28.7399 34.8824 29.0331C34.6981 29.4301 34.4921 29.8166 34.2654 30.191L35.52 31.947L36.8372 33.791L33.791 36.8369L31.947 35.52L30.1912 34.2657C29.8168 34.4924 29.4303 34.6984 29.0333 34.8827C28.74 35.0186 28.4403 35.1412 28.1348 35.2535C27.724 35.4046 27.3052 35.5329 26.8802 35.6378L26.5264 37.7609L26.1533 40H21.8472L21.474 37.7607L21.12 35.6376C20.695 35.5326 20.2761 35.4043 19.8652 35.2532C19.5594 35.1408 19.2597 35.0182 18.9662 34.8823C18.5692 34.6978 18.1825 34.4917 17.808 34.265L16.052 35.5191L14.2083 36.8359L11.1641 33.791L12.4808 31.9476L13.735 30.1918C13.5081 29.8173 13.302 29.4305 13.1176 29.0333C12.9816 28.7399 12.8591 28.4402 12.7467 28.1345C12.5956 27.7238 12.4674 27.3051 12.3625 26.8802L10.2394 26.5264L8 26.1533V21.8472L10.2393 21.474L12.3624 21.1202C12.4673 20.6953 12.5956 20.2764 12.7466 19.8656C12.8591 19.5598 12.9817 19.2599 13.1177 18.9664C13.3021 18.5693 13.5082 18.1825 13.735 17.808L12.481 16.0522L11.1641 14.2085L14.2085 11.1641L16.0522 12.481L17.808 13.735C18.1825 13.5082 18.5693 13.3021 18.9664 13.1177C19.2599 12.9817 19.5598 12.8591 19.8656 12.7466C20.2764 12.5956 20.6953 12.4673 21.1202 12.3624L21.474 10.2393L21.8472 8H26.1533L26.5264 10.2394L26.8802 12.3625C27.3051 12.4673 27.7238 12.5955 28.1345 12.7466C28.4401 12.859 28.7399 12.9816 29.0333 13.1176C29.4305 13.302 29.8173 13.5081 30.1918 13.735L31.9476 12.4808L33.791 11.1641L36.8359 14.2085L35.5191 16.0522L34.265 17.8082C34.4918 18.1827 34.6979 18.5693 34.8823 18.9664C35.0183 19.2599 35.1409 19.5597 35.2533 19.8654C35.4044 20.2763 35.5327 20.6952 35.6376 21.1202L37.7607 21.474L40 21.8472V26.1533L37.7606 26.5264L35.6375 26.88Z" fill="#000E1A"/> +</svg> diff --git a/resources/themes/cura-light/styles.qml b/resources/themes/cura-light/styles.qml index 1320b54f37..8376ecb44a 100755 --- a/resources/themes/cura-light/styles.qml +++ b/resources/themes/cura-light/styles.qml @@ -602,62 +602,6 @@ QtObject } } - property Component toolbox_action_button: Component - { - ButtonStyle - { - background: Rectangle - { - implicitWidth: UM.Theme.getSize("toolbox_action_button").width - implicitHeight: UM.Theme.getSize("toolbox_action_button").height - color: - { - if (control.installed) - { - return UM.Theme.getColor("action_button_disabled"); - } - else - { - if (control.hovered) - { - return UM.Theme.getColor("primary_hover"); - } - else - { - return UM.Theme.getColor("primary"); - } - } - - } - } - label: Label - { - text: control.text - color: - { - if (control.installed) - { - return UM.Theme.getColor("action_button_disabled_text"); - } - else - { - if (control.hovered) - { - return UM.Theme.getColor("button_text_hover"); - } - else - { - return UM.Theme.getColor("button_text"); - } - } - } - verticalAlignment: Text.AlignVCenter - horizontalAlignment: Text.AlignHCenter - font: UM.Theme.getFont("default_bold") - } - } - } - property Component monitor_button_style: Component { ButtonStyle diff --git a/resources/themes/cura-light/theme.json b/resources/themes/cura-light/theme.json index f59231d960..81986683f4 100644 --- a/resources/themes/cura-light/theme.json +++ b/resources/themes/cura-light/theme.json @@ -173,12 +173,13 @@ "colors": { "main_background": [255, 255, 255, 255], + "detail_background": [243, 243, 243, 255], "wide_lining": [245, 245, 245, 255], "thick_lining": [180, 180, 180, 255], "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], @@ -427,9 +428,6 @@ "printer_config_matched": [50, 130, 255, 255], "printer_config_mismatch": [127, 127, 127, 255], - "toolbox_header_button_text_inactive": [0, 0, 0, 255], - "toolbox_premium_packages_background": [240, 240, 240, 255], - "favorites_header_bar": [245, 245, 245, 255], "favorites_header_hover": [245, 245, 245, 255], "favorites_header_text": [31, 36, 39, 255], @@ -556,12 +554,17 @@ "standard_list_lineheight": [1.5, 1.5], "standard_arrow": [1.0, 1.0], + "card": [25.0, 10], + "card_icon": [6.0, 6.0], + "card_tiny_icon": [1.5, 1.5], + "button": [4, 4], "button_icon": [2.5, 2.5], "button_lining": [0, 0], "action_button": [15.0, 2.5], - "action_button_icon": [1.0, 1.0], + "action_button_icon": [1.5, 1.5], + "action_button_icon_small": [1.0, 1.0], "action_button_radius": [0.15, 0.15], "dialog_primary_button_padding": [3.0, 0], @@ -641,24 +644,6 @@ "build_plate_selection_size": [15, 5], "objects_menu_button": [0.3, 2.7], - "toolbox_thumbnail_small": [6.0, 6.0], - "toolbox_thumbnail_medium": [8.0, 8.0], - "toolbox_thumbnail_large": [12.0, 10.0], - "toolbox_footer": [1.0, 4.5], - "toolbox_footer_button": [8.0, 2.5], - "toolbox_header_tab": [12.0, 4.0], - "toolbox_detail_header": [1.0, 14.0], - "toolbox_back_column": [6.0, 1.0], - "toolbox_back_button": [6.0, 2.0], - "toolbox_installed_tile": [1.0, 8.0], - "toolbox_property_label": [1.0, 2.0], - "toolbox_heading_label": [1.0, 3.8], - "toolbox_header": [1.0, 4.0], - "toolbox_header_highlight": [0.25, 0.25], - "toolbox_chart_row": [1.0, 2.0], - "toolbox_action_button": [8.0, 2.5], - "toolbox_loader": [2.0, 2.0], - "notification_icon": [1.5, 1.5], "avatar_image": [6.8, 6.8], @@ -681,6 +666,10 @@ "table_row": [2.0, 2.0], "welcome_wizard_content_image_big": [18, 15], - "welcome_wizard_cloud_content_image": [4, 4] + "welcome_wizard_cloud_content_image": [4, 4], + + "banner_icon_size": [2.0, 2.0], + + "marketplace_large_icon": [4.0, 4.0] } } diff --git a/scripts/check_invalid_imports.py b/scripts/check_invalid_imports.py index ba21b9f822..b77a82568d 100644 --- a/scripts/check_invalid_imports.py +++ b/scripts/check_invalid_imports.py @@ -8,7 +8,7 @@ Run this file with the Cura project root as the working directory Checks for invalid imports. When importing from plugins, there will be no problems when running from source, but for some build types the plugins dir is not on the path, so relative imports should be used instead. eg: from ..UltimakerCloudScope import UltimakerCloudScope <-- OK -import plugins.Toolbox.src ... <-- NOT OK +import plugins.Marketplace.src ... <-- NOT OK """ |