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'\1', text) + + # Turn newlines into
so that they get displayed as newlines when rendering as rich text. + text = text.replace("\n", "
") + + 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 similarity index 100% rename from plugins/Toolbox/resources/images/placeholder.svg rename to 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 @@ - - - - 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 @@ - - - - - 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 += "%2".arg(base.technicalDataSheetUrl).arg(tds_name) - } - if (base.safetyDataSheetUrl !== undefined) - { - if (result.length > 0) - { - result += "
" - } - var sds_name = catalog.i18nc("@action:label", "Safety Data Sheet") - result += "%2".arg(base.safetyDataSheetUrl).arg(sds_name) - } - if (base.printingGuidelinesUrl !== undefined) - { - if (result.length > 0) - { - result += "
" - } - var pg_name = catalog.i18nc("@action:label", "Printing Guidelines") - result += "%2".arg(base.printingGuidelinesUrl).arg(pg_name) - } - if (base.materialWebsiteUrl !== undefined) - { - if (result.length > 0) - { - result += "
" - } - var pg_name = catalog.i18nc("@action:label", "Website") - result += "%2".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 and is the highlighted link", "Log in 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 and is the highlighted link", "Buy material spools") - 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: "".arg(toolbox.getWebMarketplaceUrl("materials") + "?utm_source=cura&utm_medium=software&utm_campaign=marketplace-search") + catalog.i18nc("@label", "Search materials") + "" - 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 "" + model.author_name + "" - } - 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 and is the highlighted link", "Log in 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 "" + details.website + "" - } - 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 "" + details.email + "" - } - 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 "" + details.author_name + "" - } - } - 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 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 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 @@ + + + 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 @@ + + + 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 @@ + + + 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 @@ + + + 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 @@ + + + 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 @@ + + + \ 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 @@ + + + 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 @@ + + + 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 @@ + + + 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 @@ + + + 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 @@ + + + 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 """