diff options
Diffstat (limited to 'plugins')
83 files changed, 2898 insertions, 4798 deletions
diff --git a/plugins/CuraDrive/src/qml/components/BackupListFooter.qml b/plugins/CuraDrive/src/qml/components/BackupListFooter.qml index 15af7521ed..76bd10bd66 100644 --- a/plugins/CuraDrive/src/qml/components/BackupListFooter.qml +++ b/plugins/CuraDrive/src/qml/components/BackupListFooter.qml @@ -5,7 +5,7 @@ import QtQuick 2.7 import QtQuick.Controls 2.1 import QtQuick.Layouts 1.3 -import UM 1.3 as UM +import UM 1.5 as UM import Cura 1.0 as Cura import "../components" @@ -35,7 +35,7 @@ RowLayout busy: CuraDrive.isCreatingBackup } - Cura.CheckBoxWithTooltip + UM.CheckBox { id: autoBackupEnabled checked: CuraDrive.autoBackupEnabled 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/SaveProjectFilesPage.qml b/plugins/DigitalLibrary/resources/qml/SaveProjectFilesPage.qml index d8ae78d96d..c66556071f 100644 --- a/plugins/DigitalLibrary/resources/qml/SaveProjectFilesPage.qml +++ b/plugins/DigitalLibrary/resources/qml/SaveProjectFilesPage.qml @@ -6,7 +6,7 @@ import QtQuick.Controls 1.4 as OldControls // TableView doesn't exist in the QtQ import QtQuick.Controls 2.3 import QtQuick.Controls.Styles 1.4 -import UM 1.2 as UM +import UM 1.5 as UM import Cura 1.6 as Cura import DigitalFactory 1.0 as DF @@ -228,7 +228,7 @@ Item width: childrenRect.width spacing: UM.Theme.getSize("default_margin").width - Cura.CheckBox + UM.CheckBox { id: asProjectCheckbox height: UM.Theme.getSize("checkbox").height @@ -238,7 +238,7 @@ Item font: UM.Theme.getFont("medium") } - Cura.CheckBox + UM.CheckBox { id: asSlicedCheckbox height: UM.Theme.getSize("checkbox").height diff --git a/plugins/DigitalLibrary/resources/qml/SelectProjectPage.qml b/plugins/DigitalLibrary/resources/qml/SelectProjectPage.qml index 89d282ab83..24d4cbfade 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,13 @@ 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 +202,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/FirmwareUpdateChecker/FirmwareUpdateCheckerJob.py b/plugins/FirmwareUpdateChecker/FirmwareUpdateCheckerJob.py index d726cc04a9..8d08cde37b 100644 --- a/plugins/FirmwareUpdateChecker/FirmwareUpdateCheckerJob.py +++ b/plugins/FirmwareUpdateChecker/FirmwareUpdateCheckerJob.py @@ -12,7 +12,7 @@ from urllib.error import URLError from typing import Dict import ssl -import certifi +import certifi # type: ignore from .FirmwareUpdateCheckerLookup import FirmwareUpdateCheckerLookup, getSettingsKeyForMachine from .FirmwareUpdateCheckerMessage import FirmwareUpdateCheckerMessage diff --git a/plugins/GCodeReader/FlavorParser.py b/plugins/GCodeReader/FlavorParser.py index bb99aa59ec..8d35bd3345 100644 --- a/plugins/GCodeReader/FlavorParser.py +++ b/plugins/GCodeReader/FlavorParser.py @@ -24,7 +24,7 @@ from cura.Settings.ExtruderManager import ExtruderManager catalog = i18nCatalog("cura") -PositionOptional = NamedTuple("Position", [("x", Optional[float]), ("y", Optional[float]), ("z", Optional[float]), ("f", Optional[float]), ("e", Optional[float])]) +PositionOptional = NamedTuple("PositionOptional", [("x", Optional[float]), ("y", Optional[float]), ("z", Optional[float]), ("f", Optional[float]), ("e", Optional[float])]) Position = NamedTuple("Position", [("x", float), ("y", float), ("z", float), ("f", float), ("e", List[float])]) diff --git a/plugins/Marketplace/Constants.py b/plugins/Marketplace/Constants.py new file mode 100644 index 0000000000..9f0f78b966 --- /dev/null +++ b/plugins/Marketplace/Constants.py @@ -0,0 +1,12 @@ +# Copyright (c) 2021 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + +from cura.UltimakerCloud import UltimakerCloudConstants +from cura.ApplicationMetadata import CuraSDKVersion + +ROOT_URL = f"{UltimakerCloudConstants.CuraCloudAPIRoot}/cura-packages/v{UltimakerCloudConstants.CuraCloudAPIVersion}" +ROOT_CURA_URL = f"{ROOT_URL}/cura/v{CuraSDKVersion}" # Root of all Marketplace API requests. +ROOT_USER_URL = f"{ROOT_URL}/user" +PACKAGES_URL = f"{ROOT_CURA_URL}/packages" # URL to use for requesting the list of packages. +PACKAGE_UPDATES_URL = f"{PACKAGES_URL}/package-updates" # URL to use for requesting the list of packages that can be updated. +USER_PACKAGES_URL = f"{ROOT_USER_URL}/packages" diff --git a/plugins/Marketplace/LocalPackageList.py b/plugins/Marketplace/LocalPackageList.py new file mode 100644 index 0000000000..a609e72d33 --- /dev/null +++ b/plugins/Marketplace/LocalPackageList.py @@ -0,0 +1,126 @@ +# Copyright (c) 2022 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + +from typing import Any, Dict, List, Optional, TYPE_CHECKING + +from PyQt5.QtCore import pyqtSlot, QObject + +from UM.Version import Version +from UM.i18n import i18nCatalog +from UM.TaskManagement.HttpRequestManager import HttpRequestManager +from UM.Logger import Logger + +from .PackageList import PackageList +from .PackageModel import PackageModel +from .Constants import PACKAGE_UPDATES_URL + +if TYPE_CHECKING: + from PyQt5.QtCore import QObject + from PyQt5.QtNetwork import QNetworkReply + +catalog = i18nCatalog("cura") + + +class LocalPackageList(PackageList): + PACKAGE_CATEGORIES = { + "installed": + { + "plugin": catalog.i18nc("@label", "Installed Plugins"), + "material": catalog.i18nc("@label", "Installed Materials") + }, + "bundled": + { + "plugin": catalog.i18nc("@label", "Bundled Plugins"), + "material": catalog.i18nc("@label", "Bundled Materials") + } + } # The section headers to be used for the different package categories + + def __init__(self, parent: Optional["QObject"] = None) -> None: + super().__init__(parent) + self._has_footer = False + self._ongoing_requests["check_updates"] = None + self._package_manager.packagesWithUpdateChanged.connect(self._sortSectionsOnUpdate) + self._package_manager.packageUninstalled.connect(self._removePackageModel) + + def _sortSectionsOnUpdate(self) -> None: + section_order = dict(zip([i for k, v in self.PACKAGE_CATEGORIES.items() for i in self.PACKAGE_CATEGORIES[k].values()], ["a", "b", "c", "d"])) + self.sort(lambda model: (section_order[model.sectionTitle], model.canUpdate, model.displayName.lower()), key = "package") + + def _removePackageModel(self, package_id: str) -> None: + """ + Cleanup function to remove the package model from the list. Note that this is only done if the package can't + be updated, it is in the to remove list and isn't in the to be installed list + """ + package = self.getPackageModel(package_id) + + if package and not package.canUpdate and \ + package_id in self._package_manager.getToRemovePackageIDs() and \ + package_id not in self._package_manager.getPackagesToInstall(): + index = self.find("package", package_id) + if index < 0: + Logger.error(f"Could not find card in Listview corresponding with {package_id}") + self.updatePackages() + return + self.removeItem(index) + + @pyqtSlot() + def updatePackages(self) -> None: + """Update the list with local packages, these are materials or plugin, either bundled or user installed. The list + will also contain **to be removed** or **to be installed** packages since the user might still want to interact + with these. + """ + self.setErrorMessage("") # Clear any previous errors. + self.setIsLoading(True) + + # Obtain and sort the local packages + self.setItems([{"package": p} for p in [self._makePackageModel(p) for p in self._package_manager.local_packages]]) + self._sortSectionsOnUpdate() + self.checkForUpdates(self._package_manager.local_packages) + + self.setIsLoading(False) + self.setHasMore(False) # All packages should have been loaded at this time + + def _makePackageModel(self, package_info: Dict[str, Any]) -> PackageModel: + """ Create a PackageModel from the package_info and determine its section_title""" + + package_id = package_info["package_id"] + bundled_or_installed = "bundled" if self._package_manager.isBundledPackage(package_id) else "installed" + package_type = package_info["package_type"] + section_title = self.PACKAGE_CATEGORIES[bundled_or_installed][package_type] + package = PackageModel(package_info, section_title = section_title, parent = self) + self._connectManageButtonSignals(package) + return package + + def checkForUpdates(self, packages: List[Dict[str, Any]]) -> None: + installed_packages = "&".join([f"installed_packages={package['package_id']}:{package['package_version']}" for package in packages]) + request_url = f"{PACKAGE_UPDATES_URL}?{installed_packages}" + + self._ongoing_requests["check_updates"] = HttpRequestManager.getInstance().get( + request_url, + scope = self._scope, + callback = self._parseResponse + ) + + def _parseResponse(self, reply: "QNetworkReply") -> None: + """ + Parse the response from the package list API request which can update. + + :param reply: A reply containing information about a number of packages. + """ + response_data = HttpRequestManager.readJSON(reply) + if "data" not in response_data: + Logger.error( + f"Could not interpret the server's response. Missing 'data' from response data. Keys in response: {response_data.keys()}") + return + if len(response_data["data"]) == 0: + return + + packages = response_data["data"] + for package in packages: + self._package_manager.addAvailablePackageVersion(package["package_id"], Version(package["package_version"])) + package_model = self.getPackageModel(package["package_id"]) + if package_model: + # Also make sure that the local list knows where to get an update + package_model.setDownloadUrl(package["download_url"]) + + self._ongoing_requests["check_updates"] = None diff --git a/plugins/Marketplace/Marketplace.py b/plugins/Marketplace/Marketplace.py new file mode 100644 index 0000000000..2d98947572 --- /dev/null +++ b/plugins/Marketplace/Marketplace.py @@ -0,0 +1,114 @@ +# Copyright (c) 2022 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + +import os.path +from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject +from typing import Optional, cast + +from cura.CuraApplication import CuraApplication # Creating QML objects and managing packages. + +from UM.Extension import Extension # We are implementing the main object of an extension here. +from UM.PluginRegistry import PluginRegistry # To find out where we are stored (the proper way). + +from .RemotePackageList import RemotePackageList # To register this type with QML. +from .LocalPackageList import LocalPackageList # To register this type with QML. + + +class Marketplace(Extension, QObject): + """ + The main managing object for the Marketplace plug-in. + """ + def __init__(self, parent: Optional[QObject] = None) -> None: + QObject.__init__(self, parent) + Extension.__init__(self) + self._window: Optional["QObject"] = None # If the window has been loaded yet, it'll be cached in here. + self._plugin_registry: Optional[PluginRegistry] = None + self._package_manager = CuraApplication.getInstance().getPackageManager() + + self._material_package_list: Optional[RemotePackageList] = None + self._plugin_package_list: Optional[RemotePackageList] = None + + # Not entirely the cleanest code, since the localPackage list also checks the server if there are updates + # Since that in turn will trigger notifications to be shown, we do need to construct it here and make sure + # that it checks for updates... + self._local_package_list = LocalPackageList(self) + self._local_package_list.checkForUpdates(self._package_manager.local_packages) + + self._package_manager.installedPackagesChanged.connect(self.checkIfRestartNeeded) + + self._tab_shown: int = 0 + self._restart_needed = False + + def getTabShown(self) -> int: + return self._tab_shown + + def setTabShown(self, tab_shown: int) -> None: + if tab_shown != self._tab_shown: + self._tab_shown = tab_shown + self.tabShownChanged.emit() + + tabShownChanged = pyqtSignal() + tabShown = pyqtProperty(int, fget=getTabShown, fset=setTabShown, notify=tabShownChanged) + + @pyqtProperty(QObject, constant=True) + def MaterialPackageList(self): + if self._material_package_list is None: + self._material_package_list = RemotePackageList() + self._material_package_list.packageTypeFilter = "material" + + return self._material_package_list + + @pyqtProperty(QObject, constant=True) + def PluginPackageList(self): + if self._plugin_package_list is None: + self._plugin_package_list = RemotePackageList() + self._plugin_package_list.packageTypeFilter = "plugin" + return self._plugin_package_list + + @pyqtProperty(QObject, constant=True) + def LocalPackageList(self): + return self._local_package_list + + @pyqtSlot() + def show(self) -> None: + """ + Opens the window of the Marketplace. + + If the window hadn't been loaded yet into Qt, it will be created lazily. + """ + if self._window is None: + self._plugin_registry = PluginRegistry.getInstance() + self._plugin_registry.pluginsEnabledOrDisabledChanged.connect(self.checkIfRestartNeeded) + plugin_path = PluginRegistry.getInstance().getPluginPath(self.getPluginId()) + if plugin_path is None: + plugin_path = os.path.dirname(__file__) + path = os.path.join(plugin_path, "resources", "qml", "Marketplace.qml") + self._window = CuraApplication.getInstance().createQmlComponent(path, {"manager": self}) + if self._window is None: # Still None? Failed to load the QML then. + return + if not self._window.isVisible(): + self.setTabShown(0) + self._window.show() + self._window.requestActivate() # Bring window into focus, if it was already open in the background. + + @pyqtSlot() + def setVisibleTabToMaterials(self) -> None: + """ + Set the tab shown to the remote materials one. + Not implemented in a more generic way because it needs the ability to be called with 'callExtensionMethod'. + """ + self.setTabShown(1) + + def checkIfRestartNeeded(self) -> None: + if self._package_manager.hasPackagesToRemoveOrInstall or \ + cast(PluginRegistry, self._plugin_registry).getCurrentSessionActivationChangedPlugins(): + self._restart_needed = True + else: + self._restart_needed = False + self.showRestartNotificationChanged.emit() + + showRestartNotificationChanged = pyqtSignal() + + @pyqtProperty(bool, notify=showRestartNotificationChanged) + def showRestartNotification(self) -> bool: + return self._restart_needed diff --git a/plugins/Marketplace/PackageList.py b/plugins/Marketplace/PackageList.py new file mode 100644 index 0000000000..04b602002c --- /dev/null +++ b/plugins/Marketplace/PackageList.py @@ -0,0 +1,305 @@ +# Copyright (c) 2021 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. +import tempfile +import json +import os.path + +from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, Qt +from typing import cast, Dict, Optional, Set, TYPE_CHECKING + +from UM.i18n import i18nCatalog +from UM.Qt.ListModel import ListModel +from UM.TaskManagement.HttpRequestScope import JsonDecoratorScope +from UM.TaskManagement.HttpRequestManager import HttpRequestData, HttpRequestManager +from UM.Logger import Logger +from UM import PluginRegistry + +from cura.CuraApplication import CuraApplication +from cura.CuraPackageManager import CuraPackageManager +from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope # To make requests to the Ultimaker API with correct authorization. + +from .PackageModel import PackageModel +from .Constants import USER_PACKAGES_URL, PACKAGES_URL + +if TYPE_CHECKING: + from PyQt5.QtCore import QObject + from PyQt5.QtNetwork import QNetworkReply + +catalog = i18nCatalog("cura") + + +class PackageList(ListModel): + """ A List model for Packages, this class serves as parent class for more detailed implementations. + such as Packages obtained from Remote or Local source + """ + PackageRole = Qt.UserRole + 1 + DISK_WRITE_BUFFER_SIZE = 256 * 1024 # 256 KB + + def __init__(self, parent: Optional["QObject"] = None) -> None: + super().__init__(parent) + self._package_manager: CuraPackageManager = cast(CuraPackageManager, CuraApplication.getInstance().getPackageManager()) + self._plugin_registry: PluginRegistry = CuraApplication.getInstance().getPluginRegistry() + self._account = CuraApplication.getInstance().getCuraAPI().account + self._error_message = "" + self.addRoleName(self.PackageRole, "package") + self._is_loading = False + self._has_more = False + self._has_footer = True + self._to_install: Dict[str, str] = {} + + self._ongoing_requests: Dict[str, Optional[HttpRequestData]] = {"download_package": None} + self._scope = JsonDecoratorScope(UltimakerCloudScope(CuraApplication.getInstance())) + self._license_dialogs: Dict[str, QObject] = {} + + def __del__(self) -> None: + """ When this object is deleted it will loop through all registered API requests and aborts them """ + try: + self.isLoadingChanged.disconnect() + self.hasMoreChanged.disconnect() + except RuntimeError: + pass + + self.cleanUpAPIRequest() + + def abortRequest(self, request_id: str) -> None: + """Aborts a single request""" + if request_id in self._ongoing_requests and self._ongoing_requests[request_id]: + HttpRequestManager.getInstance().abortRequest(self._ongoing_requests[request_id]) + self._ongoing_requests[request_id] = None + + @pyqtSlot() + def cleanUpAPIRequest(self) -> None: + for request_id in self._ongoing_requests: + self.abortRequest(request_id) + + @pyqtSlot() + def updatePackages(self) -> None: + """ A Qt slot which will update the List from a source. Actual implementation should be done in the child class""" + pass + + def reset(self) -> None: + """ Resets and clears the list""" + self.clear() + + isLoadingChanged = pyqtSignal() + + def setIsLoading(self, value: bool) -> None: + if self._is_loading != value: + self._is_loading = value + self.isLoadingChanged.emit() + + @pyqtProperty(bool, fset = setIsLoading, notify = isLoadingChanged) + def isLoading(self) -> bool: + """ Indicating if the the packages are loading + :return" ``True`` if the list is being obtained, otherwise ``False`` + """ + return self._is_loading + + hasMoreChanged = pyqtSignal() + + def setHasMore(self, value: bool) -> None: + if self._has_more != value: + self._has_more = value + self.hasMoreChanged.emit() + + @pyqtProperty(bool, fset = setHasMore, notify = hasMoreChanged) + def hasMore(self) -> bool: + """ Indicating if there are more packages available to load. + :return: ``True`` if there are more packages to load, or ``False``. + """ + return self._has_more + + errorMessageChanged = pyqtSignal() + + def setErrorMessage(self, error_message: str) -> None: + if self._error_message != error_message: + self._error_message = error_message + self.errorMessageChanged.emit() + + @pyqtProperty(str, notify = errorMessageChanged, fset = setErrorMessage) + def errorMessage(self) -> str: + """ If an error occurred getting the list of packages, an error message will be held here. + + If no error occurred (yet), this will be an empty string. + :return: An error message, if any, or an empty string if everything went okay. + """ + return self._error_message + + @pyqtProperty(bool, constant = True) + def hasFooter(self) -> bool: + """ Indicating if the PackageList should have a Footer visible. For paginated PackageLists + :return: ``True`` if a Footer should be displayed in the ListView, e.q.: paginated lists, ``False`` Otherwise""" + return self._has_footer + + def getPackageModel(self, package_id: str) -> Optional[PackageModel]: + index = self.find("package", package_id) + data = self.getItem(index) + if data: + return data.get("package") + return None + + def _openLicenseDialog(self, package_id: str, license_content: str) -> None: + plugin_path = self._plugin_registry.getPluginPath("Marketplace") + if plugin_path is None: + plugin_path = os.path.dirname(__file__) + + # create a QML component for the license dialog + license_dialog_component_path = os.path.join(plugin_path, "resources", "qml", "LicenseDialog.qml") + dialog = CuraApplication.getInstance().createQmlComponent(license_dialog_component_path, { + "licenseContent": license_content, + "packageId": package_id, + "handler": self + }) + dialog.show() + # place dialog in class such that it does not get remove by garbage collector + self._license_dialogs[package_id] = dialog + + @pyqtSlot(str) + def onLicenseAccepted(self, package_id: str) -> None: + # close dialog + dialog = self._license_dialogs.pop(package_id) + if dialog is not None: + dialog.deleteLater() + # install relevant package + self._install(package_id) + + @pyqtSlot(str) + def onLicenseDeclined(self, package_id: str) -> None: + # close dialog + dialog = self._license_dialogs.pop(package_id) + if dialog is not None: + dialog.deleteLater() + # reset package card + self._package_manager.packageInstallingFailed.emit(package_id) + + def _requestInstall(self, package_id: str, update: bool = False) -> None: + package_path = self._to_install[package_id] + license_content = self._package_manager.getPackageLicense(package_path) + + if not update and license_content is not None and license_content != "": + # If installation is not and update, and the packages contains a license then + # open dialog, prompting the using to accept the plugin license + self._openLicenseDialog(package_id, license_content) + else: + # Otherwise continue the installation + self._install(package_id, update) + + def _install(self, package_id: str, update: bool = False) -> None: + package_path = self._to_install.pop(package_id) + to_be_installed = self._package_manager.installPackage(package_path) is not None + if not to_be_installed: + Logger.warning(f"Could not install {package_id}") + return + package = self.getPackageModel(package_id) + if package: + self.subscribeUserToPackage(package_id, str(package.sdk_version)) + else: + Logger.log("w", f"Unable to get data on package {package_id}") + + def download(self, package_id: str, url: str, update: bool = False) -> None: + """Initiate the download request + + :param package_id: the package identification string + :param url: the URL from which the package needs to be obtained + :param update: A flag if this is download request is an update process + """ + + if url == "": + url = f"{PACKAGES_URL}/{package_id}/download" + + def downloadFinished(reply: "QNetworkReply") -> None: + self._downloadFinished(package_id, reply, update) + + def downloadError(reply: "QNetworkReply", error: "QNetworkReply.NetworkError") -> None: + self._downloadError(package_id, update, reply, error) + + self._ongoing_requests["download_package"] = HttpRequestManager.getInstance().get( + url, + scope = self._scope, + callback = downloadFinished, + error_callback = downloadError + ) + + def _downloadFinished(self, package_id: str, reply: "QNetworkReply", update: bool = False) -> None: + with tempfile.NamedTemporaryFile(mode = "wb+", suffix = ".curapackage", delete = False) as temp_file: + try: + bytes_read = reply.read(self.DISK_WRITE_BUFFER_SIZE) + while bytes_read: + temp_file.write(bytes_read) + bytes_read = reply.read(self.DISK_WRITE_BUFFER_SIZE) + except IOError as e: + Logger.error(f"Failed to write downloaded package to temp file {e}") + temp_file.close() + self._downloadError(package_id, update) + except RuntimeError: + # Setting the ownership of this object to not qml can still result in a RuntimeError. Which can occur when quickly toggling + # between de-/constructing Remote or Local PackageLists. This try-except is here to prevent a hard crash when the wrapped C++ object + # was deleted when it was still parsing the response + temp_file.close() + return + temp_file.close() + self._to_install[package_id] = temp_file.name + self._ongoing_requests["download_package"] = None + self._requestInstall(package_id, update) + + def _downloadError(self, package_id: str, update: bool = False, reply: Optional["QNetworkReply"] = None, error: Optional["QNetworkReply.NetworkError"] = None) -> None: + if reply: + reply_string = bytes(reply.readAll()).decode() + Logger.error(f"Failed to download package: {package_id} due to {reply_string}") + self._package_manager.packageInstallingFailed.emit(package_id) + + def subscribeUserToPackage(self, package_id: str, sdk_version: str) -> None: + """Subscribe the user (if logged in) to the package for a given SDK + + :param package_id: the package identification string + :param sdk_version: the SDK version + """ + if self._account.isLoggedIn: + HttpRequestManager.getInstance().put( + url = USER_PACKAGES_URL, + data = json.dumps({"data": {"package_id": package_id, "sdk_version": sdk_version}}).encode(), + scope = self._scope + ) + + def unsunscribeUserFromPackage(self, package_id: str) -> None: + """Unsubscribe the user (if logged in) from the package + + :param package_id: the package identification string + """ + if self._account.isLoggedIn: + HttpRequestManager.getInstance().delete(url = f"{USER_PACKAGES_URL}/{package_id}", scope = self._scope) + + # --- Handle the manage package buttons --- + + def _connectManageButtonSignals(self, package: PackageModel) -> None: + package.installPackageTriggered.connect(self.installPackage) + package.uninstallPackageTriggered.connect(self.uninstallPackage) + package.updatePackageTriggered.connect(self.updatePackage) + + def installPackage(self, package_id: str, url: str) -> None: + """Install a package from the Marketplace + + :param package_id: the package identification string + """ + if not self._package_manager.reinstallPackage(package_id): + self.download(package_id, url, False) + else: + package = self.getPackageModel(package_id) + if package: + self.subscribeUserToPackage(package_id, str(package.sdk_version)) + + def uninstallPackage(self, package_id: str) -> None: + """Uninstall a package from the Marketplace + + :param package_id: the package identification string + """ + self._package_manager.removePackage(package_id) + self.unsunscribeUserFromPackage(package_id) + + def updatePackage(self, package_id: str, url: str) -> None: + """Update a package from the Marketplace + + :param package_id: the package identification string + """ + self._package_manager.removePackage(package_id, force_add = not self._package_manager.isBundledPackage(package_id)) + self.download(package_id, url, True) diff --git a/plugins/Marketplace/PackageModel.py b/plugins/Marketplace/PackageModel.py new file mode 100644 index 0000000000..7c2a5d9ae1 --- /dev/null +++ b/plugins/Marketplace/PackageModel.py @@ -0,0 +1,382 @@ +# Copyright (c) 2021 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + +import re +from enum import Enum +from typing import Any, cast, Dict, List, Optional + +from PyQt5.QtCore import pyqtProperty, QObject, pyqtSignal, pyqtSlot +from PyQt5.QtQml import QQmlEngine + +from cura.CuraApplication import CuraApplication +from cura.CuraPackageManager import CuraPackageManager +from cura.Settings.CuraContainerRegistry import CuraContainerRegistry # To get names of materials we're compatible with. +from UM.i18n import i18nCatalog # To translate placeholder names if data is not present. +from UM.Logger import Logger +from UM.PluginRegistry import PluginRegistry + +catalog = i18nCatalog("cura") + + +class PackageModel(QObject): + """ + Represents a package, containing all the relevant information to be displayed about a package. + """ + + def __init__(self, package_data: Dict[str, Any], section_title: Optional[str] = None, parent: Optional[QObject] = None) -> None: + """ + Constructs a new model for a single package. + :param package_data: The data received from the Marketplace API about the package to create. + :param section_title: If the packages are to be categorized per section provide the section_title + :param parent: The parent QML object that controls the lifetime of this model (normally a PackageList). + """ + super().__init__(parent) + QQmlEngine.setObjectOwnership(self, QQmlEngine.CppOwnership) + self._package_manager: CuraPackageManager = cast(CuraPackageManager, CuraApplication.getInstance().getPackageManager()) + self._plugin_registry: PluginRegistry = CuraApplication.getInstance().getPluginRegistry() + + self._package_id = package_data.get("package_id", "UnknownPackageId") + self._package_type = package_data.get("package_type", "") + self._is_bundled = package_data.get("is_bundled", False) + self._icon_url = package_data.get("icon_url", "") + self._display_name = package_data.get("display_name", catalog.i18nc("@label:property", "Unknown Package")) + tags = package_data.get("tags", []) + self._is_checked_by_ultimaker = (self._package_type == "plugin" and "verified" in tags) or ( + self._package_type == "material" and "certified" in tags) + self._package_version = package_data.get("package_version", "") # Display purpose, no need for 'UM.Version'. + self._package_info_url = package_data.get("website", "") # Not to be confused with 'download_url'. + self._download_count = package_data.get("download_count", 0) + self._description = package_data.get("description", "") + self._formatted_description = self._format(self._description) + + self._download_url = package_data.get("download_url", "") + self._release_notes = package_data.get("release_notes", "") # Not used yet, propose to add to description? + + subdata = package_data.get("data", {}) + self._technical_data_sheet = self._findLink(subdata, "technical_data_sheet") + self._safety_data_sheet = self._findLink(subdata, "safety_data_sheet") + self._where_to_buy = self._findLink(subdata, "where_to_buy") + self._compatible_printers = self._getCompatiblePrinters(subdata) + self._compatible_support_materials = self._getCompatibleSupportMaterials(subdata) + self._is_compatible_material_station = self._isCompatibleMaterialStation(subdata) + self._is_compatible_air_manager = self._isCompatibleAirManager(subdata) + + author_data = package_data.get("author", {}) + self._author_name = author_data.get("display_name", catalog.i18nc("@label:property", "Unknown Author")) + self._author_info_url = author_data.get("website", "") + if not self._icon_url or self._icon_url == "": + self._icon_url = author_data.get("icon_url", "") + + self._can_update = False + self._section_title = section_title + self.sdk_version = package_data.get("sdk_version_semver", "") + # Note that there's a lot more info in the package_data than just these specified here. + + self.enablePackageTriggered.connect(self._plugin_registry.enablePlugin) + self.disablePackageTriggered.connect(self._plugin_registry.disablePlugin) + + self._plugin_registry.pluginsEnabledOrDisabledChanged.connect(self.stateManageButtonChanged) + self._package_manager.packageInstalled.connect(lambda pkg_id: self._packageInstalled(pkg_id)) + self._package_manager.packageUninstalled.connect(lambda pkg_id: self._packageInstalled(pkg_id)) + self._package_manager.packageInstallingFailed.connect(lambda pkg_id: self._packageInstalled(pkg_id)) + self._package_manager.packagesWithUpdateChanged.connect(self._processUpdatedPackages) + + self._is_busy = False + + @pyqtSlot() + def _processUpdatedPackages(self): + self.setCanUpdate(self._package_manager.checkIfPackageCanUpdate(self._package_id)) + + def __del__(self): + self._package_manager.packagesWithUpdateChanged.disconnect(self._processUpdatedPackages) + + def __eq__(self, other: object) -> bool: + if isinstance(other, PackageModel): + return other == self + elif isinstance(other, str): + return other == self._package_id + else: + return False + + def __repr__(self) -> str: + return f"<{self._package_id} : {self._package_version} : {self._section_title}>" + + def _findLink(self, subdata: Dict[str, Any], link_type: str) -> str: + """ + Searches the package data for a link of a certain type. + + The links are not in a fixed path in the package data. We need to iterate over the available links to find them. + :param subdata: The "data" element in the package data, which should contain links. + :param link_type: The type of link to find. + :return: A URL of where the link leads, or an empty string if there is no link of that type in the package data. + """ + links = subdata.get("links", []) + for link in links: + if link.get("type", "") == link_type: + return link.get("url", "") + else: + return "" # No link with the correct type was found. + + def _format(self, text: str) -> str: + """ + Formats a user-readable block of text for display. + :return: A block of rich text with formatting embedded. + """ + # Turn all in-line hyperlinks into actual links. + url_regex = re.compile(r"(((http|https)://)[a-zA-Z0-9@:%.\-_+~#?&/=]{2,256}\.[a-z]{2,12}(/[a-zA-Z0-9@:%.\-_+~#?&/=]*)?)") + text = re.sub(url_regex, r'<a href="\1">\1</a>', text) + + # Turn newlines into <br> so that they get displayed as newlines when rendering as rich text. + text = text.replace("\n", "<br>") + + return text + + def _getCompatiblePrinters(self, subdata: Dict[str, Any]) -> List[str]: + """ + Gets the list of printers that this package provides material compatibility with. + + Any printer is listed, even if it's only for a single nozzle on a single material in the package. + :param subdata: The "data" element in the package data, which should contain this compatibility information. + :return: A list of printer names that this package provides material compatibility with. + """ + result = set() + + for material in subdata.get("materials", []): + for compatibility in material.get("compatibility", []): + printer_name = compatibility.get("machine_name") + if printer_name is None: + continue # Missing printer name information. Skip this one. + for subcompatibility in compatibility.get("compatibilities", []): + if subcompatibility.get("hardware_compatible", False): + result.add(printer_name) + break + + return list(sorted(result)) + + def _getCompatibleSupportMaterials(self, subdata: Dict[str, Any]) -> List[str]: + """ + Gets the list of support materials that the materials in this package are compatible with. + + Since the materials are individually encoded as keys in the API response, only PVA and Breakaway are currently + supported. + :param subdata: The "data" element in the package data, which should contain this compatibility information. + :return: A list of support materials that the materials in this package are compatible with. + """ + result = set() + + container_registry = CuraContainerRegistry.getInstance() + try: + pva_name = container_registry.findContainersMetadata(id = "ultimaker_pva")[0].get("name", "Ultimaker PVA") + except IndexError: + pva_name = "Ultimaker PVA" + try: + breakaway_name = container_registry.findContainersMetadata(id = "ultimaker_bam")[0].get("name", "Ultimaker Breakaway") + except IndexError: + breakaway_name = "Ultimaker Breakaway" + + for material in subdata.get("materials", []): + if material.get("pva_compatible", False): + result.add(pva_name) + if material.get("breakaway_compatible", False): + result.add(breakaway_name) + + return list(sorted(result)) + + def _isCompatibleMaterialStation(self, subdata: Dict[str, Any]) -> bool: + """ + Finds out if this package provides any material that is compatible with the material station. + :param subdata: The "data" element in the package data, which should contain this compatibility information. + :return: Whether this package provides any material that is compatible with the material station. + """ + for material in subdata.get("materials", []): + for compatibility in material.get("compatibility", []): + if compatibility.get("material_station_optimized", False): + return True + return False + + def _isCompatibleAirManager(self, subdata: Dict[str, Any]) -> bool: + """ + Finds out if this package provides any material that is compatible with the air manager. + :param subdata: The "data" element in the package data, which should contain this compatibility information. + :return: Whether this package provides any material that is compatible with the air manager. + """ + for material in subdata.get("materials", []): + for compatibility in material.get("compatibility", []): + if compatibility.get("air_manager_optimized", False): + return True + return False + + @pyqtProperty(str, constant = True) + def packageId(self) -> str: + return self._package_id + + @pyqtProperty(str, constant = True) + def packageType(self) -> str: + return self._package_type + + @pyqtProperty(str, constant = True) + def iconUrl(self) -> str: + return self._icon_url + + @pyqtProperty(str, constant = True) + def displayName(self) -> str: + return self._display_name + + @pyqtProperty(bool, constant = True) + def isCheckedByUltimaker(self): + return self._is_checked_by_ultimaker + + @pyqtProperty(str, constant = True) + def packageVersion(self) -> str: + return self._package_version + + @pyqtProperty(str, constant = True) + def packageInfoUrl(self) -> str: + return self._package_info_url + + @pyqtProperty(int, constant = True) + def downloadCount(self) -> str: + return self._download_count + + @pyqtProperty(str, constant = True) + def description(self) -> str: + return self._description + + @pyqtProperty(str, constant = True) + def formattedDescription(self) -> str: + return self._formatted_description + + @pyqtProperty(str, constant = True) + def authorName(self) -> str: + return self._author_name + + @pyqtProperty(str, constant = True) + def authorInfoUrl(self) -> str: + return self._author_info_url + + @pyqtProperty(str, constant = True) + def sectionTitle(self) -> Optional[str]: + return self._section_title + + @pyqtProperty(str, constant = True) + def technicalDataSheet(self) -> str: + return self._technical_data_sheet + + @pyqtProperty(str, constant = True) + def safetyDataSheet(self) -> str: + return self._safety_data_sheet + + @pyqtProperty(str, constant = True) + def whereToBuy(self) -> str: + return self._where_to_buy + + @pyqtProperty("QStringList", constant = True) + def compatiblePrinters(self) -> List[str]: + return self._compatible_printers + + @pyqtProperty("QStringList", constant = True) + def compatibleSupportMaterials(self) -> List[str]: + return self._compatible_support_materials + + @pyqtProperty(bool, constant = True) + def isCompatibleMaterialStation(self) -> bool: + return self._is_compatible_material_station + + @pyqtProperty(bool, constant = True) + def isCompatibleAirManager(self) -> bool: + return self._is_compatible_air_manager + + @pyqtProperty(bool, constant = True) + def isBundled(self) -> bool: + return self._is_bundled + + def setDownloadUrl(self, download_url): + self._download_url = download_url + + # --- manage buttons signals --- + + stateManageButtonChanged = pyqtSignal() + + installPackageTriggered = pyqtSignal(str, str) + + uninstallPackageTriggered = pyqtSignal(str) + + updatePackageTriggered = pyqtSignal(str, str) + + enablePackageTriggered = pyqtSignal(str) + + disablePackageTriggered = pyqtSignal(str) + + busyChanged = pyqtSignal() + + @pyqtSlot() + def install(self): + self.setBusy(True) + self.installPackageTriggered.emit(self.packageId, self._download_url) + + @pyqtSlot() + def update(self): + self.setBusy(True) + self.updatePackageTriggered.emit(self.packageId, self._download_url) + + @pyqtSlot() + def uninstall(self): + self.uninstallPackageTriggered.emit(self.packageId) + + @pyqtProperty(bool, notify= busyChanged) + def busy(self): + """ + Property indicating that some kind of upgrade is active. + """ + return self._is_busy + + @pyqtSlot() + def enable(self): + self.enablePackageTriggered.emit(self.packageId) + + @pyqtSlot() + def disable(self): + self.disablePackageTriggered.emit(self.packageId) + + def setBusy(self, value: bool): + if self._is_busy != value: + self._is_busy = value + try: + self.busyChanged.emit() + except RuntimeError: + pass + + def _packageInstalled(self, package_id: str) -> None: + if self._package_id != package_id: + return + self.setBusy(False) + try: + self.stateManageButtonChanged.emit() + except RuntimeError: + pass + + @pyqtProperty(bool, notify = stateManageButtonChanged) + def isInstalled(self) -> bool: + return self._package_id in self._package_manager.getAllInstalledPackageIDs() + + @pyqtProperty(bool, notify = stateManageButtonChanged) + def isToBeInstalled(self) -> bool: + return self._package_id in self._package_manager.getPackagesToInstall() + + @pyqtProperty(bool, notify = stateManageButtonChanged) + def isActive(self) -> bool: + return not self._package_id in self._plugin_registry.getDisabledPlugins() + + @pyqtProperty(bool, notify = stateManageButtonChanged) + def canDowngrade(self) -> bool: + """Flag if the installed package can be downgraded to a bundled version""" + return self._package_manager.canDowngrade(self._package_id) + + def setCanUpdate(self, value: bool) -> None: + self._can_update = value + self.stateManageButtonChanged.emit() + + @pyqtProperty(bool, fset = setCanUpdate, notify = stateManageButtonChanged) + def canUpdate(self) -> bool: + """Flag indicating if the package can be updated""" + return self._can_update diff --git a/plugins/Marketplace/RemotePackageList.py b/plugins/Marketplace/RemotePackageList.py new file mode 100644 index 0000000000..16b0e721ad --- /dev/null +++ b/plugins/Marketplace/RemotePackageList.py @@ -0,0 +1,151 @@ +# Copyright (c) 2021 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + +from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot +from PyQt5.QtNetwork import QNetworkReply +from typing import Optional, TYPE_CHECKING + +from UM.i18n import i18nCatalog +from UM.Logger import Logger +from UM.TaskManagement.HttpRequestManager import HttpRequestManager # To request the package list from the API. + +from .Constants import PACKAGES_URL # To get the list of packages. Imported this way to prevent circular imports. +from .PackageList import PackageList +from .PackageModel import PackageModel # The contents of this list. + +if TYPE_CHECKING: + from PyQt5.QtCore import QObject + +catalog = i18nCatalog("cura") + + +class RemotePackageList(PackageList): + ITEMS_PER_PAGE = 20 # Pagination of number of elements to download at once. + + def __init__(self, parent: Optional["QObject"] = None) -> None: + super().__init__(parent) + + self._package_type_filter = "" + self._requested_search_string = "" + self._current_search_string = "" + self._request_url = self._initialRequestUrl() + self._ongoing_requests["get_packages"] = None + self.isLoadingChanged.connect(self._onLoadingChanged) + self.isLoadingChanged.emit() + + @pyqtSlot() + def updatePackages(self) -> None: + """ + Make a request for the first paginated page of packages. + + When the request is done, the list will get updated with the new package models. + """ + self.setErrorMessage("") # Clear any previous errors. + self.setIsLoading(True) + + self._ongoing_requests["get_packages"] = HttpRequestManager.getInstance().get( + self._request_url, + scope = self._scope, + callback = self._parseResponse, + error_callback = self._onError + ) + + def reset(self) -> None: + self.clear() + self._request_url = self._initialRequestUrl() + + packageTypeFilterChanged = pyqtSignal() + searchStringChanged = pyqtSignal() + + def setPackageTypeFilter(self, new_filter: str) -> None: + if new_filter != self._package_type_filter: + self._package_type_filter = new_filter + self.reset() + self.packageTypeFilterChanged.emit() + + def setSearchString(self, new_search: str) -> None: + self._requested_search_string = new_search + self._onLoadingChanged() + + @pyqtProperty(str, fset = setPackageTypeFilter, notify = packageTypeFilterChanged) + def packageTypeFilter(self) -> str: + """ + Get the package type this package list is filtering on, like ``plugin`` or ``material``. + :return: The package type this list is filtering on. + """ + return self._package_type_filter + + @pyqtProperty(str, fset = setSearchString, notify = searchStringChanged) + def searchString(self) -> str: + """ + Get the string the user is currently searching for (as in: the list is updating) within the packages, + or an empty string if no extra search filter has to be applied. Does not override package-type filter! + :return: String the user is searching for. Empty denotes 'no search filter'. + """ + return self._current_search_string + + def _onLoadingChanged(self) -> None: + if self._requested_search_string != self._current_search_string and not self._is_loading: + self._current_search_string = self._requested_search_string + self.reset() + self.updatePackages() + self.searchStringChanged.emit() + + def _initialRequestUrl(self) -> str: + """ + Get the URL to request the first paginated page with. + :return: A URL to request. + """ + request_url = f"{PACKAGES_URL}?limit={self.ITEMS_PER_PAGE}" + if self._package_type_filter != "": + request_url += f"&package_type={self._package_type_filter}" + if self._current_search_string != "": + request_url += f"&search={self._current_search_string}" + return request_url + + def _parseResponse(self, reply: "QNetworkReply") -> None: + """ + Parse the response from the package list API request. + + This converts that response into PackageModels, and triggers the ListModel to update. + :param reply: A reply containing information about a number of packages. + """ + response_data = HttpRequestManager.readJSON(reply) + if "data" not in response_data or "links" not in response_data: + Logger.error(f"Could not interpret the server's response. Missing 'data' or 'links' from response data. Keys in response: {response_data.keys()}") + self.setErrorMessage(catalog.i18nc("@info:error", "Could not interpret the server's response.")) + return + + for package_data in response_data["data"]: + package_id = package_data["package_id"] + if package_id in self._package_manager.local_packages_ids: + continue # We should only show packages which are not already installed + try: + package = PackageModel(package_data, parent = self) + self._connectManageButtonSignals(package) + self.appendItem({"package": package}) # Add it to this list model. + except RuntimeError: + # Setting the ownership of this object to not qml can still result in a RuntimeError. Which can occur when quickly toggling + # between de-/constructing RemotePackageLists. This try-except is here to prevent a hard crash when the wrapped C++ object + # was deleted when it was still parsing the response + continue + + self._request_url = response_data["links"].get("next", "") # Use empty string to signify that there is no next page. + self._ongoing_requests["get_packages"] = None + self.setIsLoading(False) + self.setHasMore(self._request_url != "") + + def _onError(self, reply: "QNetworkReply", error: Optional[QNetworkReply.NetworkError]) -> None: + """ + Handles networking and server errors when requesting the list of packages. + :param reply: The reply with packages. This will most likely be incomplete and should be ignored. + :param error: The error status of the request. + """ + if error == QNetworkReply.NetworkError.OperationCanceledError: + Logger.debug("Cancelled request for packages.") + self._ongoing_requests["get_packages"] = None + return # Don't show an error about this to the user. + Logger.error("Could not reach Marketplace server.") + self.setErrorMessage(catalog.i18nc("@info:error", "Could not reach Marketplace.")) + self._ongoing_requests["get_packages"] = None + self.setIsLoading(False) diff --git a/plugins/Marketplace/__init__.py b/plugins/Marketplace/__init__.py new file mode 100644 index 0000000000..bd65062ba6 --- /dev/null +++ b/plugins/Marketplace/__init__.py @@ -0,0 +1,17 @@ +# Copyright (c) 2021 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + +from .Marketplace import Marketplace + +def getMetaData(): + """ + Extension-type plug-ins don't have any specific metadata being used by Cura. + """ + return {} + + +def register(app): + """ + Register the plug-in object with Uranium. + """ + return { "extension": Marketplace() } diff --git a/plugins/Marketplace/plugin.json b/plugins/Marketplace/plugin.json new file mode 100644 index 0000000000..7eeeb5c986 --- /dev/null +++ b/plugins/Marketplace/plugin.json @@ -0,0 +1,8 @@ +{ + "name": "Marketplace", + "author": "Ultimaker B.V.", + "version": "1.0.0", + "api": 7, + "description": "Manages extensions to the application and allows browsing extensions from the Ultimaker website.", + "i18n-catalog": "cura" +} diff --git a/plugins/Toolbox/resources/images/placeholder.svg b/plugins/Marketplace/resources/images/placeholder.svg index cc674a4b38..cc674a4b38 100644 --- a/plugins/Toolbox/resources/images/placeholder.svg +++ b/plugins/Marketplace/resources/images/placeholder.svg diff --git a/plugins/Marketplace/resources/qml/LicenseDialog.qml b/plugins/Marketplace/resources/qml/LicenseDialog.qml new file mode 100644 index 0000000000..1c99569793 --- /dev/null +++ b/plugins/Marketplace/resources/qml/LicenseDialog.qml @@ -0,0 +1,91 @@ +//Copyright (c) 2021 Ultimaker B.V. +//Cura is released under the terms of the LGPLv3 or higher. + +import QtQuick 2.10 +import QtQuick.Dialogs 1.1 +import QtQuick.Window 2.2 +import QtQuick.Controls 2.3 +import QtQuick.Layouts 1.3 + +import UM 1.6 as UM +import Cura 1.6 as Cura + +UM.Dialog +{ + id: licenseDialog + title: catalog.i18nc("@button", "Plugin license agreement") + minimumWidth: UM.Theme.getSize("license_window_minimum").width + minimumHeight: UM.Theme.getSize("license_window_minimum").height + width: minimumWidth + height: minimumHeight + backgroundColor: UM.Theme.getColor("main_background") + + property variant catalog: UM.I18nCatalog { name: "cura" } + + ColumnLayout + { + anchors.fill: parent + spacing: UM.Theme.getSize("thick_margin").height + + Row + { + Layout.fillWidth: true + height: childrenRect.height + spacing: UM.Theme.getSize("default_margin").width + leftPadding: UM.Theme.getSize("narrow_margin").width + + UM.RecolorImage + { + id: icon + width: UM.Theme.getSize("marketplace_large_icon").width + height: UM.Theme.getSize("marketplace_large_icon").height + color: UM.Theme.getColor("text") + source: UM.Theme.getIcon("Certificate", "high") + } + + Label + { + text: catalog.i18nc("@text", "Please read and agree with the plugin licence.") + color: UM.Theme.getColor("text") + font: UM.Theme.getFont("large") + anchors.verticalCenter: icon.verticalCenter + height: UM.Theme.getSize("marketplace_large_icon").height + verticalAlignment: Qt.AlignVCenter + wrapMode: Text.Wrap + renderType: Text.NativeRendering + } + } + + Cura.ScrollableTextArea + { + Layout.fillWidth: true + Layout.fillHeight: true + anchors.topMargin: UM.Theme.getSize("default_margin").height + + textArea.text: licenseContent + textArea.readOnly: true + } + + } + rightButtons: + [ + Cura.PrimaryButton + { + text: catalog.i18nc("@button", "Accept") + onClicked: handler.onLicenseAccepted(packageId) + } + ] + + leftButtons: + [ + Cura.SecondaryButton + { + text: catalog.i18nc("@button", "Decline") + onClicked: handler.onLicenseDeclined(packageId) + } + ] + + onAccepted: handler.onLicenseAccepted(packageId) + onRejected: handler.onLicenseDeclined(packageId) + onClosing: handler.onLicenseDeclined(packageId) +} diff --git a/plugins/Marketplace/resources/qml/ManageButton.qml b/plugins/Marketplace/resources/qml/ManageButton.qml new file mode 100644 index 0000000000..36022ffd54 --- /dev/null +++ b/plugins/Marketplace/resources/qml/ManageButton.qml @@ -0,0 +1,114 @@ +// Copyright (c) 2021 Ultimaker B.V. +// Cura is released under the terms of the LGPLv3 or higher. + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.1 + +import UM 1.6 as UM +import Cura 1.6 as Cura + +Item +{ + id: manageButton + property bool button_style: true + property string text + property bool busy: false + property bool confirmed: false + + implicitWidth: childrenRect.width + implicitHeight: childrenRect.height + + signal clicked() + + property Component primaryButton: Component + { + Cura.PrimaryButton + { + text: manageButton.text + onClicked: manageButton.clicked() + } + } + + property Component secondaryButton: Component + { + Cura.SecondaryButton + { + text: manageButton.text + onClicked: manageButton.clicked() + } + } + + property Component busyButton: Component + { + Item + { + height: UM.Theme.getSize("action_button").height + width: childrenRect.width + + UM.RecolorImage + { + id: busyIndicator + visible: parent.visible + height: UM.Theme.getSize("action_button").height - 2 * UM.Theme.getSize("narrow_margin").height + width: height + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + + source: UM.Theme.getIcon("Spinner") + color: UM.Theme.getColor("primary") + + RotationAnimator + { + target: busyIndicator + running: parent.visible + from: 0 + to: 360 + loops: Animation.Infinite + duration: 2500 + } + } + Label + { + visible: parent.visible + anchors.left: busyIndicator.right + anchors.leftMargin: UM.Theme.getSize("narrow_margin").width + anchors.verticalCenter: parent.verticalCenter + text: manageButton.text + + font: UM.Theme.getFont("medium_bold") + color: UM.Theme.getColor("primary") + } + } + } + + property Component confirmButton: Component + { + Item + { + height: UM.Theme.getSize("action_button").height + width: childrenRect.width + + Label + { + anchors.verticalCenter: parent.verticalCenter + text: manageButton.text + + font: UM.Theme.getFont("medium_bold") + color: UM.Theme.getColor("primary") + } + } + } + + Loader + { + + sourceComponent: + { + if (busy) { return manageButton.busyButton; } + else if (confirmed) { return manageButton.confirmButton; } + else if (manageButton.button_style) { return manageButton.primaryButton; } + else { return manageButton.secondaryButton; } + } + } +} diff --git a/plugins/Marketplace/resources/qml/ManagePackagesButton.qml b/plugins/Marketplace/resources/qml/ManagePackagesButton.qml new file mode 100644 index 0000000000..92e2196beb --- /dev/null +++ b/plugins/Marketplace/resources/qml/ManagePackagesButton.qml @@ -0,0 +1,49 @@ +// Copyright (c) 2021 Ultimaker B.V. +// Cura is released under the terms of the LGPLv3 or higher. + +import UM 1.2 as UM +import Cura 1.6 as Cura + +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +TabButton +{ + id: root + width: UM.Theme.getSize("button_icon").width + UM.Theme.getSize("narrow_margin").width + height: UM.Theme.getSize("button_icon").height + hoverEnabled: true + property color inactiveBackgroundColor : hovered ? UM.Theme.getColor("action_button_hovered") : UM.Theme.getColor("detail_background") + property color activeBackgroundColor : UM.Theme.getColor("main_background") + leftInset: UM.Theme.getSize("narrow_margin").width + + background: Rectangle + { + color: parent.checked ? activeBackgroundColor : inactiveBackgroundColor + border.color: parent.checked ? UM.Theme.getColor("detail_background") : "transparent" + border.width: UM.Theme.getSize("thick_lining").width + radius: Math.round(width * 0.5) + } + + Cura.ToolTip + { + id: tooltip + + tooltipText: catalog.i18nc("@info:tooltip", "Manage packages") + visible: root.hovered + } + + UM.RecolorImage + { + id: icon + + width: UM.Theme.getSize("section_icon").width + height: UM.Theme.getSize("section_icon").height + + color: UM.Theme.getColor("icon") + source: UM.Theme.getIcon("Settings") + anchors.horizontalCenter: parent.horizontalCenter + anchors.horizontalCenterOffset: Math.round(UM.Theme.getSize("narrow_margin").width /2) + anchors.verticalCenter: parent.verticalCenter + } +} diff --git a/plugins/Marketplace/resources/qml/ManagedPackages.qml b/plugins/Marketplace/resources/qml/ManagedPackages.qml new file mode 100644 index 0000000000..8ccaacea46 --- /dev/null +++ b/plugins/Marketplace/resources/qml/ManagedPackages.qml @@ -0,0 +1,25 @@ +// Copyright (c) 2021 Ultimaker B.V. +// Cura is released under the terms of the LGPLv3 or higher. +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +import UM 1.4 as UM + +Packages +{ + pageTitle: catalog.i18nc("@header", "Manage packages") + + bannerVisible: UM.Preferences.getValue("cura/market_place_show_manage_packages_banner"); + bannerIcon: UM.Theme.getIcon("ArrowDoubleCircleRight") + bannerText: catalog.i18nc("@text", "Manage your Ultimaker Cura plugins and material profiles here. Make sure to keep your plugins up to date and backup your setup regularly.") + bannerReadMoreUrl: "" // TODO add when support page is ready + onRemoveBanner: function() { + UM.Preferences.setValue("cura/market_place_show_manage_packages_banner", false); + bannerVisible = false; + } + searchInBrowserUrl: "https://marketplace.ultimaker.com/app/cura/plugins?utm_source=cura&utm_medium=software&utm_campaign=marketplace-search-plugins-browser" + packagesManageableInListView: true + + model: manager.LocalPackageList +} diff --git a/plugins/Marketplace/resources/qml/Marketplace.qml b/plugins/Marketplace/resources/qml/Marketplace.qml new file mode 100644 index 0000000000..fc6d3cd755 --- /dev/null +++ b/plugins/Marketplace/resources/qml/Marketplace.qml @@ -0,0 +1,299 @@ +// Copyright (c) 2021 Ultimaker B.V. +// Cura is released under the terms of the LGPLv3 or higher. + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 +import QtQuick.Window 2.2 + +import UM 1.2 as UM +import Cura 1.6 as Cura + +Window +{ + id: marketplaceDialog + property variant catalog: UM.I18nCatalog { name: "cura" } + + signal searchStringChanged(string new_search) + + minimumWidth: UM.Theme.getSize("modal_window_minimum").width + minimumHeight: UM.Theme.getSize("modal_window_minimum").height + width: minimumWidth + height: minimumHeight + + onVisibleChanged: + { + while(contextStack.depth > 1) + { + contextStack.pop(); //Do NOT use the StackView.Immediate transition here, since it causes the window to stay empty. Seemingly a Qt bug: https://bugreports.qt.io/browse/QTBUG-60670? + } + } + + Connections + { + target: Cura.API.account + function onLoginStateChanged() + { + close(); + } + } + + title: "Marketplace" //Seen by Ultimaker as a brand name, so this doesn't get translated. + modality: Qt.NonModal + + // Background color + Rectangle + { + anchors.fill: parent + color: UM.Theme.getColor("main_background") + + //The Marketplace can have a page in front of everything with package details. The stack view controls its visibility. + StackView + { + id: contextStack + anchors.fill: parent + + initialItem: packageBrowse + + ColumnLayout + { + id: packageBrowse + + spacing: UM.Theme.getSize("default_margin").height + + // Page title. + Item + { + Layout.preferredWidth: parent.width + Layout.preferredHeight: childrenRect.height + UM.Theme.getSize("default_margin").height + + Label + { + id: pageTitle + anchors + { + left: parent.left + leftMargin: UM.Theme.getSize("default_margin").width + right: parent.right + rightMargin: UM.Theme.getSize("default_margin").width + bottom: parent.bottom + } + + font: UM.Theme.getFont("large") + color: UM.Theme.getColor("text") + text: content.item ? content.item.pageTitle: catalog.i18nc("@title", "Loading...") + } + } + + OnboardBanner + { + visible: content.item && content.item.bannerVisible + text: content.item && content.item.bannerText + icon: content.item && content.item.bannerIcon + onRemove: content.item && content.item.onRemoveBanner + readMoreUrl: content.item && content.item.bannerReadMoreUrl + } + + // Search & Top-Level Tabs + Item + { + Layout.preferredHeight: childrenRect.height + Layout.preferredWidth: parent.width - 2 * UM.Theme.getSize("thin_margin").width + RowLayout + { + width: parent.width + height: UM.Theme.getSize("button_icon").height + UM.Theme.getSize("default_margin").height + spacing: UM.Theme.getSize("thin_margin").width + + Item + { + Layout.preferredHeight: parent.height + Layout.preferredWidth: searchBar.visible ? UM.Theme.getSize("thin_margin").width : 0 + Layout.fillWidth: ! searchBar.visible + } + + Cura.SearchBar + { + id: searchBar + Layout.preferredHeight: UM.Theme.getSize("button_icon").height + Layout.fillWidth: true + onTextEdited: searchStringChanged(text) + } + + // Page selection. + TabBar + { + id: pageSelectionTabBar + Layout.alignment: Qt.AlignRight + height: UM.Theme.getSize("button_icon").height + spacing: 0 + background: Rectangle { color: "transparent" } + currentIndex: manager.tabShown + + onCurrentIndexChanged: + { + manager.tabShown = currentIndex + searchBar.text = ""; + searchBar.visible = currentItem.hasSearch; + content.source = currentItem.sourcePage; + } + + PackageTypeTab + { + id: pluginTabText + width: implicitWidth + text: catalog.i18nc("@button", "Plugins") + property string sourcePage: "Plugins.qml" + property bool hasSearch: true + } + PackageTypeTab + { + id: materialsTabText + width: implicitWidth + text: catalog.i18nc("@button", "Materials") + property string sourcePage: "Materials.qml" + property bool hasSearch: true + } + ManagePackagesButton + { + property string sourcePage: "ManagedPackages.qml" + property bool hasSearch: false + + Cura.NotificationIcon + { + anchors + { + horizontalCenter: parent.right + verticalCenter: parent.top + } + visible: CuraApplication.getPackageManager().packagesWithUpdate.length > 0 + + labelText: + { + const itemCount = CuraApplication.getPackageManager().packagesWithUpdate.length + return itemCount > 9 ? "9+" : itemCount + } + } + } + } + + TextMetrics + { + id: pluginTabTextMetrics + text: pluginTabText.text + font: pluginTabText.font + } + TextMetrics + { + id: materialsTabTextMetrics + text: materialsTabText.text + font: materialsTabText.font + } + } + } + + FontMetrics + { + id: fontMetrics + font: UM.Theme.getFont("default") + } + + Cura.TertiaryButton + { + text: catalog.i18nc("@info", "Search in the browser") + iconSource: UM.Theme.getIcon("LinkExternal") + visible: pageSelectionTabBar.currentItem.hasSearch + isIconOnRightSide: true + height: fontMetrics.height + textFont: fontMetrics.font + textColor: UM.Theme.getColor("text") + + onClicked: content.item && Qt.openUrlExternally(content.item.searchInBrowserUrl) + } + + // Page contents. + Rectangle + { + Layout.preferredWidth: parent.width + Layout.fillHeight: true + color: UM.Theme.getColor("detail_background") + + // Page contents. + Loader + { + id: content + anchors.fill: parent + anchors.margins: UM.Theme.getSize("default_margin").width + source: "Plugins.qml" + + Connections + { + target: content + function onLoaded() + { + pageTitle.text = content.item.pageTitle + searchStringChanged.connect(handleSearchStringChanged) + } + function handleSearchStringChanged(new_search) + { + content.item.model.searchString = new_search + } + } + } + } + } + } + } + + Rectangle + { + height: quitButton.height + 2 * UM.Theme.getSize("default_margin").width + color: UM.Theme.getColor("primary") + visible: manager.showRestartNotification + anchors + { + left: parent.left + right: parent.right + bottom: parent.bottom + } + + RowLayout + { + anchors + { + left: parent.left + right: parent.right + verticalCenter: parent.verticalCenter + margins: UM.Theme.getSize("default_margin").width + } + spacing: UM.Theme.getSize("default_margin").width + UM.RecolorImage + { + id: bannerIcon + source: UM.Theme.getIcon("Plugin") + + color: UM.Theme.getColor("primary_button_text") + implicitWidth: UM.Theme.getSize("banner_icon_size").width + implicitHeight: UM.Theme.getSize("banner_icon_size").height + } + Text + { + color: UM.Theme.getColor("primary_button_text") + text: catalog.i18nc("@button", "In order to use the package you will need to restart Cura") + font: UM.Theme.getFont("default") + renderType: Text.NativeRendering + Layout.fillWidth: true + } + Cura.SecondaryButton + { + id: quitButton + text: catalog.i18nc("@info:button, %1 is the application name", "Quit %1").arg(CuraApplication.applicationDisplayName) + onClicked: + { + marketplaceDialog.hide(); + CuraApplication.closeApplication(); + } + } + } + } +} diff --git a/plugins/Marketplace/resources/qml/Materials.qml b/plugins/Marketplace/resources/qml/Materials.qml new file mode 100644 index 0000000000..39fae7042a --- /dev/null +++ b/plugins/Marketplace/resources/qml/Materials.qml @@ -0,0 +1,22 @@ +// Copyright (c) 2021 Ultimaker B.V. +// Cura is released under the terms of the LGPLv3 or higher. + +import UM 1.4 as UM + +Packages +{ + pageTitle: catalog.i18nc("@header", "Install Materials") + + bannerVisible: UM.Preferences.getValue("cura/market_place_show_material_banner") + bannerIcon: UM.Theme.getIcon("Spool") + bannerText: catalog.i18nc("@text", "Select and install material profiles optimised for your Ultimaker 3D printers.") + bannerReadMoreUrl: "" // TODO add when support page is ready + onRemoveBanner: function() { + UM.Preferences.setValue("cura/market_place_show_material_banner", false); + bannerVisible = false; + } + searchInBrowserUrl: "https://marketplace.ultimaker.com/app/cura/materials?utm_source=cura&utm_medium=software&utm_campaign=marketplace-search-materials-browser" + packagesManageableInListView: false + + model: manager.MaterialPackageList +} diff --git a/plugins/Marketplace/resources/qml/OnboardBanner.qml b/plugins/Marketplace/resources/qml/OnboardBanner.qml new file mode 100644 index 0000000000..25e4b53241 --- /dev/null +++ b/plugins/Marketplace/resources/qml/OnboardBanner.qml @@ -0,0 +1,119 @@ +// Copyright (c) 2021 Ultimaker B.V. +// Cura is released under the terms of the LGPLv3 or higher. + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.1 + +import UM 1.6 as UM +import Cura 1.6 as Cura + +// Onboarding banner. +Rectangle +{ + property alias icon: onboardingIcon.source + property alias text: infoText.text + property var onRemove + property string readMoreUrl + + Layout.preferredHeight: childrenRect.height + 2 * UM.Theme.getSize("default_margin").height + Layout.fillWidth: true + Layout.margins: UM.Theme.getSize("default_margin").width + + color: UM.Theme.getColor("action_panel_secondary") + + // Icon + UM.RecolorImage + { + id: onboardingIcon + anchors + { + top: parent.top + left: parent.left + margins: UM.Theme.getSize("default_margin").width + } + width: UM.Theme.getSize("banner_icon_size").width + height: UM.Theme.getSize("banner_icon_size").height + } + + // Close button + UM.SimpleButton + { + id: onboardingClose + anchors + { + top: parent.top + right: parent.right + margins: UM.Theme.getSize("default_margin").width + } + width: UM.Theme.getSize("message_close").width + height: UM.Theme.getSize("message_close").height + color: UM.Theme.getColor("primary_text") + hoverColor: UM.Theme.getColor("primary_text_hover") + iconSource: UM.Theme.getIcon("Cancel") + + onClicked: onRemove() + } + + // Body + Label { + id: infoText + anchors + { + top: parent.top + left: onboardingIcon.right + right: onboardingClose.left + margins: UM.Theme.getSize("default_margin").width + } + + font: UM.Theme.getFont("default") + + renderType: Text.NativeRendering + color: UM.Theme.getColor("primary_text") + wrapMode: Text.Wrap + elide: Text.ElideRight + + onLineLaidOut: + { + if(line.isLast) + { + // Check if read more button still fits after the body text + if (line.implicitWidth + readMoreButton.width + UM.Theme.getSize("default_margin").width > width) + { + // If it does place it after the body text + readMoreButton.anchors.bottomMargin = -(fontMetrics.height); + readMoreButton.anchors.leftMargin = UM.Theme.getSize("thin_margin").width; + } + else + { + // Otherwise place it under the text + readMoreButton.anchors.leftMargin = line.implicitWidth + UM.Theme.getSize("default_margin").width; + readMoreButton.anchors.bottomMargin = 0; + } + } + } + } + + FontMetrics + { + id: fontMetrics + font: UM.Theme.getFont("default") + } + + Cura.TertiaryButton + { + id: readMoreButton + anchors.left: infoText.left + anchors.bottom: infoText.bottom + text: "Learn More" + textFont: UM.Theme.getFont("default") + textColor: infoText.color + leftPadding: 0 + rightPadding: 0 + iconSource: UM.Theme.getIcon("LinkExternal") + isIconOnRightSide: true + height: fontMetrics.height + + onClicked: Qt.openUrlExternally(readMoreUrl) + } +}
\ No newline at end of file diff --git a/plugins/Marketplace/resources/qml/PackageCard.qml b/plugins/Marketplace/resources/qml/PackageCard.qml new file mode 100644 index 0000000000..633d2b25b9 --- /dev/null +++ b/plugins/Marketplace/resources/qml/PackageCard.qml @@ -0,0 +1,101 @@ +// Copyright (c) 2021 Ultimaker B.V. +// Cura is released under the terms of the LGPLv3 or higher. + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.1 + +import UM 1.6 as UM +import Cura 1.6 as Cura + +Rectangle +{ + property alias packageData: packageCardHeader.packageData + property alias manageableInListView: packageCardHeader.showManageButtons + + height: childrenRect.height + color: UM.Theme.getColor("main_background") + radius: UM.Theme.getSize("default_radius").width + + PackageCardHeader + { + id: packageCardHeader + + Item + { + id: shortDescription + + anchors.fill: parent + + Label + { + id: descriptionLabel + width: parent.width + property real lastLineWidth: 0; //Store the width of the last line, to properly position the elision. + + text: packageData.description + textFormat: Text.PlainText //Must be plain text, or we won't get onLineLaidOut signals. Don't auto-detect! + font: UM.Theme.getFont("default") + color: UM.Theme.getColor("text") + maximumLineCount: 2 + wrapMode: Text.Wrap + elide: Text.ElideRight + visible: text !== "" + + onLineLaidOut: + { + if(truncated && line.isLast) + { + let max_line_width = parent.width - readMoreButton.width - fontMetrics.advanceWidth("… ") - 2 * UM.Theme.getSize("default_margin").width; + if(line.implicitWidth > max_line_width) + { + line.width = max_line_width; + } + else + { + line.width = line.implicitWidth - fontMetrics.advanceWidth("…"); //Truncate the ellipsis. We're adding this ourselves. + } + descriptionLabel.lastLineWidth = line.implicitWidth; + } + } + } + Label + { + id: tripleDotLabel + anchors.left: parent.left + anchors.leftMargin: descriptionLabel.lastLineWidth + anchors.bottom: descriptionLabel.bottom + + text: "… " + font: descriptionLabel.font + color: descriptionLabel.color + visible: descriptionLabel.truncated && descriptionLabel.text !== "" + } + Cura.TertiaryButton + { + id: readMoreButton + anchors.right: parent.right + anchors.bottom: descriptionLabel.bottom + height: fontMetrics.height //Height of a single line. + + text: catalog.i18nc("@info", "Read more") + iconSource: UM.Theme.getIcon("LinkExternal") + + visible: descriptionLabel.truncated && descriptionLabel.text !== "" + enabled: visible + leftPadding: UM.Theme.getSize("default_margin").width + rightPadding: UM.Theme.getSize("wide_margin").width + textFont: descriptionLabel.font + isIconOnRightSide: true + + onClicked: Qt.openUrlExternally(packageData.packageInfoUrl) + } + } + } + + FontMetrics + { + id: fontMetrics + font: UM.Theme.getFont("default") + } +} diff --git a/plugins/Marketplace/resources/qml/PackageCardHeader.qml b/plugins/Marketplace/resources/qml/PackageCardHeader.qml new file mode 100644 index 0000000000..3a76f7a959 --- /dev/null +++ b/plugins/Marketplace/resources/qml/PackageCardHeader.qml @@ -0,0 +1,215 @@ +// Copyright (c) 2021 Ultimaker B.V. +// Cura is released under the terms of the LGPLv3 or higher. + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.1 + +import UM 1.6 as UM +import Cura 1.6 as Cura + +// As both the PackageCard and Package contain similar components; a package icon, title, author bar. These components +// are combined into the reusable "PackageCardHeader" component +Item +{ + default property alias contents: contentItem.children; + + property var packageData + property bool showManageButtons: false + + width: parent.width + height: UM.Theme.getSize("card").height + + // card icon + Image + { + id: packageItem + anchors + { + top: parent.top + left: parent.left + margins: UM.Theme.getSize("default_margin").width + } + width: UM.Theme.getSize("card_icon").width + height: width + + source: packageData.iconUrl != "" ? packageData.iconUrl : "../images/placeholder.svg" + } + + ColumnLayout + { + anchors + { + left: packageItem.right + leftMargin: UM.Theme.getSize("default_margin").width + right: parent.right + rightMargin: UM.Theme.getSize("default_margin").width + top: parent.top + topMargin: UM.Theme.getSize("narrow_margin").height + } + height: packageItem.height + packageItem.anchors.margins * 2 + + // Title row. + RowLayout + { + id: titleBar + Layout.preferredWidth: parent.width + Layout.preferredHeight: childrenRect.height + + Label + { + text: packageData.displayName + font: UM.Theme.getFont("medium_bold") + color: UM.Theme.getColor("text") + verticalAlignment: Text.AlignTop + } + VerifiedIcon + { + enabled: packageData.isCheckedByUltimaker + visible: packageData.isCheckedByUltimaker + } + + Label + { + id: packageVersionLabel + text: packageData.packageVersion + font: UM.Theme.getFont("default") + color: UM.Theme.getColor("text") + Layout.fillWidth: true + } + + Button + { + id: externalLinkButton + + // For some reason if i set padding, they don't match up. If i set all of them explicitly, it does work? + leftPadding: UM.Theme.getSize("narrow_margin").width + rightPadding: UM.Theme.getSize("narrow_margin").width + topPadding: UM.Theme.getSize("narrow_margin").width + bottomPadding: UM.Theme.getSize("narrow_margin").width + + Layout.preferredWidth: UM.Theme.getSize("card_tiny_icon").width + 2 * padding + Layout.preferredHeight: UM.Theme.getSize("card_tiny_icon").width + 2 * padding + contentItem: UM.RecolorImage + { + source: UM.Theme.getIcon("LinkExternal") + color: UM.Theme.getColor("icon") + implicitWidth: UM.Theme.getSize("card_tiny_icon").width + implicitHeight: UM.Theme.getSize("card_tiny_icon").height + } + + background: Rectangle + { + color: externalLinkButton.hovered ? UM.Theme.getColor("action_button_hovered"): "transparent" + radius: externalLinkButton.width / 2 + } + onClicked: Qt.openUrlExternally(packageData.authorInfoUrl) + } + } + + // When a package Card companent is created and children are provided to it they are rendered here + Item { + id: contentItem + Layout.fillHeight: true + Layout.preferredWidth: parent.width + } + + // Author and action buttons. + RowLayout + { + id: authorAndActionButton + Layout.preferredWidth: parent.width + Layout.preferredHeight: childrenRect.height + + spacing: UM.Theme.getSize("narrow_margin").width + + // label "By" + Label + { + id: authorBy + Layout.alignment: Qt.AlignCenter + + text: catalog.i18nc("@label", "By") + font: UM.Theme.getFont("default") + color: UM.Theme.getColor("text") + } + + // clickable author name + Item + { + Layout.fillWidth: true + implicitHeight: authorBy.height + Layout.alignment: Qt.AlignTop + Cura.TertiaryButton + { + text: packageData.authorName + textFont: UM.Theme.getFont("default_bold") + textColor: UM.Theme.getColor("text") // override normal link color + leftPadding: 0 + rightPadding: 0 + iconSource: UM.Theme.getIcon("LinkExternal") + isIconOnRightSide: true + + onClicked: Qt.openUrlExternally(packageData.authorInfoUrl) + } + } + + ManageButton + { + id: enableManageButton + visible: showManageButtons && packageData.isInstalled && !packageData.isToBeInstalled && packageData.packageType != "material" + enabled: !packageData.busy + + button_style: !packageData.isActive + Layout.alignment: Qt.AlignTop + + text: button_style ? catalog.i18nc("@button", "Enable") : catalog.i18nc("@button", "Disable") + + onClicked: packageData.isActive ? packageData.disable(): packageData.enable() + } + + ManageButton + { + id: installManageButton + visible: showManageButtons && (packageData.canDowngrade || !packageData.isBundled) + enabled: !packageData.busy + busy: packageData.busy + button_style: !(packageData.isInstalled || packageData.isToBeInstalled) + Layout.alignment: Qt.AlignTop + + text: + { + if (packageData.canDowngrade) + { + if (busy) { return catalog.i18nc("@button", "Downgrading..."); } + else { return catalog.i18nc("@button", "Downgrade"); } + } + if (!(packageData.isInstalled || packageData.isToBeInstalled)) + { + if (busy) { return catalog.i18nc("@button", "Installing..."); } + else { return catalog.i18nc("@button", "Install"); } + } + else + { + return catalog.i18nc("@button", "Uninstall"); + } + } + + onClicked: packageData.isInstalled || packageData.isToBeInstalled ? packageData.uninstall(): packageData.install() + } + + ManageButton + { + id: updateManageButton + visible: showManageButtons && packageData.canUpdate + enabled: !packageData.busy + busy: packageData.busy + Layout.alignment: Qt.AlignTop + + text: busy ? catalog.i18nc("@button", "Updating..."): catalog.i18nc("@button", "Update") + + onClicked: packageData.update() + } + } + } +} diff --git a/plugins/Marketplace/resources/qml/PackageDetails.qml b/plugins/Marketplace/resources/qml/PackageDetails.qml new file mode 100644 index 0000000000..2599c7f28c --- /dev/null +++ b/plugins/Marketplace/resources/qml/PackageDetails.qml @@ -0,0 +1,96 @@ +// Copyright (c) 2021 Ultimaker B.V. +// Cura is released under the terms of the LGPLv3 or higher. + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.3 + +import Cura 1.0 as Cura +import UM 1.0 as UM + +Item +{ + id: detailPage + property var packageData: packages.selectedPackage + property string title: catalog.i18nc("@header", "Package details") + + RowLayout + { + id: header + anchors + { + top: parent.top + topMargin: UM.Theme.getSize("default_margin").height + left: parent.left + leftMargin: UM.Theme.getSize("default_margin").width + right: parent.right + rightMargin: anchors.leftMargin + } + + spacing: UM.Theme.getSize("default_margin").width + + Cura.SecondaryButton + { + Layout.alignment: Qt.AlignVCenter + Layout.preferredHeight: UM.Theme.getSize("action_button").height + Layout.preferredWidth: height + + onClicked: contextStack.pop() //Remove this page, returning to the main package list or whichever thing is beneath it. + + tooltip: catalog.i18nc("@button:tooltip", "Back") + toolTipContentAlignment: Cura.ToolTip.ContentAlignment.AlignRight + leftPadding: UM.Theme.getSize("narrow_margin").width + rightPadding: leftPadding + iconSource: UM.Theme.getIcon("ArrowLeft") + iconSize: height - leftPadding * 2 + } + + Label + { + Layout.alignment: Qt.AlignVCenter + Layout.fillWidth: true + + text: detailPage.title + font: UM.Theme.getFont("large") + color: UM.Theme.getColor("text") + } + } + + Rectangle + { + anchors + { + top: header.bottom + topMargin: UM.Theme.getSize("default_margin").height + left: parent.left + right: parent.right + bottom: parent.bottom + } + color: UM.Theme.getColor("detail_background") + + ScrollView + { + anchors.fill: parent + + clip: true //Need to clip, not for the bottom (which is off the window) but for the top (which would overlap the header). + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + contentHeight: packagePage.height + UM.Theme.getSize("default_margin").height * 2 + + PackagePage + { + id: packagePage + anchors + { + left: parent.left + leftMargin: UM.Theme.getSize("default_margin").width + right: parent.right + rightMargin: anchors.leftMargin + top: parent.top + topMargin: UM.Theme.getSize("default_margin").height + } + + packageData: detailPage.packageData + } + } + } +}
\ No newline at end of file diff --git a/plugins/Marketplace/resources/qml/PackagePage.qml b/plugins/Marketplace/resources/qml/PackagePage.qml new file mode 100644 index 0000000000..21c400fff2 --- /dev/null +++ b/plugins/Marketplace/resources/qml/PackagePage.qml @@ -0,0 +1,295 @@ +// Copyright (c) 2021 Ultimaker B.V. +// Cura is released under the terms of the LGPLv3 or higher. + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.1 + +import UM 1.6 as UM +import Cura 1.6 as Cura + +Rectangle +{ + id: root + property alias packageData: packageCardHeader.packageData + + height: childrenRect.height + color: UM.Theme.getColor("main_background") + radius: UM.Theme.getSize("default_radius").width + + Column + { + width: parent.width + + spacing: 0 + + Item + { + width: parent.width + height: UM.Theme.getSize("card").height + + PackageCardHeader + { + id: packageCardHeader + showManageButtons: true + + anchors.fill: parent + + Row + { + id: downloadCount + Layout.preferredWidth: parent.width + Layout.fillHeight: true + + UM.RecolorImage + { + id: downloadsIcon + width: UM.Theme.getSize("card_tiny_icon").width + height: UM.Theme.getSize("card_tiny_icon").height + + source: UM.Theme.getIcon("Download") + color: UM.Theme.getColor("text") + } + + Label + { + anchors.verticalCenter: downloadsIcon.verticalCenter + + color: UM.Theme.getColor("text") + font: UM.Theme.getFont("default") + text: packageData.downloadCount + } + } + } + } + + Column + { + id: extendedDescription + width: parent.width + + padding: UM.Theme.getSize("default_margin").width + topPadding: 0 + spacing: UM.Theme.getSize("default_margin").height + + Label + { + width: parent.width - parent.padding * 2 + + text: catalog.i18nc("@header", "Description") + font: UM.Theme.getFont("medium_bold") + color: UM.Theme.getColor("text") + elide: Text.ElideRight + } + + Label + { + width: parent.width - parent.padding * 2 + + text: packageData.formattedDescription + font: UM.Theme.getFont("medium") + color: UM.Theme.getColor("text") + linkColor: UM.Theme.getColor("text_link") + wrapMode: Text.Wrap + textFormat: Text.RichText + + onLinkActivated: UM.UrlUtil.openUrl(link, ["http", "https"]) + } + + Column //Separate column to have no spacing between compatible printers. + { + id: compatiblePrinterColumn + width: parent.width - parent.padding * 2 + + visible: packageData.packageType === "material" + spacing: 0 + + Label + { + width: parent.width + + text: catalog.i18nc("@header", "Compatible printers") + font: UM.Theme.getFont("medium_bold") + color: UM.Theme.getColor("text") + elide: Text.ElideRight + } + + Repeater + { + model: packageData.compatiblePrinters + + Label + { + width: compatiblePrinterColumn.width + + text: modelData + font: UM.Theme.getFont("medium") + color: UM.Theme.getColor("text") + elide: Text.ElideRight + } + } + + Label + { + width: parent.width + + visible: packageData.compatiblePrinters.length == 0 + text: "(" + catalog.i18nc("@info", "No compatibility information") + ")" + font: UM.Theme.getFont("medium") + color: UM.Theme.getColor("text") + elide: Text.ElideRight + } + } + + Column + { + id: compatibleSupportMaterialColumn + width: parent.width - parent.padding * 2 + + visible: packageData.packageType === "material" + spacing: 0 + + Label + { + width: parent.width + + text: catalog.i18nc("@header", "Compatible support materials") + font: UM.Theme.getFont("medium_bold") + color: UM.Theme.getColor("text") + elide: Text.ElideRight + } + + Repeater + { + model: packageData.compatibleSupportMaterials + + Label + { + width: compatibleSupportMaterialColumn.width + + text: modelData + font: UM.Theme.getFont("medium") + color: UM.Theme.getColor("text") + elide: Text.ElideRight + } + } + + Label + { + width: parent.width + + visible: packageData.compatibleSupportMaterials.length == 0 + text: "(" + catalog.i18nc("@info No materials", "None") + ")" + font: UM.Theme.getFont("medium") + color: UM.Theme.getColor("text") + elide: Text.ElideRight + } + } + + Column + { + width: parent.width - parent.padding * 2 + + visible: packageData.packageType === "material" + spacing: 0 + + Label + { + width: parent.width + + text: catalog.i18nc("@header", "Compatible with Material Station") + font: UM.Theme.getFont("medium_bold") + color: UM.Theme.getColor("text") + elide: Text.ElideRight + } + + Label + { + width: parent.width + + text: packageData.isCompatibleMaterialStation ? catalog.i18nc("@info", "Yes") : catalog.i18nc("@info", "No") + font: UM.Theme.getFont("medium") + color: UM.Theme.getColor("text") + elide: Text.ElideRight + } + } + + Column + { + width: parent.width - parent.padding * 2 + + visible: packageData.packageType === "material" + spacing: 0 + + Label + { + width: parent.width + + text: catalog.i18nc("@header", "Optimized for Air Manager") + font: UM.Theme.getFont("medium_bold") + color: UM.Theme.getColor("text") + elide: Text.ElideRight + } + + Label + { + width: parent.width + + text: packageData.isCompatibleAirManager ? catalog.i18nc("@info", "Yes") : catalog.i18nc("@info", "No") + font: UM.Theme.getFont("medium") + color: UM.Theme.getColor("text") + elide: Text.ElideRight + } + } + + Row + { + id: externalButtonRow + anchors.horizontalCenter: parent.horizontalCenter + + spacing: UM.Theme.getSize("narrow_margin").width + + Cura.SecondaryButton + { + text: packageData.packageType === "plugin" ? catalog.i18nc("@button", "Visit plug-in website") : catalog.i18nc("@button", "Website") + iconSource: UM.Theme.getIcon("Globe") + outlineColor: "transparent" + onClicked: Qt.openUrlExternally(packageData.packageInfoUrl) + } + + Cura.SecondaryButton + { + visible: packageData.packageType === "material" + text: catalog.i18nc("@button", "Buy spool") + iconSource: UM.Theme.getIcon("ShoppingCart") + outlineColor: "transparent" + onClicked: Qt.openUrlExternally(packageData.whereToBuy) + } + + Cura.SecondaryButton + { + visible: packageData.packageType === "material" + text: catalog.i18nc("@button", "Safety datasheet") + iconSource: UM.Theme.getIcon("Warning") + outlineColor: "transparent" + onClicked: Qt.openUrlExternally(packageData.safetyDataSheet) + } + + Cura.SecondaryButton + { + visible: packageData.packageType === "material" + text: catalog.i18nc("@button", "Technical datasheet") + iconSource: UM.Theme.getIcon("DocumentFilled") + outlineColor: "transparent" + onClicked: Qt.openUrlExternally(packageData.technicalDataSheet) + } + } + } + } + + FontMetrics + { + id: fontMetrics + font: UM.Theme.getFont("default") + } +} diff --git a/plugins/Marketplace/resources/qml/PackageTypeTab.qml b/plugins/Marketplace/resources/qml/PackageTypeTab.qml new file mode 100644 index 0000000000..79eaa9a16c --- /dev/null +++ b/plugins/Marketplace/resources/qml/PackageTypeTab.qml @@ -0,0 +1,33 @@ +// Copyright (c) 2021 Ultimaker B.V. +// Cura is released under the terms of the LGPLv3 or higher. + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import UM 1.0 as UM + +TabButton +{ + property string pageTitle + padding: UM.Theme.getSize("narrow_margin").width + horizontalPadding: UM.Theme.getSize("default_margin").width + hoverEnabled: true + property color inactiveBackgroundColor : hovered ? UM.Theme.getColor("action_button_hovered") : UM.Theme.getColor("detail_background") + property color activeBackgroundColor : UM.Theme.getColor("main_background") + + background: Rectangle + { + anchors.fill: parent + color: parent.checked ? activeBackgroundColor : inactiveBackgroundColor + border.color: UM.Theme.getColor("detail_background") + border.width: UM.Theme.getSize("thick_lining").width + } + + contentItem: Label + { + text: parent.text + font: UM.Theme.getFont("medium_bold") + color: UM.Theme.getColor("text") + width: contentWidth + anchors.centerIn: parent + } +}
\ No newline at end of file diff --git a/plugins/Marketplace/resources/qml/Packages.qml b/plugins/Marketplace/resources/qml/Packages.qml new file mode 100644 index 0000000000..194c90c248 --- /dev/null +++ b/plugins/Marketplace/resources/qml/Packages.qml @@ -0,0 +1,232 @@ +// Copyright (c) 2021 Ultimaker B.V. +// Cura is released under the terms of the LGPLv3 or higher. + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import UM 1.4 as UM + + +ListView +{ + id: packages + width: parent.width + + property string pageTitle + property var selectedPackage + property string searchInBrowserUrl + property bool bannerVisible + property var bannerIcon + property string bannerText + property string bannerReadMoreUrl + property var onRemoveBanner + property bool packagesManageableInListView + + clip: true + + Component.onCompleted: model.updatePackages() + Component.onDestruction: model.cleanUpAPIRequest() + + spacing: UM.Theme.getSize("default_margin").height + + section.property: "package.sectionTitle" + section.delegate: Rectangle + { + width: packages.width + height: sectionHeaderText.height + UM.Theme.getSize("default_margin").height + + color: UM.Theme.getColor("detail_background") + + Label + { + id: sectionHeaderText + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + + text: section + font: UM.Theme.getFont("large") + color: UM.Theme.getColor("text") + } + } + + ScrollBar.vertical: ScrollBar + { + // Vertical ScrollBar, styled similarly to the scrollBar in the settings panel + id: verticalScrollBar + visible: packages.contentHeight > packages.height + + background: Item{} + + contentItem: Rectangle + { + id: scrollViewHandle + implicitWidth: UM.Theme.getSize("scrollbar").width + radius: Math.round(implicitWidth / 2) + color: verticalScrollBar.pressed ? UM.Theme.getColor("scrollbar_handle_down") : verticalScrollBar.hovered ? UM.Theme.getColor("scrollbar_handle_hover") : UM.Theme.getColor("scrollbar_handle") + Behavior on color { ColorAnimation { duration: 50; } } + } + } + + delegate: MouseArea + { + id: cardMouseArea + width: parent ? parent.width : 0 + height: childrenRect.height + + hoverEnabled: true + onClicked: + { + packages.selectedPackage = model.package; + contextStack.push(packageDetailsComponent); + } + + PackageCard + { + manageableInListView: packages.packagesManageableInListView + packageData: model.package + width: parent.width - UM.Theme.getSize("default_margin").width - UM.Theme.getSize("narrow_margin").width + color: cardMouseArea.containsMouse ? UM.Theme.getColor("action_button_hovered") : UM.Theme.getColor("main_background") + } + } + + Component + { + id: packageDetailsComponent + + PackageDetails + { + packageData: packages.selectedPackage + title: packages.pageTitle + } + } + + //Wrapper item to add spacing between content and footer. + footer: Item + { + width: parent.width - UM.Theme.getSize("default_margin").width - UM.Theme.getSize("narrow_margin").width + height: model.hasFooter || packages.model.errorMessage != "" ? UM.Theme.getSize("card").height + packages.spacing : 0 + visible: model.hasFooter || packages.model.errorMessage != "" + Button + { + id: loadMoreButton + width: parent.width + height: UM.Theme.getSize("card").height + anchors.bottom: parent.bottom + + enabled: packages.model.hasMore && !packages.model.isLoading || packages.model.errorMessage != "" + onClicked: packages.model.updatePackages() //Load next page in plug-in list. + + background: Rectangle + { + anchors.fill: parent + radius: UM.Theme.getSize("default_radius").width + color: UM.Theme.getColor("main_background") + } + + Row + { + anchors.centerIn: parent + + spacing: UM.Theme.getSize("thin_margin").width + + states: + [ + State + { + name: "Error" + when: packages.model.errorMessage != "" + PropertyChanges + { + target: errorIcon + visible: true + } + PropertyChanges + { + target: loadMoreIcon + visible: false + } + PropertyChanges + { + target: loadMoreLabel + text: catalog.i18nc("@button", "Failed to load packages:") + " " + packages.model.errorMessage + "\n" + catalog.i18nc("@button", "Retry?") + } + }, + State + { + name: "Loading" + when: packages.model.isLoading + PropertyChanges + { + target: loadMoreIcon + source: UM.Theme.getIcon("ArrowDoubleCircleRight") + color: UM.Theme.getColor("action_button_disabled_text") + } + PropertyChanges + { + target: loadMoreLabel + text: catalog.i18nc("@button", "Loading") + color: UM.Theme.getColor("action_button_disabled_text") + } + }, + State + { + name: "LastPage" + when: !packages.model.hasMore + PropertyChanges + { + target: loadMoreIcon + visible: false + } + PropertyChanges + { + target: loadMoreLabel + text: packages.model.count > 0 ? catalog.i18nc("@message", "No more results to load") : catalog.i18nc("@message", "No results found with current filter") + color: UM.Theme.getColor("action_button_disabled_text") + } + } + ] + + Item + { + width: (errorIcon.visible || loadMoreIcon.visible) ? UM.Theme.getSize("small_button_icon").width : 0 + height: UM.Theme.getSize("small_button_icon").height + anchors.verticalCenter: loadMoreLabel.verticalCenter + + UM.StatusIcon + { + id: errorIcon + anchors.fill: parent + + status: UM.StatusIcon.Status.ERROR + visible: false + } + UM.RecolorImage + { + id: loadMoreIcon + anchors.fill: parent + + source: UM.Theme.getIcon("ArrowDown") + color: UM.Theme.getColor("secondary_button_text") + + RotationAnimator + { + target: loadMoreIcon + from: 0 + to: 360 + duration: 1000 + loops: Animation.Infinite + running: packages.model.isLoading + alwaysRunToEnd: true + } + } + } + Label + { + id: loadMoreLabel + text: catalog.i18nc("@button", "Load more") + font: UM.Theme.getFont("medium_bold") + color: UM.Theme.getColor("secondary_button_text") + } + } + } + } +} diff --git a/plugins/Marketplace/resources/qml/Plugins.qml b/plugins/Marketplace/resources/qml/Plugins.qml new file mode 100644 index 0000000000..9983a827d8 --- /dev/null +++ b/plugins/Marketplace/resources/qml/Plugins.qml @@ -0,0 +1,22 @@ +// Copyright (c) 2021 Ultimaker B.V. +// Cura is released under the terms of the LGPLv3 or higher. + +import UM 1.4 as UM + +Packages +{ + pageTitle: catalog.i18nc("@header", "Install Plugins") + + bannerVisible: UM.Preferences.getValue("cura/market_place_show_plugin_banner") + bannerIcon: UM.Theme.getIcon("Shop") + bannerText: catalog.i18nc("@text", "Streamline your workflow and customize your Ultimaker Cura experience with plugins contributed by our amazing community of users.") + bannerReadMoreUrl: "" // TODO add when support page is ready + onRemoveBanner: function() { + UM.Preferences.setValue("cura/market_place_show_plugin_banner", false) + bannerVisible = false; + } + searchInBrowserUrl: "https://marketplace.ultimaker.com/app/cura/plugins?utm_source=cura&utm_medium=software&utm_campaign=marketplace-search-plugins-browser" + packagesManageableInListView: false + + model: manager.PluginPackageList +} diff --git a/plugins/Marketplace/resources/qml/VerifiedIcon.qml b/plugins/Marketplace/resources/qml/VerifiedIcon.qml new file mode 100644 index 0000000000..30ef3080a0 --- /dev/null +++ b/plugins/Marketplace/resources/qml/VerifiedIcon.qml @@ -0,0 +1,45 @@ +// Copyright (c) 2021 Ultimaker B.V. +// Cura is released under the terms of the LGPLv3 or higher. + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.1 + +import UM 1.6 as UM +import Cura 1.6 as Cura +Control +{ + implicitWidth: UM.Theme.getSize("card_tiny_icon").width + implicitHeight: UM.Theme.getSize("card_tiny_icon").height + + Cura.ToolTip + { + tooltipText: + { + switch(packageData.packageType) + { + case "plugin": return catalog.i18nc("@info", "Ultimaker Verified Plug-in"); + case "material": return catalog.i18nc("@info", "Ultimaker Certified Material"); + default: return catalog.i18nc("@info", "Ultimaker Verified Package"); + } + } + visible: parent.hovered + targetPoint: Qt.point(0, Math.round(parent.y + parent.height / 4)) + } + + Rectangle + { + anchors.fill: parent + color: UM.Theme.getColor("action_button_hovered") + radius: width + UM.RecolorImage + { + anchors.fill: parent + color: UM.Theme.getColor("primary") + source: packageData.packageType == "plugin" ? UM.Theme.getIcon("CheckCircle") : UM.Theme.getIcon("Certified") + } + } + + //NOTE: Can we link to something here? (Probably a static link explaining what verified is): + // onClicked: Qt.openUrlExternally( XXXXXX ) +}
\ No newline at end of file diff --git a/plugins/PerObjectSettingsTool/PerObjectItem.qml b/plugins/PerObjectSettingsTool/PerObjectItem.qml index bb1c31e1f3..9700b2265b 100644 --- a/plugins/PerObjectSettingsTool/PerObjectItem.qml +++ b/plugins/PerObjectSettingsTool/PerObjectItem.qml @@ -6,7 +6,9 @@ import QtQuick.Layouts 1.1 import QtQuick.Controls 1.1 import QtQuick.Controls.Styles 1.1 -import UM 1.2 as UM +import UM 1.5 as UM + +import Cura 1.0 as Cura UM.TooltipArea { @@ -16,7 +18,7 @@ UM.TooltipArea width: childrenRect.width; height: childrenRect.height; - CheckBox + UM.CheckBox { id: check diff --git a/plugins/PerObjectSettingsTool/SettingPickDialog.qml b/plugins/PerObjectSettingsTool/SettingPickDialog.qml index 1bba094e49..0b03ef5008 100644 --- a/plugins/PerObjectSettingsTool/SettingPickDialog.qml +++ b/plugins/PerObjectSettingsTool/SettingPickDialog.qml @@ -2,7 +2,7 @@ import QtQuick 2.2 import QtQuick.Controls 1.2 import QtQuick.Controls.Styles 1.2 -import UM 1.2 as UM +import UM 1.5 as UM import Cura 1.0 as Cura import ".." @@ -57,13 +57,14 @@ UM.Dialog onTextChanged: settingPickDialog.updateFilter() } - CheckBox + UM.CheckBox { id: toggleShowAll anchors { top: parent.top right: parent.right + verticalCenter: filterInput.verticalCenter } text: catalog.i18nc("@label:checkbox", "Show all") } diff --git a/plugins/PostProcessingPlugin/PostProcessingPlugin.py b/plugins/PostProcessingPlugin/PostProcessingPlugin.py index 8968e2c547..755d815d0a 100644 --- a/plugins/PostProcessingPlugin/PostProcessingPlugin.py +++ b/plugins/PostProcessingPlugin/PostProcessingPlugin.py @@ -193,6 +193,8 @@ class PostProcessingPlugin(QObject, Extension): spec = importlib.util.spec_from_file_location(__name__ + "." + script_name, file_path) + if spec is None: + continue loaded_script = importlib.util.module_from_spec(spec) if spec.loader is None: continue diff --git a/plugins/PostProcessingPlugin/PostProcessingPlugin.qml b/plugins/PostProcessingPlugin/PostProcessingPlugin.qml index bbba2e7621..bd94d1fdfd 100644 --- a/plugins/PostProcessingPlugin/PostProcessingPlugin.qml +++ b/plugins/PostProcessingPlugin/PostProcessingPlugin.qml @@ -527,10 +527,8 @@ UM.Dialog visible: activeScriptsList.count > 0 anchors { - top: parent.top - right: parent.right - rightMargin: (-0.5 * width) | 0 - topMargin: (-0.5 * height) | 0 + horizontalCenter: parent.right + verticalCenter: parent.top } labelText: activeScriptsList.count diff --git a/plugins/Toolbox/__init__.py b/plugins/Toolbox/__init__.py deleted file mode 100644 index 51f1b643d0..0000000000 --- a/plugins/Toolbox/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright (c) 2018 Ultimaker B.V. -# Toolbox is released under the terms of the LGPLv3 or higher. - -from .src import Toolbox -from .src.CloudSync.SyncOrchestrator import SyncOrchestrator - - -def getMetaData(): - return {} - - -def register(app): - return { - "extension": [Toolbox.Toolbox(app), SyncOrchestrator(app)] - } diff --git a/plugins/Toolbox/plugin.json b/plugins/Toolbox/plugin.json deleted file mode 100644 index ed4a3eae97..0000000000 --- a/plugins/Toolbox/plugin.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "Toolbox", - "author": "Ultimaker B.V.", - "version": "1.0.1", - "api": 7, - "description": "Find, manage and install new Cura packages." -} diff --git a/plugins/Toolbox/resources/images/Shop.svg b/plugins/Toolbox/resources/images/Shop.svg deleted file mode 100755 index 5056a25c51..0000000000 --- a/plugins/Toolbox/resources/images/Shop.svg +++ /dev/null @@ -1,6 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> -<path d="M19,3H5C3.3,3,2,4.3,2,6v3c0,1.5,0.8,2.7,2,3.4V22h16v-9.6c1.2-0.7,2-2,2-3.4V6C22,4.3,20.7,3,19,3z - M10,5h4v4c0,1.1-0.9,2-2,2s-2-0.9-2-2V5z M4,9V5h4v4c0,1.1-0.9,2-2,2S4,10.1,4,9z M18,20h-4v-5h-4v5H6v-7c1.2,0,2.3-0.5,3-1.4 - c0.7,0.8,1.8,1.4,3,1.4s2.3-0.5,3-1.4c0.7,0.8,1.8,1.4,3,1.4V20z M20,9c0,1.1-0.9,2-2,2s-2-0.9-2-2V5h4V9z" /> -</svg> diff --git a/plugins/Toolbox/resources/images/installed_check.svg b/plugins/Toolbox/resources/images/installed_check.svg deleted file mode 100644 index 1f1302770b..0000000000 --- a/plugins/Toolbox/resources/images/installed_check.svg +++ /dev/null @@ -1,8 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Generator: Adobe Illustrator 22.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> -<svg version="1.1" id="Layer_3" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" - viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve"> -<path d="M0,512h512V0L0,512z M440.4,318.3L331.2,431.6c-1.4,1.4-2.7,2-4.8,2c-2,0-3.4-0.7-4.8-2l-53.3-57.3l-1.4-2 - c-1.4-1.4-2-3.4-2-4.8c0-1.4,0.7-3.4,2-4.8l9.6-9.6c2.7-2.7,6.8-2.7,9.6,0l0.7,0.7l37.6,40.2c1.4,1.4,3.4,1.4,4.8,0l91.4-94.9h0.7 - c2.7-2.7,6.8-2.7,9.6,0l9.5,9.6C443.1,311.5,443.1,315.6,440.4,318.3z"/> -</svg> diff --git a/plugins/Toolbox/resources/qml/Toolbox.qml b/plugins/Toolbox/resources/qml/Toolbox.qml deleted file mode 100644 index b67d175194..0000000000 --- a/plugins/Toolbox/resources/qml/Toolbox.qml +++ /dev/null @@ -1,112 +0,0 @@ -// Copyright (c) 2018 Ultimaker B.V. -// Toolbox is released under the terms of the LGPLv3 or higher. - -// Main window for the Toolbox - -import QtQuick 2.2 -import QtQuick.Dialogs 1.1 -import QtQuick.Window 2.2 -import UM 1.1 as UM - -import "./pages" -import "./dialogs" -import "./components" - -Window -{ - id: base - property var selection: null - title: catalog.i18nc("@title", "Marketplace") - modality: Qt.ApplicationModal - flags: Qt.Dialog | Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowCloseButtonHint - - width: UM.Theme.getSize("large_popup_dialog").width - height: UM.Theme.getSize("large_popup_dialog").height - minimumWidth: width - maximumWidth: minimumWidth - minimumHeight: height - maximumHeight: minimumHeight - color: UM.Theme.getColor("main_background") - UM.I18nCatalog - { - id: catalog - name: "cura" - } - Item - { - anchors.fill: parent - - WelcomePage - { - visible: toolbox.viewPage === "welcome" - } - - ToolboxHeader - { - id: header - visible: toolbox.viewPage !== "welcome" - } - - Item - { - id: mainView - width: parent.width - z: parent.z - 1 - anchors - { - top: header.bottom - bottom: footer.top - } - // TODO: This could be improved using viewFilter instead of viewCategory - ToolboxLoadingPage - { - id: viewLoading - visible: toolbox.viewCategory !== "installed" && toolbox.viewPage === "loading" - } - ToolboxErrorPage - { - id: viewErrored - visible: toolbox.viewCategory !== "installed" && toolbox.viewPage === "errored" - } - ToolboxDownloadsPage - { - id: viewDownloads - visible: toolbox.viewCategory !== "installed" && toolbox.viewPage === "overview" - } - ToolboxDetailPage - { - id: viewDetail - visible: toolbox.viewCategory !== "installed" && toolbox.viewPage === "detail" - } - ToolboxAuthorPage - { - id: viewAuthor - visible: toolbox.viewCategory !== "installed" && toolbox.viewPage === "author" - } - ToolboxInstalledPage - { - id: installedPluginList - visible: toolbox.viewCategory === "installed" - } - } - - ToolboxFooter - { - id: footer - visible: toolbox.restartRequired - height: visible ? UM.Theme.getSize("toolbox_footer").height : 0 - } - - Connections - { - target: toolbox - function onShowLicenseDialog() { licenseDialog.show() } - function onCloseLicenseDialog() { licenseDialog.close() } - } - - ToolboxLicenseDialog - { - id: licenseDialog - } - } -} diff --git a/plugins/Toolbox/resources/qml/components/ToolboxActionButtonStyle.qml b/plugins/Toolbox/resources/qml/components/ToolboxActionButtonStyle.qml deleted file mode 100644 index eff74278c9..0000000000 --- a/plugins/Toolbox/resources/qml/components/ToolboxActionButtonStyle.qml +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) 2018 Ultimaker B.V. -// Toolbox is released under the terms of the LGPLv3 or higher. - -import QtQuick 2.2 -import QtQuick.Controls 1.4 -import QtQuick.Controls.Styles 1.4 -import UM 1.1 as UM - -ButtonStyle -{ - background: Rectangle - { - implicitWidth: UM.Theme.getSize("toolbox_action_button").width - implicitHeight: UM.Theme.getSize("toolbox_action_button").height - color: "transparent" - border - { - width: UM.Theme.getSize("default_lining").width - color: UM.Theme.getColor("lining") - } - } - label: Label - { - text: control.text - color: UM.Theme.getColor("text") - verticalAlignment: Text.AlignVCenter - horizontalAlignment: Text.AlignHCenter - } -}
\ No newline at end of file diff --git a/plugins/Toolbox/resources/qml/components/ToolboxBackColumn.qml b/plugins/Toolbox/resources/qml/components/ToolboxBackColumn.qml deleted file mode 100644 index 9874a977f5..0000000000 --- a/plugins/Toolbox/resources/qml/components/ToolboxBackColumn.qml +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright (c) 2018 Ultimaker B.V. -// Toolbox is released under the terms of the LGPLv3 or higher. - -import QtQuick 2.10 -import QtQuick.Controls 1.4 -import QtQuick.Controls.Styles 1.4 -import UM 1.1 as UM - -Item -{ - id: sidebar - height: parent.height - width: UM.Theme.getSize("toolbox_back_column").width - anchors - { - top: parent.top - left: parent.left - topMargin: UM.Theme.getSize("wide_margin").height - leftMargin: UM.Theme.getSize("default_margin").width - rightMargin: UM.Theme.getSize("default_margin").width - } - Button - { - id: button - text: catalog.i18nc("@action:button", "Back") - enabled: !toolbox.isDownloading - UM.RecolorImage - { - id: backArrow - anchors - { - verticalCenter: parent.verticalCenter - left: parent.left - rightMargin: UM.Theme.getSize("default_margin").width - } - width: UM.Theme.getSize("standard_arrow").width - height: UM.Theme.getSize("standard_arrow").height - sourceSize - { - width: width - height: height - } - color: button.enabled ? (button.hovered ? UM.Theme.getColor("primary") : UM.Theme.getColor("text")) : UM.Theme.getColor("text_inactive") - source: UM.Theme.getIcon("ChevronSingleLeft") - } - width: UM.Theme.getSize("toolbox_back_button").width - height: UM.Theme.getSize("toolbox_back_button").height - onClicked: - { - toolbox.viewPage = "overview" - if (toolbox.viewCategory == "material") - { - toolbox.filterModelByProp("authors", "package_types", "material") - } - else if (toolbox.viewCategory == "plugin") - { - toolbox.filterModelByProp("packages", "type", "plugin") - } - - } - style: ButtonStyle - { - background: Rectangle - { - color: "transparent" - } - label: Label - { - id: labelStyle - text: control.text - color: control.enabled ? (control.hovered ? UM.Theme.getColor("primary") : UM.Theme.getColor("text")) : UM.Theme.getColor("text_inactive") - font: UM.Theme.getFont("medium_bold") - horizontalAlignment: Text.AlignLeft - anchors - { - left: parent.left - leftMargin: UM.Theme.getSize("default_margin").width - } - width: control.width - renderType: Text.NativeRendering - } - } - } -} diff --git a/plugins/Toolbox/resources/qml/components/ToolboxCompatibilityChart.qml b/plugins/Toolbox/resources/qml/components/ToolboxCompatibilityChart.qml deleted file mode 100644 index e1f88a473f..0000000000 --- a/plugins/Toolbox/resources/qml/components/ToolboxCompatibilityChart.qml +++ /dev/null @@ -1,209 +0,0 @@ -// Copyright (c) 2019 Ultimaker B.V. -// Toolbox is released under the terms of the LGPLv3 or higher. - -import QtQuick 2.10 -import QtQuick.Controls 1.4 - -import UM 1.5 as UM - -Item -{ - id: base - - property var packageData - property var technicalDataSheetUrl: packageData.links.technicalDataSheet - property var safetyDataSheetUrl: packageData.links.safetyDataSheet - property var printingGuidelinesUrl: packageData.links.printingGuidelines - property var materialWebsiteUrl: packageData.links.website - - height: childrenRect.height - onVisibleChanged: packageData.type === "material" && (compatibilityItem.visible || dataSheetLinks.visible) - - Column - { - id: compatibilityItem - visible: packageData.has_configs - width: parent.width - // This is a bit of a hack, but the whole QML is pretty messy right now. This needs a big overhaul. - height: visible ? heading.height + table.height: 0 - - Label - { - id: heading - width: parent.width - text: catalog.i18nc("@label", "Compatibility") - wrapMode: Text.WordWrap - color: UM.Theme.getColor("text_medium") - font: UM.Theme.getFont("medium") - renderType: Text.NativeRendering - } - - TableView - { - id: table - width: parent.width - frameVisible: false - - // Workaround for scroll issues (QTBUG-49652) - flickableItem.interactive: false - Component.onCompleted: - { - for (var i = 0; i < flickableItem.children.length; ++i) - { - flickableItem.children[i].enabled = false - } - } - selectionMode: 0 - model: packageData.supported_configs - headerDelegate: Rectangle - { - color: UM.Theme.getColor("main_background") - height: UM.Theme.getSize("toolbox_chart_row").height - Label - { - anchors.verticalCenter: parent.verticalCenter - elide: Text.ElideRight - text: styleData.value || "" - color: UM.Theme.getColor("text") - font: UM.Theme.getFont("default_bold") - renderType: Text.NativeRendering - } - Rectangle - { - anchors.bottom: parent.bottom - height: UM.Theme.getSize("default_lining").height - width: parent.width - color: "black" - } - } - rowDelegate: Item - { - height: UM.Theme.getSize("toolbox_chart_row").height - Label - { - anchors.verticalCenter: parent.verticalCenter - elide: Text.ElideRight - text: styleData.value || "" - color: UM.Theme.getColor("text_medium") - font: UM.Theme.getFont("default") - renderType: Text.NativeRendering - } - } - itemDelegate: Item - { - height: UM.Theme.getSize("toolbox_chart_row").height - Label - { - anchors.verticalCenter: parent.verticalCenter - elide: Text.ElideRight - text: styleData.value || "" - color: UM.Theme.getColor("text_medium") - font: UM.Theme.getFont("default") - renderType: Text.NativeRendering - } - } - - Component - { - id: columnTextDelegate - Label - { - anchors.fill: parent - verticalAlignment: Text.AlignVCenter - text: styleData.value || "" - elide: Text.ElideRight - color: UM.Theme.getColor("text_medium") - font: UM.Theme.getFont("default") - renderType: Text.NativeRendering - } - } - - TableViewColumn - { - role: "machine" - title: catalog.i18nc("@label:table_header", "Machine") - width: Math.floor(table.width * 0.25) - delegate: columnTextDelegate - } - TableViewColumn - { - role: "print_core" - title: "Print Core" //This term should not be translated. - width: Math.floor(table.width * 0.2) - } - TableViewColumn - { - role: "build_plate" - title: catalog.i18nc("@label:table_header", "Build Plate") - width: Math.floor(table.width * 0.225) - } - TableViewColumn - { - role: "support_material" - title: catalog.i18nc("@label:table_header", "Support") - width: Math.floor(table.width * 0.225) - } - TableViewColumn - { - role: "quality" - title: catalog.i18nc("@label:table_header", "Quality") - width: Math.floor(table.width * 0.1) - } - } - } - - Label - { - id: dataSheetLinks - anchors.top: compatibilityItem.bottom - anchors.topMargin: UM.Theme.getSize("narrow_margin").height - visible: base.technicalDataSheetUrl !== undefined || - base.safetyDataSheetUrl !== undefined || - base.printingGuidelinesUrl !== undefined || - base.materialWebsiteUrl !== undefined - - text: - { - var result = "" - if (base.technicalDataSheetUrl !== undefined) - { - var tds_name = catalog.i18nc("@action:label", "Technical Data Sheet") - result += "<a href='%1'>%2</a>".arg(base.technicalDataSheetUrl).arg(tds_name) - } - if (base.safetyDataSheetUrl !== undefined) - { - if (result.length > 0) - { - result += "<br/>" - } - var sds_name = catalog.i18nc("@action:label", "Safety Data Sheet") - result += "<a href='%1'>%2</a>".arg(base.safetyDataSheetUrl).arg(sds_name) - } - if (base.printingGuidelinesUrl !== undefined) - { - if (result.length > 0) - { - result += "<br/>" - } - var pg_name = catalog.i18nc("@action:label", "Printing Guidelines") - result += "<a href='%1'>%2</a>".arg(base.printingGuidelinesUrl).arg(pg_name) - } - if (base.materialWebsiteUrl !== undefined) - { - if (result.length > 0) - { - result += "<br/>" - } - var pg_name = catalog.i18nc("@action:label", "Website") - result += "<a href='%1'>%2</a>".arg(base.materialWebsiteUrl).arg(pg_name) - } - - return result - } - font: UM.Theme.getFont("default") - color: UM.Theme.getColor("text") - linkColor: UM.Theme.getColor("text_link") - onLinkActivated: UM.UrlUtil.openUrl(link, ["http", "https"]) - renderType: Text.NativeRendering - } -} diff --git a/plugins/Toolbox/resources/qml/components/ToolboxDetailList.qml b/plugins/Toolbox/resources/qml/components/ToolboxDetailList.qml deleted file mode 100644 index 22c6b6045f..0000000000 --- a/plugins/Toolbox/resources/qml/components/ToolboxDetailList.qml +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (c) 2019 Ultimaker B.V. -// Toolbox is released under the terms of the LGPLv3 or higher. - -import QtQuick 2.10 -import QtQuick.Controls 2.3 -import UM 1.1 as UM - -Item -{ - id: detailList - ScrollView - { - clip: true - anchors.fill: detailList - - Column - { - anchors - { - right: parent.right - topMargin: UM.Theme.getSize("wide_margin").height - bottomMargin: UM.Theme.getSize("wide_margin").height - top: parent.top - } - height: childrenRect.height + 2 * UM.Theme.getSize("wide_margin").height - spacing: UM.Theme.getSize("default_margin").height - - Repeater - { - model: toolbox.packagesModel - delegate: Loader - { - // FIXME: When using asynchronous loading, on Mac and Windows, the tile may fail to load complete, - // leaving an empty space below the title part. We turn it off for now to make it work on Mac and - // Windows. - // Can be related to this QT bug: https://bugreports.qt.io/browse/QTBUG-50992 - asynchronous: false - source: "ToolboxDetailTile.qml" - } - } - } - } -} diff --git a/plugins/Toolbox/resources/qml/components/ToolboxDetailTile.qml b/plugins/Toolbox/resources/qml/components/ToolboxDetailTile.qml deleted file mode 100644 index 5badc6b66d..0000000000 --- a/plugins/Toolbox/resources/qml/components/ToolboxDetailTile.qml +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright (c) 2019 Ultimaker B.V. -// Toolbox is released under the terms of the LGPLv3 or higher. - -import QtQuick 2.10 -import QtQuick.Controls 2.3 - -import UM 1.1 as UM - -Item -{ - id: tile - width: detailList.width - UM.Theme.getSize("wide_margin").width - height: normalData.height + 2 * UM.Theme.getSize("wide_margin").height - Column - { - id: normalData - - anchors - { - top: parent.top - left: parent.left - right: controls.left - rightMargin: UM.Theme.getSize("wide_margin").width - } - - Label - { - width: parent.width - height: UM.Theme.getSize("toolbox_property_label").height - text: model.name - wrapMode: Text.WordWrap - color: UM.Theme.getColor("text") - font: UM.Theme.getFont("medium_bold") - renderType: Text.NativeRendering - } - - Label - { - width: parent.width - text: model.description - maximumLineCount: 25 - elide: Text.ElideRight - wrapMode: Text.WordWrap - color: UM.Theme.getColor("text") - font: UM.Theme.getFont("default") - renderType: Text.NativeRendering - } - - ToolboxCompatibilityChart - { - width: parent.width - packageData: model - } - } - - ToolboxDetailTileActions - { - id: controls - anchors.right: tile.right - anchors.top: tile.top - width: childrenRect.width - height: childrenRect.height - packageData: model - } - - Rectangle - { - color: UM.Theme.getColor("lining") - width: tile.width - height: UM.Theme.getSize("default_lining").height - anchors.top: normalData.bottom - anchors.topMargin: UM.Theme.getSize("default_margin").height + UM.Theme.getSize("wide_margin").height //Normal margin for spacing after chart, wide margin between items. - } -} diff --git a/plugins/Toolbox/resources/qml/components/ToolboxDetailTileActions.qml b/plugins/Toolbox/resources/qml/components/ToolboxDetailTileActions.qml deleted file mode 100644 index d683877605..0000000000 --- a/plugins/Toolbox/resources/qml/components/ToolboxDetailTileActions.qml +++ /dev/null @@ -1,121 +0,0 @@ -// Copyright (c) 2018 Ultimaker B.V. -// Toolbox is released under the terms of the LGPLv3 or higher. - -import QtQuick 2.10 -import QtQuick.Controls 1.4 -import QtQuick.Controls.Styles 1.4 -import UM 1.5 as UM -import Cura 1.1 as Cura - -Column -{ - property bool installed: toolbox.isInstalled(model.id) - property bool canUpdate: CuraApplication.getPackageManager().packagesWithUpdate.indexOf(model.id) != -1 - property bool loginRequired: model.login_required && !Cura.API.account.isLoggedIn - property var packageData - - width: UM.Theme.getSize("toolbox_action_button").width - spacing: UM.Theme.getSize("narrow_margin").height - - Item - { - width: installButton.width - height: installButton.height - ToolboxProgressButton - { - id: installButton - active: toolbox.isDownloading && toolbox.activePackage == model - onReadyAction: - { - toolbox.activePackage = model - toolbox.startDownload(model.download_url) - } - onActiveAction: toolbox.cancelDownload() - - // Don't allow installing while another download is running - enabled: installed || (!(toolbox.isDownloading && toolbox.activePackage != model) && !loginRequired) - opacity: enabled ? 1.0 : 0.5 - visible: !updateButton.visible && !installed // Don't show when the update button is visible - } - - Cura.SecondaryButton - { - id: installedButton - visible: installed - onClicked: toolbox.viewCategory = "installed" - text: catalog.i18nc("@action:button", "Installed") - fixedWidthMode: true - width: installButton.width - height: installButton.height - } - } - - Label - { - wrapMode: Text.WordWrap - text: catalog.i18nc("@label:The string between <a href=> and </a> is the highlighted link", "<a href='%1'>Log in</a> is required to install or update") - font: UM.Theme.getFont("default") - color: UM.Theme.getColor("text") - linkColor: UM.Theme.getColor("text_link") - visible: loginRequired - width: installButton.width - renderType: Text.NativeRendering - - MouseArea - { - anchors.fill: parent - onClicked: Cura.API.account.login() - } - } - - Label - { - property var whereToBuyUrl: - { - var pg_name = "whereToBuy" - return (pg_name in packageData.links) ? packageData.links[pg_name] : undefined - } - - renderType: Text.NativeRendering - text: catalog.i18nc("@label:The string between <a href=> and </a> is the highlighted link", "<a href='%1'>Buy material spools</a>") - linkColor: UM.Theme.getColor("text_link") - visible: whereToBuyUrl != undefined - font: UM.Theme.getFont("default") - color: UM.Theme.getColor("text") - MouseArea - { - anchors.fill: parent - onClicked: UM.UrlUtil.openUrl(parent.whereToBuyUrl, ["https", "http"]) - } - } - - ToolboxProgressButton - { - id: updateButton - active: toolbox.isDownloading && toolbox.activePackage == model - readyLabel: catalog.i18nc("@action:button", "Update") - activeLabel: catalog.i18nc("@action:button", "Updating") - completeLabel: catalog.i18nc("@action:button", "Updated") - - onReadyAction: - { - toolbox.activePackage = model - toolbox.update(model.id) - } - onActiveAction: toolbox.cancelDownload() - // Don't allow installing while another download is running - enabled: !(toolbox.isDownloading && toolbox.activePackage != model) && !loginRequired - opacity: enabled ? 1.0 : 0.5 - visible: canUpdate - } - - Connections - { - target: toolbox - function onInstallChanged() { installed = toolbox.isInstalled(model.id) } - function onFilterChanged() - { - installed = toolbox.isInstalled(model.id) - } - } -} diff --git a/plugins/Toolbox/resources/qml/components/ToolboxDownloadsGrid.qml b/plugins/Toolbox/resources/qml/components/ToolboxDownloadsGrid.qml deleted file mode 100644 index 6682281a31..0000000000 --- a/plugins/Toolbox/resources/qml/components/ToolboxDownloadsGrid.qml +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright (c) 2018 Ultimaker B.V. -// Toolbox is released under the terms of the LGPLv3 or higher. - -import QtQuick 2.10 -import QtQuick.Controls 2.3 -import UM 1.1 as UM - -Column -{ - property var heading: "" - property var model - id: gridArea - height: childrenRect.height + 2 * padding - width: parent.width - spacing: UM.Theme.getSize("default_margin").height - padding: UM.Theme.getSize("wide_margin").height - Label - { - id: heading - text: gridArea.heading - width: parent.width - color: UM.Theme.getColor("text_medium") - font: UM.Theme.getFont("large") - renderType: Text.NativeRendering - } - Grid - { - id: grid - width: parent.width - 2 * parent.padding - columns: 2 - columnSpacing: UM.Theme.getSize("default_margin").height - rowSpacing: UM.Theme.getSize("default_margin").width - Repeater - { - model: gridArea.model - delegate: Loader - { - asynchronous: true - width: Math.round((grid.width - (grid.columns - 1) * grid.columnSpacing) / grid.columns) - height: UM.Theme.getSize("toolbox_thumbnail_small").height - source: "ToolboxDownloadsGridTile.qml" - } - } - } -} diff --git a/plugins/Toolbox/resources/qml/components/ToolboxDownloadsGridTile.qml b/plugins/Toolbox/resources/qml/components/ToolboxDownloadsGridTile.qml deleted file mode 100644 index c310bd7121..0000000000 --- a/plugins/Toolbox/resources/qml/components/ToolboxDownloadsGridTile.qml +++ /dev/null @@ -1,125 +0,0 @@ -// Copyright (c) 2018 Ultimaker B.V. -// Toolbox is released under the terms of the LGPLv3 or higher. - -import QtQuick 2.10 -import QtQuick.Controls 1.4 -import QtQuick.Controls.Styles 1.4 -import QtQuick.Layouts 1.3 -import UM 1.1 as UM -import Cura 1.1 as Cura - -Item -{ - id: toolboxDownloadsGridTile - property int packageCount: (toolbox.viewCategory == "material" && model.type === undefined) ? toolbox.getTotalNumberOfMaterialPackagesByAuthor(model.id) : 1 - property int installedPackages: (toolbox.viewCategory == "material" && model.type === undefined) ? toolbox.getNumberOfInstalledPackagesByAuthor(model.id) : (toolbox.isInstalled(model.id) ? 1 : 0) - height: childrenRect.height - Layout.alignment: Qt.AlignTop | Qt.AlignLeft - - MouseArea - { - anchors.fill: parent - hoverEnabled: true - onEntered: thumbnail.border.color = UM.Theme.getColor("primary") - onExited: thumbnail.border.color = UM.Theme.getColor("lining") - onClicked: - { - base.selection = model - switch(toolbox.viewCategory) - { - case "material": - - // If model has a type, it must be a package - if (model.type !== undefined) - { - toolbox.viewPage = "detail" - toolbox.filterModelByProp("packages", "id", model.id) - } - else - { - toolbox.viewPage = "author" - toolbox.setFilters("packages", { - "author_id": model.id, - "type": "material" - }) - } - break - default: - toolbox.viewPage = "detail" - toolbox.filterModelByProp("packages", "id", model.id) - break - } - } - } - - Rectangle - { - id: thumbnail - width: UM.Theme.getSize("toolbox_thumbnail_small").width - height: UM.Theme.getSize("toolbox_thumbnail_small").height - color: UM.Theme.getColor("main_background") - border.width: UM.Theme.getSize("default_lining").width - border.color: UM.Theme.getColor("lining") - - Image - { - anchors.centerIn: parent - width: UM.Theme.getSize("toolbox_thumbnail_small").width - UM.Theme.getSize("wide_margin").width - height: UM.Theme.getSize("toolbox_thumbnail_small").height - UM.Theme.getSize("wide_margin").width - sourceSize.width: width - sourceSize.height: height - fillMode: Image.PreserveAspectFit - source: model.icon_url || "../../images/placeholder.svg" - mipmap: true - } - UM.RecolorImage - { - width: (parent.width * 0.4) | 0 - height: (parent.height * 0.4) | 0 - anchors - { - bottom: parent.bottom - right: parent.right - } - sourceSize.height: height - visible: installedPackages != 0 - color: (installedPackages >= packageCount) ? UM.Theme.getColor("primary") : UM.Theme.getColor("border") - source: "../../images/installed_check.svg" - } - } - Item - { - anchors - { - left: thumbnail.right - leftMargin: Math.floor(UM.Theme.getSize("narrow_margin").width) - right: parent.right - top: parent.top - bottom: parent.bottom - } - - Label - { - id: name - text: model.name - width: parent.width - elide: Text.ElideRight - color: UM.Theme.getColor("text") - font: UM.Theme.getFont("default_bold") - } - Label - { - id: info - text: model.description - elide: Text.ElideRight - width: parent.width - wrapMode: Text.WordWrap - color: UM.Theme.getColor("text") - font: UM.Theme.getFont("default") - anchors.top: name.bottom - anchors.bottom: parent.bottom - verticalAlignment: Text.AlignVCenter - maximumLineCount: 2 - } - } -} diff --git a/plugins/Toolbox/resources/qml/components/ToolboxDownloadsShowcase.qml b/plugins/Toolbox/resources/qml/components/ToolboxDownloadsShowcase.qml deleted file mode 100644 index a42a10aa29..0000000000 --- a/plugins/Toolbox/resources/qml/components/ToolboxDownloadsShowcase.qml +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright (c) 2018 Ultimaker B.V. -// Toolbox is released under the terms of the LGPLv3 or higher. - -import QtQuick 2.10 -import QtQuick.Controls 1.4 -import QtQuick.Controls.Styles 1.4 -import UM 1.1 as UM - -Rectangle -{ - color: UM.Theme.getColor("toolbox_premium_packages_background") - height: childrenRect.height - width: parent.width - Column - { - height: childrenRect.height + 2 * padding - spacing: UM.Theme.getSize("default_margin").height - width: parent.width - padding: UM.Theme.getSize("wide_margin").height - Item - { - width: parent.width - parent.padding * 2 - height: childrenRect.height - Label - { - id: heading - text: catalog.i18nc("@label", "Premium") - width: contentWidth - height: contentHeight - color: UM.Theme.getColor("text_medium") - font: UM.Theme.getFont("large") - renderType: Text.NativeRendering - } - UM.TooltipArea - { - width: childrenRect.width - height: childrenRect.height - anchors.right: parent.right - text: catalog.i18nc("@info:tooltip", "Go to Web Marketplace") - Label - { - text: "<a href='%2'>".arg(toolbox.getWebMarketplaceUrl("materials") + "?utm_source=cura&utm_medium=software&utm_campaign=marketplace-search") + catalog.i18nc("@label", "Search materials") + "</a>" - width: contentWidth - height: contentHeight - horizontalAlignment: Text.AlignRight - font: UM.Theme.getFont("default") - renderType: Text.NativeRendering - - linkColor: UM.Theme.getColor("text_link") - onLinkActivated: Qt.openUrlExternally(link) - - visible: toolbox.viewCategory === "material" - } - } - } - Grid - { - height: childrenRect.height - spacing: UM.Theme.getSize("wide_margin").width - columns: 3 - anchors.horizontalCenter: parent.horizontalCenter - - Repeater - { - model: - { - if (toolbox.viewCategory == "plugin") - { - return toolbox.pluginsShowcaseModel - } - if (toolbox.viewCategory == "material") - { - return toolbox.materialsShowcaseModel - } - } - delegate: Loader - { - asynchronous: true - source: "ToolboxDownloadsShowcaseTile.qml" - } - } - } - } -} diff --git a/plugins/Toolbox/resources/qml/components/ToolboxDownloadsShowcaseTile.qml b/plugins/Toolbox/resources/qml/components/ToolboxDownloadsShowcaseTile.qml deleted file mode 100644 index 6695921126..0000000000 --- a/plugins/Toolbox/resources/qml/components/ToolboxDownloadsShowcaseTile.qml +++ /dev/null @@ -1,114 +0,0 @@ -// Copyright (c) 2018 Ultimaker B.V. -// Cura is released under the terms of the LGPLv3 or higher. - -import QtQuick 2.10 -import QtQuick.Controls 1.4 -import QtQuick.Controls.Styles 1.4 - -import UM 1.1 as UM - -Rectangle -{ - property int packageCount: toolbox.viewCategory == "material" ? toolbox.getTotalNumberOfMaterialPackagesByAuthor(model.id) : 1 - property int installedPackages: toolbox.viewCategory == "material" ? toolbox.getNumberOfInstalledPackagesByAuthor(model.id) : (toolbox.isInstalled(model.id) ? 1 : 0) - id: tileBase - width: UM.Theme.getSize("toolbox_thumbnail_large").width + (2 * UM.Theme.getSize("default_lining").width) - height: thumbnail.height + packageName.height + UM.Theme.getSize("default_margin").width - border.width: UM.Theme.getSize("default_lining").width - border.color: UM.Theme.getColor("lining") - color: UM.Theme.getColor("main_background") - Image - { - id: thumbnail - height: UM.Theme.getSize("toolbox_thumbnail_large").height - 4 * UM.Theme.getSize("default_margin").height - width: UM.Theme.getSize("toolbox_thumbnail_large").height - 4 * UM.Theme.getSize("default_margin").height - sourceSize.height: height - sourceSize.width: width - fillMode: Image.PreserveAspectFit - source: model.icon_url || "../../images/placeholder.svg" - mipmap: true - anchors - { - top: parent.top - topMargin: UM.Theme.getSize("default_margin").height - horizontalCenter: parent.horizontalCenter - } - } - Label - { - id: packageName - text: model.name - anchors - { - horizontalCenter: parent.horizontalCenter - top: thumbnail.bottom - } - verticalAlignment: Text.AlignVCenter - horizontalAlignment: Text.AlignHCenter - renderType: Text.NativeRendering - height: UM.Theme.getSize("toolbox_heading_label").height - width: parent.width - UM.Theme.getSize("default_margin").width - wrapMode: Text.WordWrap - elide: Text.ElideRight - font: UM.Theme.getFont("medium_bold") - color: UM.Theme.getColor("text") - } - UM.RecolorImage - { - width: (parent.width * 0.20) | 0 - height: width - anchors - { - bottom: bottomBorder.top - right: parent.right - } - visible: installedPackages != 0 - color: (installedPackages >= packageCount) ? UM.Theme.getColor("primary") : UM.Theme.getColor("border") - source: "../../images/installed_check.svg" - } - - Rectangle - { - id: bottomBorder - color: UM.Theme.getColor("primary") - anchors.bottom: parent.bottom - width: parent.width - height: UM.Theme.getSize("toolbox_header_highlight").height - } - - MouseArea - { - anchors.fill: parent - hoverEnabled: true - onEntered: tileBase.border.color = UM.Theme.getColor("primary") - onExited: tileBase.border.color = UM.Theme.getColor("lining") - onClicked: - { - base.selection = model - switch(toolbox.viewCategory) - { - case "material": - - // If model has a type, it must be a package - if (model.type !== undefined) - { - toolbox.viewPage = "detail" - toolbox.filterModelByProp("packages", "id", model.id) - } - else - { - toolbox.viewPage = "author" - toolbox.setFilters("packages", { - "author_id": model.id, - "type": "material" - }) - } - break - default: - toolbox.viewPage = "detail" - toolbox.filterModelByProp("packages", "id", model.id) - break - } - } - } -} diff --git a/plugins/Toolbox/resources/qml/components/ToolboxFooter.qml b/plugins/Toolbox/resources/qml/components/ToolboxFooter.qml deleted file mode 100644 index 9863bd9a93..0000000000 --- a/plugins/Toolbox/resources/qml/components/ToolboxFooter.qml +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright (c) 2018 Ultimaker B.V. -// Toolbox is released under the terms of the LGPLv3 or higher. - -import QtQuick 2.10 -import QtQuick.Controls 2.3 - -import UM 1.1 as UM -import Cura 1.0 as Cura - -Item -{ - id: footer - width: parent.width - anchors.bottom: parent.bottom - height: visible ? UM.Theme.getSize("toolbox_footer").height : 0 - - Label - { - text: catalog.i18nc("@info", "You will need to restart Cura before changes in packages have effect.") - color: UM.Theme.getColor("text") - height: UM.Theme.getSize("toolbox_footer_button").height - verticalAlignment: Text.AlignVCenter - wrapMode: Text.WordWrap - anchors - { - top: restartButton.top - left: parent.left - leftMargin: UM.Theme.getSize("wide_margin").width - right: restartButton.left - rightMargin: UM.Theme.getSize("default_margin").width - } - renderType: Text.NativeRendering - } - - Cura.PrimaryButton - { - id: restartButton - anchors - { - top: parent.top - topMargin: UM.Theme.getSize("default_margin").height - right: parent.right - rightMargin: UM.Theme.getSize("wide_margin").width - } - height: UM.Theme.getSize("toolbox_footer_button").height - text: catalog.i18nc("@info:button, %1 is the application name", "Quit %1").arg(CuraApplication.applicationDisplayName) - onClicked: - { - base.hide() - toolbox.restart() - } - } - - ToolboxShadow - { - visible: footer.visible - anchors.bottom: footer.top - reversed: true - } -} diff --git a/plugins/Toolbox/resources/qml/components/ToolboxHeader.qml b/plugins/Toolbox/resources/qml/components/ToolboxHeader.qml deleted file mode 100644 index 2c43110af9..0000000000 --- a/plugins/Toolbox/resources/qml/components/ToolboxHeader.qml +++ /dev/null @@ -1,112 +0,0 @@ -// Copyright (c) 2020 Ultimaker B.V. -// Toolbox is released under the terms of the LGPLv3 or higher. - -import QtQuick 2.10 -import QtQuick.Controls 1.4 - -import UM 1.4 as UM -import Cura 1.0 as Cura - -Item -{ - id: header - width: parent.width - height: UM.Theme.getSize("toolbox_header").height - Row - { - id: bar - spacing: UM.Theme.getSize("default_margin").width - height: childrenRect.height - width: childrenRect.width - anchors - { - left: parent.left - leftMargin: UM.Theme.getSize("default_margin").width - } - - ToolboxTabButton - { - id: pluginsTabButton - text: catalog.i18nc("@title:tab", "Plugins") - active: toolbox.viewCategory == "plugin" && enabled - enabled: !toolbox.isDownloading && toolbox.viewPage != "loading" && toolbox.viewPage != "errored" - onClicked: - { - toolbox.filterModelByProp("packages", "type", "plugin") - toolbox.viewCategory = "plugin" - toolbox.viewPage = "overview" - } - } - - ToolboxTabButton - { - id: materialsTabButton - text: catalog.i18nc("@title:tab", "Materials") - active: toolbox.viewCategory == "material" && enabled - enabled: !toolbox.isDownloading && toolbox.viewPage != "loading" && toolbox.viewPage != "errored" - onClicked: - { - toolbox.filterModelByProp("authors", "package_types", "material") - toolbox.viewCategory = "material" - toolbox.viewPage = "overview" - } - } - - ToolboxTabButton - { - id: installedTabButton - text: catalog.i18nc("@title:tab", "Installed") - active: toolbox.viewCategory == "installed" - enabled: !toolbox.isDownloading - onClicked: toolbox.viewCategory = "installed" - width: UM.Theme.getSize("toolbox_header_tab").width + marketplaceNotificationIcon.width - UM.Theme.getSize("default_margin").width - } - - - } - - Cura.NotificationIcon - { - id: marketplaceNotificationIcon - visible: CuraApplication.getPackageManager().packagesWithUpdate.length > 0 - anchors.right: bar.right - labelText: - { - const itemCount = CuraApplication.getPackageManager().packagesWithUpdate.length - return itemCount > 9 ? "9+" : itemCount - } - } - - - UM.TooltipArea - { - id: webMarketplaceButtonTooltipArea - width: childrenRect.width - height: parent.height - text: catalog.i18nc("@info:tooltip", "Go to Web Marketplace") - anchors - { - right: parent.right - rightMargin: UM.Theme.getSize("default_margin").width - verticalCenter: parent.verticalCenter - } - acceptedButtons: Qt.LeftButton - onClicked: Qt.openUrlExternally(toolbox.getWebMarketplaceUrl("plugins") + "?utm_source=cura&utm_medium=software&utm_campaign=marketplace-button") - UM.RecolorImage - { - id: cloudMarketplaceButton - source: "../../images/Shop.svg" - color: UM.Theme.getColor(webMarketplaceButtonTooltipArea.containsMouse ? "primary" : "text") - height: parent.height / 2 - width: height - anchors.verticalCenter: parent.verticalCenter - sourceSize.width: width - sourceSize.height: height - } - } - - ToolboxShadow - { - anchors.top: bar.bottom - } -} diff --git a/plugins/Toolbox/resources/qml/components/ToolboxInstalledTile.qml b/plugins/Toolbox/resources/qml/components/ToolboxInstalledTile.qml deleted file mode 100644 index e5c94fc996..0000000000 --- a/plugins/Toolbox/resources/qml/components/ToolboxInstalledTile.qml +++ /dev/null @@ -1,123 +0,0 @@ -// Copyright (c) 2018 Ultimaker B.V. -// Toolbox is released under the terms of the LGPLv3 or higher. - -import QtQuick 2.10 -import QtQuick.Controls 1.4 -import QtQuick.Controls.Styles 1.4 -import UM 1.1 as UM - -Item -{ - height: UM.Theme.getSize("toolbox_installed_tile").height - width: parent.width - property bool isEnabled: true - - Rectangle - { - color: UM.Theme.getColor("lining") - width: parent.width - height: Math.floor(UM.Theme.getSize("default_lining").height) - anchors.bottom: parent.top - visible: index != 0 - } - Row - { - id: tileRow - height: parent.height - width: parent.width - spacing: UM.Theme.getSize("default_margin").width - topPadding: UM.Theme.getSize("default_margin").height - - CheckBox - { - id: disableButton - anchors.verticalCenter: pluginInfo.verticalCenter - checked: isEnabled - visible: model.type == "plugin" - width: visible ? UM.Theme.getSize("checkbox").width : 0 - enabled: !toolbox.isDownloading - style: UM.Theme.styles.checkbox - onClicked: toolbox.isEnabled(model.id) ? toolbox.disable(model.id) : toolbox.enable(model.id) - } - Column - { - id: pluginInfo - topPadding: UM.Theme.getSize("narrow_margin").height - property var color: model.type === "plugin" && !isEnabled ? UM.Theme.getColor("lining") : UM.Theme.getColor("text") - width: Math.floor(tileRow.width - (authorInfo.width + pluginActions.width + 2 * tileRow.spacing + ((disableButton.visible) ? disableButton.width + tileRow.spacing : 0))) - Label - { - text: model.name - width: parent.width - maximumLineCount: 1 - elide: Text.ElideRight - wrapMode: Text.WordWrap - font: UM.Theme.getFont("large_bold") - color: pluginInfo.color - renderType: Text.NativeRendering - } - Label - { - text: model.description - font: UM.Theme.getFont("default") - maximumLineCount: 3 - elide: Text.ElideRight - width: parent.width - wrapMode: Text.WordWrap - color: pluginInfo.color - renderType: Text.NativeRendering - } - } - Column - { - id: authorInfo - width: Math.floor(UM.Theme.getSize("toolbox_action_button").width * 1.25) - - Label - { - text: - { - if (model.author_email) - { - return "<a href=\"mailto:" + model.author_email + "?Subject=Cura: " + model.name + "\">" + model.author_name + "</a>" - } - else - { - return model.author_name - } - } - font: UM.Theme.getFont("medium") - width: parent.width - height: Math.floor(UM.Theme.getSize("toolbox_property_label").height) - wrapMode: Text.WordWrap - verticalAlignment: Text.AlignVCenter - horizontalAlignment: Text.AlignLeft - onLinkActivated: Qt.openUrlExternally("mailto:" + model.author_email + "?Subject=Cura: " + model.name + " Plugin") - color: model.enabled ? UM.Theme.getColor("text") : UM.Theme.getColor("lining") - linkColor: UM.Theme.getColor("text_link") - renderType: Text.NativeRendering - } - - Label - { - text: model.version - font: UM.Theme.getFont("default") - width: parent.width - height: UM.Theme.getSize("toolbox_property_label").height - color: UM.Theme.getColor("text") - verticalAlignment: Text.AlignVCenter - horizontalAlignment: Text.AlignLeft - renderType: Text.NativeRendering - } - } - ToolboxInstalledTileActions - { - id: pluginActions - } - Connections - { - target: toolbox - function onToolboxEnabledChanged() { isEnabled = toolbox.isEnabled(model.id) } - } - } -} diff --git a/plugins/Toolbox/resources/qml/components/ToolboxInstalledTileActions.qml b/plugins/Toolbox/resources/qml/components/ToolboxInstalledTileActions.qml deleted file mode 100644 index 1726497c00..0000000000 --- a/plugins/Toolbox/resources/qml/components/ToolboxInstalledTileActions.qml +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright (c) 2018 Ultimaker B.V. -// Toolbox is released under the terms of the LGPLv3 or higher. - -import QtQuick 2.10 -import QtQuick.Controls 1.4 -import QtQuick.Controls.Styles 1.4 -import UM 1.1 as UM - -import Cura 1.1 as Cura - -Column -{ - property bool canUpdate: CuraApplication.getPackageManager().packagesWithUpdate.indexOf(model.id) != -1 - property bool canDowngrade: false - property bool loginRequired: model.login_required && !Cura.API.account.isLoggedIn - width: UM.Theme.getSize("toolbox_action_button").width - spacing: UM.Theme.getSize("narrow_margin").height - - Label - { - visible: !model.is_installed - text: catalog.i18nc("@label", "Will install upon restarting") - color: UM.Theme.getColor("lining") - font: UM.Theme.getFont("default") - wrapMode: Text.WordWrap - width: parent.width - renderType: Text.NativeRendering - } - - ToolboxProgressButton - { - id: updateButton - active: toolbox.isDownloading && toolbox.activePackage == model - readyLabel: catalog.i18nc("@action:button", "Update") - activeLabel: catalog.i18nc("@action:button", "Updating") - completeLabel: catalog.i18nc("@action:button", "Updated") - onReadyAction: - { - toolbox.activePackage = model - toolbox.update(model.id) - } - onActiveAction: toolbox.cancelDownload() - - // Don't allow installing while another download is running - enabled: !(toolbox.isDownloading && toolbox.activePackage != model) && !loginRequired - opacity: enabled ? 1.0 : 0.5 - visible: canUpdate - } - - Label - { - wrapMode: Text.WordWrap - text: catalog.i18nc("@label:The string between <a href=> and </a> is the highlighted link", "<a href='%1'>Log in</a> is required to update") - font: UM.Theme.getFont("default") - color: UM.Theme.getColor("text") - linkColor: UM.Theme.getColor("text_link") - visible: loginRequired - width: updateButton.width - renderType: Text.NativeRendering - - MouseArea - { - anchors.fill: parent - onClicked: Cura.API.account.login() - } - } - - Cura.SecondaryButton - { - id: removeButton - text: canDowngrade ? catalog.i18nc("@action:button", "Downgrade") : catalog.i18nc("@action:button", "Uninstall") - visible: !model.is_bundled && model.is_installed - enabled: !toolbox.isDownloading - - width: UM.Theme.getSize("toolbox_action_button").width - height: UM.Theme.getSize("toolbox_action_button").height - - fixedWidthMode: true - - onClicked: toolbox.checkPackageUsageAndUninstall(model.id) - Connections - { - target: toolbox - function onMetadataChanged() - { - canDowngrade = toolbox.canDowngrade(model.id) - } - } - } -} diff --git a/plugins/Toolbox/resources/qml/components/ToolboxProgressButton.qml b/plugins/Toolbox/resources/qml/components/ToolboxProgressButton.qml deleted file mode 100644 index 40d6c1af47..0000000000 --- a/plugins/Toolbox/resources/qml/components/ToolboxProgressButton.qml +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright (c) 2019 Ultimaker B.V. -// Toolbox is released under the terms of the LGPLv3 or higher. - -import QtQuick 2.10 -import QtQuick.Controls 2.3 - -import UM 1.1 as UM -import Cura 1.0 as Cura - - -Cura.PrimaryButton -{ - id: button - - property var active: false - property var complete: false - - property var readyLabel: catalog.i18nc("@action:button", "Install") - property var activeLabel: catalog.i18nc("@action:button", "Cancel") - property var completeLabel: catalog.i18nc("@action:button", "Installed") - - signal readyAction() // Action when button is ready and clicked (likely install) - signal activeAction() // Action when button is active and clicked (likely cancel) - signal completeAction() // Action when button is complete and clicked (likely go to installed) - - width: UM.Theme.getSize("toolbox_action_button").width - height: UM.Theme.getSize("toolbox_action_button").height - fixedWidthMode: true - text: - { - if (complete) - { - return completeLabel - } - else if (active) - { - return activeLabel - } - else - { - return readyLabel - } - } - onClicked: - { - if (complete) - { - completeAction() - } - else if (active) - { - activeAction() - } - else - { - readyAction() - } - } - busy: active -} diff --git a/plugins/Toolbox/resources/qml/components/ToolboxShadow.qml b/plugins/Toolbox/resources/qml/components/ToolboxShadow.qml deleted file mode 100644 index 0f2f98beb9..0000000000 --- a/plugins/Toolbox/resources/qml/components/ToolboxShadow.qml +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) 2018 Ultimaker B.V. -// Toolbox is released under the terms of the LGPLv3 or higher. - -import QtQuick 2.2 - -Rectangle -{ - property bool reversed: false - width: parent.width - height: 8 - gradient: Gradient - { - GradientStop - { - position: reversed ? 1.0 : 0.0 - color: reversed ? Qt.rgba(0,0,0,0.05) : Qt.rgba(0,0,0,0.2) - } - GradientStop - { - position: reversed ? 0.0 : 1.0 - color: Qt.rgba(0,0,0,0) - } - } -} diff --git a/plugins/Toolbox/resources/qml/components/ToolboxTabButton.qml b/plugins/Toolbox/resources/qml/components/ToolboxTabButton.qml deleted file mode 100644 index 7a7d2be48a..0000000000 --- a/plugins/Toolbox/resources/qml/components/ToolboxTabButton.qml +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright (c) 2018 Ultimaker B.V. -// Toolbox is released under the terms of the LGPLv3 or higher. - -import QtQuick 2.10 -import QtQuick.Controls 2.3 -import UM 1.1 as UM - -Button -{ - id: control - property bool active: false - - implicitWidth: UM.Theme.getSize("toolbox_header_tab").width - implicitHeight: UM.Theme.getSize("toolbox_header_tab").height - - background: Item - { - id: backgroundItem - Rectangle - { - id: highlight - - visible: control.active - color: UM.Theme.getColor("primary") - anchors.bottom: parent.bottom - width: parent.width - height: UM.Theme.getSize("toolbox_header_highlight").height - } - } - - contentItem: Label - { - id: label - text: control.text - color: UM.Theme.getColor("toolbox_header_button_text_inactive") - font: UM.Theme.getFont("medium") - - verticalAlignment: Text.AlignVCenter - horizontalAlignment: Text.AlignHCenter - - renderType: Text.NativeRendering - } - - states: - [ - State - { - name: "disabled" - when: !control.enabled - PropertyChanges - { - target: label - font: UM.Theme.getFont("default_italic") - } - }, - State - { - name: "active" - when: control.active - PropertyChanges - { - target: label - font: UM.Theme.getFont("medium_bold") - color: UM.Theme.getColor("action_button_text") - } - } - ] -}
\ No newline at end of file diff --git a/plugins/Toolbox/resources/qml/dialogs/CompatibilityDialog.qml b/plugins/Toolbox/resources/qml/dialogs/CompatibilityDialog.qml deleted file mode 100644 index b33036847b..0000000000 --- a/plugins/Toolbox/resources/qml/dialogs/CompatibilityDialog.qml +++ /dev/null @@ -1,153 +0,0 @@ -// Copyright (c) 2020 Ultimaker B.V. -// Toolbox is released under the terms of the LGPLv3 or higher. - -import QtQuick 2.10 -import QtQuick.Window 2.2 -import QtQuick.Controls 2.3 - -import UM 1.1 as UM -import Cura 1.6 as Cura - - -UM.Dialog{ - visible: true - title: catalog.i18nc("@title", "Changes from your account") - width: UM.Theme.getSize("popup_dialog").width - height: UM.Theme.getSize("popup_dialog").height - minimumWidth: width - maximumWidth: minimumWidth - minimumHeight: height - maximumHeight: minimumHeight - margin: 0 - - property string actionButtonText: subscribedPackagesModel.hasIncompatiblePackages && !subscribedPackagesModel.hasCompatiblePackages ? catalog.i18nc("@button", "Dismiss") : catalog.i18nc("@button", "Next") - - Rectangle - { - id: root - anchors.fill: parent - color: UM.Theme.getColor("main_background") - - UM.I18nCatalog - { - id: catalog - name: "cura" - } - - ScrollView - { - width: parent.width - height: parent.height - nextButton.height - nextButton.anchors.margins * 2 // We want some leftover space for the button at the bottom - clip: true - - Column - { - anchors.fill: parent - anchors.margins: UM.Theme.getSize("default_margin").width - - // Compatible packages - Label - { - font: UM.Theme.getFont("default") - text: catalog.i18nc("@label", "The following packages will be added:") - visible: subscribedPackagesModel.hasCompatiblePackages - color: UM.Theme.getColor("text") - height: contentHeight + UM.Theme.getSize("default_margin").height - } - Repeater - { - model: subscribedPackagesModel - Component - { - Item - { - width: parent.width - property int lineHeight: 60 - visible: model.is_compatible - height: visible ? (lineHeight + UM.Theme.getSize("default_margin").height) : 0 // We only show the compatible packages here - Image - { - id: packageIcon - source: model.icon_url || "../../images/placeholder.svg" - height: lineHeight - width: height - sourceSize.height: height - sourceSize.width: width - mipmap: true - fillMode: Image.PreserveAspectFit - } - Label - { - text: model.display_name - font: UM.Theme.getFont("medium_bold") - anchors.left: packageIcon.right - anchors.leftMargin: UM.Theme.getSize("default_margin").width - anchors.verticalCenter: packageIcon.verticalCenter - color: UM.Theme.getColor("text") - elide: Text.ElideRight - } - } - } - } - - // Incompatible packages - Label - { - font: UM.Theme.getFont("default") - text: catalog.i18nc("@label", "The following packages can not be installed because of an incompatible Cura version:") - visible: subscribedPackagesModel.hasIncompatiblePackages - color: UM.Theme.getColor("text") - height: contentHeight + UM.Theme.getSize("default_margin").height - } - Repeater - { - model: subscribedPackagesModel - Component - { - Item - { - width: parent.width - property int lineHeight: 60 - visible: !model.is_compatible && !model.is_dismissed - height: visible ? (lineHeight + UM.Theme.getSize("default_margin").height) : 0 // We only show the incompatible packages here - Image - { - id: packageIcon - source: model.icon_url || "../../images/placeholder.svg" - height: lineHeight - width: height - sourceSize.height: height - sourceSize.width: width - mipmap: true - fillMode: Image.PreserveAspectFit - } - Label - { - text: model.display_name - font: UM.Theme.getFont("medium_bold") - anchors.left: packageIcon.right - anchors.leftMargin: UM.Theme.getSize("default_margin").width - anchors.verticalCenter: packageIcon.verticalCenter - color: UM.Theme.getColor("text") - elide: Text.ElideRight - } - } - } - } - } - - } // End of ScrollView - - Cura.PrimaryButton - { - id: nextButton - anchors.bottom: parent.bottom - anchors.right: parent.right - anchors.margins: UM.Theme.getSize("default_margin").height - text: actionButtonText - onClicked: accept() - leftPadding: UM.Theme.getSize("dialog_primary_button_padding").width - rightPadding: UM.Theme.getSize("dialog_primary_button_padding").width - } - } -} diff --git a/plugins/Toolbox/resources/qml/dialogs/ToolboxConfirmUninstallResetDialog.qml b/plugins/Toolbox/resources/qml/dialogs/ToolboxConfirmUninstallResetDialog.qml deleted file mode 100644 index 1b5e4d1d46..0000000000 --- a/plugins/Toolbox/resources/qml/dialogs/ToolboxConfirmUninstallResetDialog.qml +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright (c) 2018 Ultimaker B.V. -// Cura is released under the terms of the LGPLv3 or higher. - -import QtQuick 2.10 -import QtQuick.Controls 1.1 -import QtQuick.Controls.Styles 1.1 -import QtQuick.Layouts 1.1 -import QtQuick.Dialogs 1.1 -import QtQuick.Window 2.1 - -import UM 1.3 as UM -import Cura 1.0 as Cura - - -UM.Dialog -{ - // This dialog asks the user to confirm he/she wants to uninstall materials/pprofiles which are currently in use - id: base - - title: catalog.i18nc("@title:window", "Confirm uninstall") + toolbox.pluginToUninstall - width: 450 * screenScaleFactor - height: 50 * screenScaleFactor + dialogText.height + buttonBar.height - - maximumWidth: 450 * screenScaleFactor - maximumHeight: 450 * screenScaleFactor - minimumWidth: 450 * screenScaleFactor - minimumHeight: 150 * screenScaleFactor - - modality: Qt.WindowModal - - Column - { - UM.I18nCatalog { id: catalog; name: "cura" } - - anchors - { - fill: parent - leftMargin: Math.round(20 * screenScaleFactor) - rightMargin: Math.round(20 * screenScaleFactor) - topMargin: Math.round(10 * screenScaleFactor) - bottomMargin: Math.round(10 * screenScaleFactor) - } - spacing: Math.round(15 * screenScaleFactor) - - Label - { - id: dialogText - text: - { - var base_text = catalog.i18nc("@text:window", "You are uninstalling materials and/or profiles that are still in use. Confirming will reset the following materials/profiles to their defaults.") - var materials_text = catalog.i18nc("@text:window", "Materials") - var qualities_text = catalog.i18nc("@text:window", "Profiles") - var machines_with_materials = toolbox.uninstallUsedMaterials - var machines_with_qualities = toolbox.uninstallUsedQualities - if (machines_with_materials != "") - { - base_text += "\n\n" + materials_text +": \n" + machines_with_materials - } - if (machines_with_qualities != "") - { - base_text += "\n\n" + qualities_text + ": \n" + machines_with_qualities - } - return base_text - } - anchors.left: parent.left - anchors.right: parent.right - font: UM.Theme.getFont("default") - wrapMode: Text.WordWrap - renderType: Text.NativeRendering - } - - // Buttons - Item { - id: buttonBar - anchors.right: parent.right - anchors.left: parent.left - height: childrenRect.height - - Button { - id: cancelButton - text: catalog.i18nc("@action:button", "Cancel") - anchors.right: confirmButton.left - anchors.rightMargin: UM.Theme.getSize("default_margin").width - isDefault: true - onClicked: toolbox.closeConfirmResetDialog() - } - - Button { - id: confirmButton - text: catalog.i18nc("@action:button", "Confirm") - anchors.right: parent.right - onClicked: toolbox.resetMaterialsQualitiesAndUninstall() - } - } - } -} diff --git a/plugins/Toolbox/resources/qml/dialogs/ToolboxLicenseDialog.qml b/plugins/Toolbox/resources/qml/dialogs/ToolboxLicenseDialog.qml deleted file mode 100644 index 9219f4ed32..0000000000 --- a/plugins/Toolbox/resources/qml/dialogs/ToolboxLicenseDialog.qml +++ /dev/null @@ -1,110 +0,0 @@ -// Copyright (c) 2018 Ultimaker B.V. -// Toolbox is released under the terms of the LGPLv3 or higher. - -import QtQuick 2.10 -import QtQuick.Dialogs 1.1 -import QtQuick.Window 2.2 -import QtQuick.Controls 2.3 -import QtQuick.Layouts 1.3 -import QtQuick.Controls.Styles 1.4 - -import UM 1.1 as UM -import Cura 1.6 as Cura - -UM.Dialog -{ - id: licenseDialog - title: licenseModel.dialogTitle - minimumWidth: UM.Theme.getSize("license_window_minimum").width - minimumHeight: UM.Theme.getSize("license_window_minimum").height - width: minimumWidth - height: minimumHeight - backgroundColor: UM.Theme.getColor("main_background") - margin: screenScaleFactor * 10 - - ColumnLayout - { - anchors.fill: parent - spacing: UM.Theme.getSize("thick_margin").height - - UM.I18nCatalog{id: catalog; name: "cura"} - - Label - { - id: licenseHeader - Layout.fillWidth: true - text: catalog.i18nc("@label", "You need to accept the license to install the package") - color: UM.Theme.getColor("text") - wrapMode: Text.Wrap - renderType: Text.NativeRendering - } - - Row { - id: packageRow - - Layout.fillWidth: true - height: childrenRect.height - spacing: UM.Theme.getSize("default_margin").width - leftPadding: UM.Theme.getSize("narrow_margin").width - - Image - { - id: icon - width: 30 * screenScaleFactor - height: width - sourceSize.width: width - sourceSize.height: height - fillMode: Image.PreserveAspectFit - source: licenseModel.iconUrl || "../../images/placeholder.svg" - mipmap: true - } - - Label - { - id: packageName - text: licenseModel.packageName - color: UM.Theme.getColor("text") - font.bold: true - anchors.verticalCenter: icon.verticalCenter - height: contentHeight - wrapMode: Text.Wrap - renderType: Text.NativeRendering - } - - - } - - Cura.ScrollableTextArea - { - - Layout.fillWidth: true - Layout.fillHeight: true - anchors.topMargin: UM.Theme.getSize("default_margin").height - - textArea.text: licenseModel.licenseText - textArea.readOnly: true - } - - } - rightButtons: - [ - Cura.PrimaryButton - { - leftPadding: UM.Theme.getSize("dialog_primary_button_padding").width - rightPadding: UM.Theme.getSize("dialog_primary_button_padding").width - - text: licenseModel.acceptButtonText - onClicked: { handler.onLicenseAccepted() } - } - ] - - leftButtons: - [ - Cura.SecondaryButton - { - id: declineButton - text: licenseModel.declineButtonText - onClicked: { handler.onLicenseDeclined() } - } - ] -} diff --git a/plugins/Toolbox/resources/qml/pages/ToolboxAuthorPage.qml b/plugins/Toolbox/resources/qml/pages/ToolboxAuthorPage.qml deleted file mode 100644 index 2fa4224388..0000000000 --- a/plugins/Toolbox/resources/qml/pages/ToolboxAuthorPage.qml +++ /dev/null @@ -1,176 +0,0 @@ -// Copyright (c) 2018 Ultimaker B.V. -// Toolbox is released under the terms of the LGPLv3 or higher. - -import QtQuick 2.10 -import QtQuick.Controls 1.4 -import QtQuick.Controls.Styles 1.4 -import UM 1.5 as UM - -import "../components" - -Item -{ - id: page - property var details: base.selection || {} - anchors.fill: parent - ToolboxBackColumn - { - id: sidebar - } - Item - { - id: header - anchors - { - left: sidebar.right - right: parent.right - rightMargin: UM.Theme.getSize("wide_margin").width - } - height: UM.Theme.getSize("toolbox_detail_header").height - Image - { - id: thumbnail - width: UM.Theme.getSize("toolbox_thumbnail_medium").width - height: UM.Theme.getSize("toolbox_thumbnail_medium").height - fillMode: Image.PreserveAspectFit - source: details && details.icon_url ? details.icon_url : "../../images/placeholder.svg" - mipmap: true - anchors - { - top: parent.top - left: parent.left - leftMargin: UM.Theme.getSize("wide_margin").width - topMargin: UM.Theme.getSize("wide_margin").height - } - } - - Label - { - id: title - anchors - { - top: thumbnail.top - left: thumbnail.right - leftMargin: UM.Theme.getSize("default_margin").width - right: parent.right - rightMargin: UM.Theme.getSize("wide_margin").width - bottomMargin: UM.Theme.getSize("default_margin").height - } - text: details && details.name ? details.name : "" - font: UM.Theme.getFont("large_bold") - color: UM.Theme.getColor("text_medium") - wrapMode: Text.WordWrap - width: parent.width - height: UM.Theme.getSize("toolbox_property_label").height - renderType: Text.NativeRendering - } - Label - { - id: description - text: details && details.description ? details.description : "" - font: UM.Theme.getFont("default") - color: UM.Theme.getColor("text_medium") - anchors - { - top: title.bottom - left: title.left - topMargin: UM.Theme.getSize("default_margin").height - } - renderType: Text.NativeRendering - } - Column - { - id: properties - anchors - { - top: description.bottom - left: description.left - topMargin: UM.Theme.getSize("default_margin").height - } - spacing: Math.floor(UM.Theme.getSize("narrow_margin").height) - width: childrenRect.width - - Label - { - text: catalog.i18nc("@label", "Website") + ":" - font: UM.Theme.getFont("default") - color: UM.Theme.getColor("text_medium") - renderType: Text.NativeRendering - } - Label - { - text: catalog.i18nc("@label", "Email") + ":" - font: UM.Theme.getFont("default") - color: UM.Theme.getColor("text_medium") - renderType: Text.NativeRendering - } - } - Column - { - id: values - anchors - { - top: description.bottom - left: properties.right - leftMargin: UM.Theme.getSize("default_margin").width - right: parent.right - rightMargin: UM.Theme.getSize("default_margin").width - topMargin: UM.Theme.getSize("default_margin").height - } - spacing: Math.floor(UM.Theme.getSize("narrow_margin").height) - - Label - { - text: - { - if (details && details.website) - { - return "<a href=\"" + details.website + "\">" + details.website + "</a>" - } - return "" - } - width: parent.width - elide: Text.ElideRight - font: UM.Theme.getFont("default") - color: UM.Theme.getColor("text") - linkColor: UM.Theme.getColor("text_link") - onLinkActivated: UM.UrlUtil.openUrl(link, ["https", "http"]) - renderType: Text.NativeRendering - } - - Label - { - text: - { - if (details && details.email) - { - return "<a href=\"mailto:" + details.email + "\">" + details.email + "</a>" - } - return "" - } - font: UM.Theme.getFont("default") - color: UM.Theme.getColor("text") - linkColor: UM.Theme.getColor("text_link") - onLinkActivated: Qt.openUrlExternally(link) - renderType: Text.NativeRendering - } - } - Rectangle - { - color: UM.Theme.getColor("lining") - width: parent.width - height: UM.Theme.getSize("default_lining").height - anchors.bottom: parent.bottom - } - } - ToolboxDetailList - { - anchors - { - top: header.bottom - bottom: page.bottom - left: header.left - right: page.right - } - } -} diff --git a/plugins/Toolbox/resources/qml/pages/ToolboxDetailPage.qml b/plugins/Toolbox/resources/qml/pages/ToolboxDetailPage.qml deleted file mode 100644 index 645b77a8c9..0000000000 --- a/plugins/Toolbox/resources/qml/pages/ToolboxDetailPage.qml +++ /dev/null @@ -1,188 +0,0 @@ -// Copyright (c) 2018 Ultimaker B.V. -// Toolbox is released under the terms of the LGPLv3 or higher. - -import QtQuick 2.10 -import QtQuick.Controls 1.4 -import QtQuick.Controls.Styles 1.4 -import UM 1.5 as UM - -import Cura 1.1 as Cura - -import "../components" - -Item -{ - id: page - property var details: base.selection || {} - anchors.fill: parent - ToolboxBackColumn - { - id: sidebar - } - Item - { - id: header - anchors - { - left: sidebar.right - right: parent.right - rightMargin: UM.Theme.getSize("wide_margin").width - } - height: childrenRect.height + 3 * UM.Theme.getSize("default_margin").width - Rectangle - { - id: thumbnail - width: UM.Theme.getSize("toolbox_thumbnail_medium").width - height: UM.Theme.getSize("toolbox_thumbnail_medium").height - anchors - { - top: parent.top - left: parent.left - leftMargin: UM.Theme.getSize("wide_margin").width - topMargin: UM.Theme.getSize("wide_margin").height - } - color: UM.Theme.getColor("main_background") - Image - { - anchors.fill: parent - fillMode: Image.PreserveAspectFit - source: details === null ? "" : (details.icon_url || "../../images/placeholder.svg") - mipmap: true - height: UM.Theme.getSize("toolbox_thumbnail_large").height - 4 * UM.Theme.getSize("default_margin").height - width: UM.Theme.getSize("toolbox_thumbnail_large").height - 4 * UM.Theme.getSize("default_margin").height - sourceSize.height: height - sourceSize.width: width - } - } - - Label - { - id: title - anchors - { - top: thumbnail.top - left: thumbnail.right - leftMargin: UM.Theme.getSize("default_margin").width - } - text: details === null ? "" : (details.name || "") - font: UM.Theme.getFont("large_bold") - color: UM.Theme.getColor("text") - width: contentWidth - height: contentHeight - renderType: Text.NativeRendering - } - - Column - { - id: properties - anchors - { - top: title.bottom - left: title.left - topMargin: UM.Theme.getSize("default_margin").height - } - spacing: Math.floor(UM.Theme.getSize("narrow_margin").height) - width: childrenRect.width - height: childrenRect.height - Label - { - text: catalog.i18nc("@label", "Version") + ":" - font: UM.Theme.getFont("default") - color: UM.Theme.getColor("text_medium") - renderType: Text.NativeRendering - } - Label - { - text: catalog.i18nc("@label", "Last updated") + ":" - font: UM.Theme.getFont("default") - color: UM.Theme.getColor("text_medium") - renderType: Text.NativeRendering - } - Label - { - text: catalog.i18nc("@label", "Brand") + ":" - font: UM.Theme.getFont("default") - color: UM.Theme.getColor("text_medium") - renderType: Text.NativeRendering - } - Label - { - text: catalog.i18nc("@label", "Downloads") + ":" - font: UM.Theme.getFont("default") - color: UM.Theme.getColor("text_medium") - renderType: Text.NativeRendering - } - } - Column - { - id: values - anchors - { - top: title.bottom - left: properties.right - leftMargin: UM.Theme.getSize("default_margin").width - topMargin: UM.Theme.getSize("default_margin").height - } - spacing: Math.floor(UM.Theme.getSize("narrow_margin").height) - height: childrenRect.height - Label - { - text: details === null ? "" : (details.version || catalog.i18nc("@label", "Unknown")) - font: UM.Theme.getFont("default") - color: UM.Theme.getColor("text") - renderType: Text.NativeRendering - } - Label - { - text: - { - if (details === null) - { - return "" - } - var date = new Date(details.last_updated) - return date.toLocaleString(UM.Preferences.getValue("general/language")) - } - font: UM.Theme.getFont("default") - color: UM.Theme.getColor("text") - renderType: Text.NativeRendering - } - Label - { - text: - { - if (details === null) - { - return "" - } - else - { - return "<a href=\"" + details.website + "\">" + details.author_name + "</a>" - } - } - font: UM.Theme.getFont("default") - color: UM.Theme.getColor("text") - linkColor: UM.Theme.getColor("text_link") - onLinkActivated: UM.UrlUtil.openUrl(link, ["http", "https"]) - renderType: Text.NativeRendering - } - Label - { - text: details === null ? "" : (details.download_count || catalog.i18nc("@label", "Unknown")) - font: UM.Theme.getFont("default") - color: UM.Theme.getColor("text") - renderType: Text.NativeRendering - } - } - } - ToolboxDetailList - { - anchors - { - top: header.bottom - bottom: page.bottom - left: header.left - right: page.right - } - } -} diff --git a/plugins/Toolbox/resources/qml/pages/ToolboxDownloadsPage.qml b/plugins/Toolbox/resources/qml/pages/ToolboxDownloadsPage.qml deleted file mode 100644 index 9be8cbe2b9..0000000000 --- a/plugins/Toolbox/resources/qml/pages/ToolboxDownloadsPage.qml +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (c) 2019 Ultimaker B.V. -// Toolbox is released under the terms of the LGPLv3 or higher. - -import QtQuick 2.10 -import QtQuick.Controls 2.3 -import UM 1.1 as UM - -import "../components" - -ScrollView -{ - clip: true - width: parent.width - height: parent.height - contentHeight: mainColumn.height - - Column - { - id: mainColumn - width: base.width - spacing: UM.Theme.getSize("default_margin").height - - ToolboxDownloadsShowcase - { - id: showcase - width: parent.width - } - - ToolboxDownloadsGrid - { - id: allPlugins - width: parent.width - heading: toolbox.viewCategory === "material" ? catalog.i18nc("@label", "Community Contributions") : catalog.i18nc("@label", "Community Plugins") - model: toolbox.viewCategory === "material" ? toolbox.materialsAvailableModel : toolbox.pluginsAvailableModel - } - - ToolboxDownloadsGrid - { - id: genericMaterials - visible: toolbox.viewCategory === "material" - width: parent.width - heading: catalog.i18nc("@label", "Generic Materials") - model: toolbox.materialsGenericModel - } - } -} diff --git a/plugins/Toolbox/resources/qml/pages/ToolboxErrorPage.qml b/plugins/Toolbox/resources/qml/pages/ToolboxErrorPage.qml deleted file mode 100644 index e57e63dbb9..0000000000 --- a/plugins/Toolbox/resources/qml/pages/ToolboxErrorPage.qml +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) 2018 Ultimaker B.V. -// Toolbox is released under the terms of the LGPLv3 or higher. - -import QtQuick 2.10 -import QtQuick.Controls 1.4 -import QtQuick.Controls.Styles 1.4 - -Rectangle -{ - id: page - width: parent.width - height: parent.height - color: "transparent" - Label - { - text: catalog.i18nc("@info", "Could not connect to the Cura Package database. Please check your connection.") - anchors - { - centerIn: parent - } - renderType: Text.NativeRendering - } -} diff --git a/plugins/Toolbox/resources/qml/pages/ToolboxInstalledPage.qml b/plugins/Toolbox/resources/qml/pages/ToolboxInstalledPage.qml deleted file mode 100644 index fa7bd24c9d..0000000000 --- a/plugins/Toolbox/resources/qml/pages/ToolboxInstalledPage.qml +++ /dev/null @@ -1,223 +0,0 @@ -// Copyright (c) 2019 Ultimaker B.V. -// Toolbox is released under the terms of the LGPLv3 or higher. - -import QtQuick 2.10 -import QtQuick.Controls 2.3 - -import UM 1.1 as UM - -import "../components" - -ScrollView -{ - id: page - clip: true - width: parent.width - height: parent.height - - Column - { - width: page.width - spacing: UM.Theme.getSize("default_margin").height - padding: UM.Theme.getSize("wide_margin").width - height: childrenRect.height + 2 * UM.Theme.getSize("wide_margin").height - - Label - { - anchors - { - left: parent.left - right: parent.right - margins: parent.padding - } - text: catalog.i18nc("@title:tab", "Installed plugins") - color: UM.Theme.getColor("text_medium") - font: UM.Theme.getFont("medium") - renderType: Text.NativeRendering - } - - Rectangle - { - anchors - { - left: parent.left - right: parent.right - margins: parent.padding - } - id: installedPlugins - color: "transparent" - height: childrenRect.height + UM.Theme.getSize("default_margin").width - border.color: UM.Theme.getColor("lining") - border.width: UM.Theme.getSize("default_lining").width - Column - { - anchors - { - top: parent.top - right: parent.right - left: parent.left - margins: UM.Theme.getSize("default_margin").width - } - Repeater - { - id: pluginList - model: toolbox.pluginsInstalledModel - delegate: ToolboxInstalledTile { } - } - } - Label - { - visible: toolbox.pluginsInstalledModel.count < 1 - padding: UM.Theme.getSize("default_margin").width - text: catalog.i18nc("@info", "No plugin has been installed.") - font: UM.Theme.getFont("medium") - color: UM.Theme.getColor("lining") - renderType: Text.NativeRendering - } - } - - Label - { - anchors - { - left: parent.left - right: parent.right - margins: parent.padding - } - text: catalog.i18nc("@title:tab", "Installed materials") - color: UM.Theme.getColor("text_medium") - font: UM.Theme.getFont("medium") - renderType: Text.NativeRendering - } - - Rectangle - { - anchors - { - left: parent.left - right: parent.right - margins: parent.padding - } - id: installedMaterials - color: "transparent" - height: childrenRect.height + UM.Theme.getSize("default_margin").width - border.color: UM.Theme.getColor("lining") - border.width: UM.Theme.getSize("default_lining").width - Column - { - anchors - { - top: parent.top - right: parent.right - left: parent.left - margins: UM.Theme.getSize("default_margin").width - } - Repeater - { - id: installedMaterialsList - model: toolbox.materialsInstalledModel - delegate: ToolboxInstalledTile { } - } - } - Label - { - visible: toolbox.materialsInstalledModel.count < 1 - padding: UM.Theme.getSize("default_margin").width - text: catalog.i18nc("@info", "No material has been installed.") - color: UM.Theme.getColor("lining") - font: UM.Theme.getFont("medium") - renderType: Text.NativeRendering - } - } - - Label - { - anchors - { - left: parent.left - right: parent.right - margins: parent.padding - } - text: catalog.i18nc("@title:tab", "Bundled plugins") - color: UM.Theme.getColor("text_medium") - font: UM.Theme.getFont("medium") - renderType: Text.NativeRendering - } - - Rectangle - { - anchors - { - left: parent.left - right: parent.right - margins: parent.padding - } - id: bundledPlugins - color: "transparent" - height: childrenRect.height + UM.Theme.getSize("default_margin").width - border.color: UM.Theme.getColor("lining") - border.width: UM.Theme.getSize("default_lining").width - Column - { - anchors - { - top: parent.top - right: parent.right - left: parent.left - margins: UM.Theme.getSize("default_margin").width - } - Repeater - { - id: bundledPluginsList - model: toolbox.pluginsBundledModel - delegate: ToolboxInstalledTile { } - } - } - } - - Label - { - anchors - { - left: parent.left - right: parent.right - margins: parent.padding - } - text: catalog.i18nc("@title:tab", "Bundled materials") - color: UM.Theme.getColor("text_medium") - font: UM.Theme.getFont("medium") - renderType: Text.NativeRendering - } - - Rectangle - { - anchors - { - left: parent.left - right: parent.right - margins: parent.padding - } - id: bundledMaterials - color: "transparent" - height: childrenRect.height + UM.Theme.getSize("default_margin").width - border.color: UM.Theme.getColor("lining") - border.width: UM.Theme.getSize("default_lining").width - Column - { - anchors - { - top: parent.top - right: parent.right - left: parent.left - margins: UM.Theme.getSize("default_margin").width - } - Repeater - { - id: bundledMaterialsList - model: toolbox.materialsBundledModel - delegate: ToolboxInstalledTile {} - } - } - } - } -} diff --git a/plugins/Toolbox/resources/qml/pages/ToolboxLoadingPage.qml b/plugins/Toolbox/resources/qml/pages/ToolboxLoadingPage.qml deleted file mode 100644 index a30af6b335..0000000000 --- a/plugins/Toolbox/resources/qml/pages/ToolboxLoadingPage.qml +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) 2018 Ultimaker B.V. -// Toolbox is released under the terms of the LGPLv3 or higher. - -import QtQuick 2.10 -import QtQuick.Controls 1.4 -import QtQuick.Controls.Styles 1.4 -import UM 1.3 as UM - -Rectangle -{ - id: page - width: parent.width - height: parent.height - color: "transparent" - Label - { - text: catalog.i18nc("@info", "Fetching packages...") - color: UM.Theme.getColor("text") - anchors - { - centerIn: parent - } - renderType: Text.NativeRendering - } -} diff --git a/plugins/Toolbox/resources/qml/pages/WelcomePage.qml b/plugins/Toolbox/resources/qml/pages/WelcomePage.qml deleted file mode 100644 index 04110cbc0f..0000000000 --- a/plugins/Toolbox/resources/qml/pages/WelcomePage.qml +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright (c) 2018 Ultimaker B.V. -// Cura is released under the terms of the LGPLv3 or higher. - -import QtQuick 2.7 -import QtQuick.Controls 2.1 -import QtQuick.Window 2.2 - -import UM 1.3 as UM -import Cura 1.1 as Cura - -Column -{ - id: welcomePage - spacing: UM.Theme.getSize("wide_margin").height - width: parent.width - height: childrenRect.height - anchors.centerIn: parent - - Label - { - id: welcomeTextLabel - text: catalog.i18nc("@description", "Please sign in to get verified plugins and materials for Ultimaker Cura Enterprise") - width: Math.round(parent.width / 2) - font: UM.Theme.getFont("default") - color: UM.Theme.getColor("text") - verticalAlignment: Text.AlignVCenter - horizontalAlignment: Text.AlignHCenter - anchors.horizontalCenter: parent.horizontalCenter - wrapMode: Label.WordWrap - renderType: Text.NativeRendering - } - - Cura.PrimaryButton - { - id: loginButton - width: UM.Theme.getSize("account_button").width - height: UM.Theme.getSize("account_button").height - anchors.horizontalCenter: parent.horizontalCenter - text: catalog.i18nc("@button", "Sign in") - onClicked: Cura.API.account.login() - fixedWidthMode: true - } -} - diff --git a/plugins/Toolbox/src/AuthorsModel.py b/plugins/Toolbox/src/AuthorsModel.py deleted file mode 100644 index 04c8ed3a40..0000000000 --- a/plugins/Toolbox/src/AuthorsModel.py +++ /dev/null @@ -1,104 +0,0 @@ -# Copyright (c) 2018 Ultimaker B.V. -# Cura is released under the terms of the LGPLv3 or higher. - -import re -from typing import Dict, List, Optional, Union, cast - -from PyQt5.QtCore import Qt, pyqtProperty - -from UM.Qt.ListModel import ListModel - - -class AuthorsModel(ListModel): - """Model that holds cura packages. - - By setting the filter property the instances held by this model can be changed. - """ - - def __init__(self, parent = None) -> None: - super().__init__(parent) - - self._metadata = None # type: Optional[List[Dict[str, Union[str, List[str], int]]]] - - self.addRoleName(Qt.UserRole + 1, "id") - self.addRoleName(Qt.UserRole + 2, "name") - self.addRoleName(Qt.UserRole + 3, "email") - self.addRoleName(Qt.UserRole + 4, "website") - self.addRoleName(Qt.UserRole + 5, "package_count") - self.addRoleName(Qt.UserRole + 6, "package_types") - self.addRoleName(Qt.UserRole + 7, "icon_url") - self.addRoleName(Qt.UserRole + 8, "description") - - # List of filters for queries. The result is the union of the each list of results. - self._filter = {} # type: Dict[str, str] - - def setMetadata(self, data: List[Dict[str, Union[str, List[str], int]]]): - if self._metadata != data: - self._metadata = data - self._update() - - def _update(self) -> None: - items = [] # type: List[Dict[str, Union[str, List[str], int, None]]] - if not self._metadata: - self.setItems(items) - return - - for author in self._metadata: - items.append({ - "id": author.get("author_id"), - "name": author.get("display_name"), - "email": author.get("email"), - "website": author.get("website"), - "package_count": author.get("package_count", 0), - "package_types": author.get("package_types", []), - "icon_url": author.get("icon_url"), - "description": "Material and quality profiles from {author_name}".format(author_name = author.get("display_name", "")) - }) - - # Filter on all the key-word arguments. - for key, value in self._filter.items(): - if key == "package_types": - key_filter = lambda item, value = value: value in item["package_types"] # type: ignore - elif "*" in value: - key_filter = lambda item, key = key, value = value: self._matchRegExp(item, key, value) # type: ignore - else: - key_filter = lambda item, key = key, value = value: self._matchString(item, key, value) # type: ignore - items = filter(key_filter, items) # type: ignore - - # Execute all filters. - filtered_items = list(items) - - filtered_items.sort(key = lambda k: cast(str, k["name"])) - self.setItems(filtered_items) - - def setFilter(self, filter_dict: Dict[str, str]) -> None: - """Set the filter of this model based on a string. - - :param filter_dict: Dictionary to do the filtering by. - """ - if filter_dict != self._filter: - self._filter = filter_dict - self._update() - - @pyqtProperty("QVariantMap", fset = setFilter, constant = True) - def filter(self) -> Dict[str, str]: - return self._filter - - # Check to see if a container matches with a regular expression - def _matchRegExp(self, metadata, property_name, value): - if property_name not in metadata: - return False - value = re.escape(value) #Escape for regex patterns. - value = "^" + value.replace("\\*", ".*") + "$" #Instead of (now escaped) asterisks, match on any string. Also add anchors for a complete match. - if self._ignore_case: - value_pattern = re.compile(value, re.IGNORECASE) - else: - value_pattern = re.compile(value) - - return value_pattern.match(str(metadata[property_name])) - - # Check to see if a container matches with a string - def _matchString(self, metadata, property_name, value): - if property_name not in metadata: - return False - return value.lower() == str(metadata[property_name]).lower() diff --git a/plugins/Toolbox/src/CloudApiModel.py b/plugins/Toolbox/src/CloudApiModel.py deleted file mode 100644 index bef37d8173..0000000000 --- a/plugins/Toolbox/src/CloudApiModel.py +++ /dev/null @@ -1,29 +0,0 @@ -from typing import Union - -from cura import ApplicationMetadata -from cura.UltimakerCloud import UltimakerCloudConstants - - -class CloudApiModel: - sdk_version = ApplicationMetadata.CuraSDKVersion # type: Union[str, int] - cloud_api_version = UltimakerCloudConstants.CuraCloudAPIVersion # type: str - cloud_api_root = UltimakerCloudConstants.CuraCloudAPIRoot # type: str - api_url = "{cloud_api_root}/cura-packages/v{cloud_api_version}/cura/v{sdk_version}".format( - cloud_api_root = cloud_api_root, - cloud_api_version = cloud_api_version, - sdk_version = sdk_version - ) # type: str - - # https://api.ultimaker.com/cura-packages/v1/user/packages - api_url_user_packages = "{cloud_api_root}/cura-packages/v{cloud_api_version}/user/packages".format( - cloud_api_root=cloud_api_root, - cloud_api_version=cloud_api_version, - ) - - @classmethod - def userPackageUrl(cls, package_id: str) -> str: - """https://api.ultimaker.com/cura-packages/v1/user/packages/{package_id}""" - - return (CloudApiModel.api_url_user_packages + "/{package_id}").format( - package_id=package_id - ) diff --git a/plugins/Toolbox/src/CloudSync/CloudApiClient.py b/plugins/Toolbox/src/CloudSync/CloudApiClient.py deleted file mode 100644 index 21eb1bdbd2..0000000000 --- a/plugins/Toolbox/src/CloudSync/CloudApiClient.py +++ /dev/null @@ -1,52 +0,0 @@ -from UM.Logger import Logger -from UM.TaskManagement.HttpRequestManager import HttpRequestManager -from UM.TaskManagement.HttpRequestScope import JsonDecoratorScope -from cura.CuraApplication import CuraApplication -from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope -from ..CloudApiModel import CloudApiModel - - -class CloudApiClient: - """Manages Cloud subscriptions - - When a package is added to a user's account, the user is 'subscribed' to that package. - Whenever the user logs in on another instance of Cura, these subscriptions can be used to sync the user's plugins - - Singleton: use CloudApiClient.getInstance() instead of CloudApiClient() - """ - - __instance = None - - @classmethod - def getInstance(cls, app: CuraApplication): - if not cls.__instance: - cls.__instance = CloudApiClient(app) - return cls.__instance - - def __init__(self, app: CuraApplication) -> None: - if self.__instance is not None: - raise RuntimeError("This is a Singleton. use getInstance()") - - self._scope = JsonDecoratorScope(UltimakerCloudScope(app)) # type: JsonDecoratorScope - - app.getPackageManager().packageInstalled.connect(self._onPackageInstalled) - - def unsubscribe(self, package_id: str) -> None: - url = CloudApiModel.userPackageUrl(package_id) - HttpRequestManager.getInstance().delete(url = url, scope = self._scope) - - def _subscribe(self, package_id: str) -> None: - """You probably don't want to use this directly. All installed packages will be automatically subscribed.""" - - Logger.debug("Subscribing to {}", package_id) - data = "{\"data\": {\"package_id\": \"%s\", \"sdk_version\": \"%s\"}}" % (package_id, CloudApiModel.sdk_version) - HttpRequestManager.getInstance().put( - url = CloudApiModel.api_url_user_packages, - data = data.encode(), - scope = self._scope - ) - - def _onPackageInstalled(self, package_id: str): - if CuraApplication.getInstance().getCuraAPI().account.isLoggedIn: - # We might already be subscribed, but checking would take one extra request. Instead, simply subscribe - self._subscribe(package_id) diff --git a/plugins/Toolbox/src/CloudSync/CloudPackageChecker.py b/plugins/Toolbox/src/CloudSync/CloudPackageChecker.py deleted file mode 100644 index 6d2ed1dcbd..0000000000 --- a/plugins/Toolbox/src/CloudSync/CloudPackageChecker.py +++ /dev/null @@ -1,164 +0,0 @@ -# Copyright (c) 2020 Ultimaker B.V. -# Cura is released under the terms of the LGPLv3 or higher. - -import json -from typing import List, Dict, Any, Set -from typing import Optional - -from PyQt5.QtCore import QObject -from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest - -from UM import i18nCatalog -from UM.Logger import Logger -from UM.Message import Message -from UM.Signal import Signal -from UM.TaskManagement.HttpRequestScope import JsonDecoratorScope -from cura.API.Account import SyncState -from cura.CuraApplication import CuraApplication, ApplicationMetadata -from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope -from .SubscribedPackagesModel import SubscribedPackagesModel -from ..CloudApiModel import CloudApiModel - - -class CloudPackageChecker(QObject): - - SYNC_SERVICE_NAME = "CloudPackageChecker" - - def __init__(self, application: CuraApplication) -> None: - super().__init__() - - self.discrepancies = Signal() # Emits SubscribedPackagesModel - self._application = application # type: CuraApplication - self._scope = JsonDecoratorScope(UltimakerCloudScope(application)) - self._model = SubscribedPackagesModel() - self._message = None # type: Optional[Message] - - self._application.initializationFinished.connect(self._onAppInitialized) - self._i18n_catalog = i18nCatalog("cura") - self._sdk_version = ApplicationMetadata.CuraSDKVersion - self._last_notified_packages = set() # type: Set[str] - """Packages for which a notification has been shown. No need to bother the user twice for equal content""" - - # This is a plugin, so most of the components required are not ready when - # this is initialized. Therefore, we wait until the application is ready. - def _onAppInitialized(self) -> None: - self._package_manager = self._application.getPackageManager() - # initial check - self._getPackagesIfLoggedIn() - - self._application.getCuraAPI().account.loginStateChanged.connect(self._onLoginStateChanged) - self._application.getCuraAPI().account.syncRequested.connect(self._getPackagesIfLoggedIn) - - def _onLoginStateChanged(self) -> None: - # reset session - self._last_notified_packages = set() - self._getPackagesIfLoggedIn() - - def _getPackagesIfLoggedIn(self) -> None: - if self._application.getCuraAPI().account.isLoggedIn: - self._getUserSubscribedPackages() - else: - self._hideSyncMessage() - - def _getUserSubscribedPackages(self) -> None: - self._application.getCuraAPI().account.setSyncState(self.SYNC_SERVICE_NAME, SyncState.SYNCING) - url = CloudApiModel.api_url_user_packages - self._application.getHttpRequestManager().get(url, - callback = self._onUserPackagesRequestFinished, - error_callback = self._onUserPackagesRequestFinished, - timeout = 10, - scope = self._scope) - - def _onUserPackagesRequestFinished(self, reply: "QNetworkReply", error: Optional["QNetworkReply.NetworkError"] = None) -> None: - if error is not None or reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) != 200: - Logger.log("w", - "Requesting user packages failed, response code %s while trying to connect to %s", - reply.attribute(QNetworkRequest.HttpStatusCodeAttribute), reply.url()) - self._application.getCuraAPI().account.setSyncState(self.SYNC_SERVICE_NAME, SyncState.ERROR) - return - - try: - json_data = json.loads(bytes(reply.readAll()).decode("utf-8")) - # Check for errors: - if "errors" in json_data: - for error in json_data["errors"]: - Logger.log("e", "%s", error["title"]) - self._application.getCuraAPI().account.setSyncState(self.SYNC_SERVICE_NAME, SyncState.ERROR) - return - self._handleCompatibilityData(json_data["data"]) - except json.decoder.JSONDecodeError: - Logger.log("w", "Received invalid JSON for user subscribed packages from the Web Marketplace") - - self._application.getCuraAPI().account.setSyncState(self.SYNC_SERVICE_NAME, SyncState.SUCCESS) - - def _handleCompatibilityData(self, subscribed_packages_payload: List[Dict[str, Any]]) -> None: - user_subscribed_packages = {plugin["package_id"] for plugin in subscribed_packages_payload} - user_installed_packages = self._package_manager.getAllInstalledPackageIDs() - - # We need to re-evaluate the dismissed packages - # (i.e. some package might got updated to the correct SDK version in the meantime, - # hence remove them from the Dismissed Incompatible list) - self._package_manager.reEvaluateDismissedPackages(subscribed_packages_payload, self._sdk_version) - user_dismissed_packages = self._package_manager.getDismissedPackages() - if user_dismissed_packages: - user_installed_packages.update(user_dismissed_packages) - - # We check if there are packages installed in Web Marketplace but not in Cura marketplace - package_discrepancy = list(user_subscribed_packages.difference(user_installed_packages)) - - if user_subscribed_packages != self._last_notified_packages: - # scenario: - # 1. user subscribes to a package - # 2. dismisses the license/unsubscribes - # 3. subscribes to the same package again - # in this scenario we want to notify the user again. To capture that there was a change during - # step 2, we clear the last_notified after step 2. This way, the user will be notified after - # step 3 even though the list of packages for step 1 and 3 are equal - self._last_notified_packages = set() - - if package_discrepancy: - account = self._application.getCuraAPI().account - account.setUpdatePackagesAction(lambda: self._onSyncButtonClicked(None, None)) - - if user_subscribed_packages == self._last_notified_packages: - # already notified user about these - return - - Logger.log("d", "Discrepancy found between Cloud subscribed packages and Cura installed packages") - self._model.addDiscrepancies(package_discrepancy) - self._model.initialize(self._package_manager, subscribed_packages_payload) - self._showSyncMessage() - self._last_notified_packages = user_subscribed_packages - - def _showSyncMessage(self) -> None: - """Show the message if it is not already shown""" - - if self._message is not None: - self._message.show() - return - - sync_message = Message(self._i18n_catalog.i18nc( - "@info:generic", - "Do you want to sync material and software packages with your account?"), - title = self._i18n_catalog.i18nc("@info:title", "Changes detected from your Ultimaker account", )) - sync_message.addAction("sync", - name = self._i18n_catalog.i18nc("@action:button", "Sync"), - icon = "", - description = "Sync your plugins and print profiles to Ultimaker Cura.", - button_align = Message.ActionButtonAlignment.ALIGN_RIGHT) - sync_message.actionTriggered.connect(self._onSyncButtonClicked) - sync_message.show() - self._message = sync_message - - def _hideSyncMessage(self) -> None: - """Hide the message if it is showing""" - - if self._message is not None: - self._message.hide() - self._message = None - - def _onSyncButtonClicked(self, sync_message: Optional[Message], sync_message_action: Optional[str]) -> None: - if sync_message is not None: - sync_message.hide() - self._hideSyncMessage() # Should be the same message, but also sets _message to None - self.discrepancies.emit(self._model) diff --git a/plugins/Toolbox/src/CloudSync/DiscrepanciesPresenter.py b/plugins/Toolbox/src/CloudSync/DiscrepanciesPresenter.py deleted file mode 100644 index cee2f6318a..0000000000 --- a/plugins/Toolbox/src/CloudSync/DiscrepanciesPresenter.py +++ /dev/null @@ -1,41 +0,0 @@ -import os -from typing import Optional - -from PyQt5.QtCore import QObject, pyqtSlot - -from UM.Qt.QtApplication import QtApplication -from UM.Signal import Signal -from .SubscribedPackagesModel import SubscribedPackagesModel - - -class DiscrepanciesPresenter(QObject): - """Shows a list of packages to be added or removed. The user can select which packages to (un)install. The user's - - choices are emitted on the `packageMutations` Signal. - """ - - def __init__(self, app: QtApplication) -> None: - super().__init__(app) - - self.packageMutations = Signal() # Emits SubscribedPackagesModel - - self._app = app - self._package_manager = app.getPackageManager() - self._dialog = None # type: Optional[QObject] - self._compatibility_dialog_path = "resources/qml/dialogs/CompatibilityDialog.qml" - - def present(self, plugin_path: str, model: SubscribedPackagesModel) -> None: - path = os.path.join(plugin_path, self._compatibility_dialog_path) - self._dialog = self._app.createQmlComponent(path, {"subscribedPackagesModel": model, "handler": self}) - assert self._dialog - self._dialog.accepted.connect(lambda: self._onConfirmClicked(model)) - - def _onConfirmClicked(self, model: SubscribedPackagesModel) -> None: - # If there are incompatible packages - automatically dismiss them - if model.getIncompatiblePackages(): - self._package_manager.dismissAllIncompatiblePackages(model.getIncompatiblePackages()) - # For now, all compatible packages presented to the user should be installed. - # Later, we might remove items for which the user unselected the package - if model.getCompatiblePackages(): - model.setItems(model.getCompatiblePackages()) - self.packageMutations.emit(model) diff --git a/plugins/Toolbox/src/CloudSync/DownloadPresenter.py b/plugins/Toolbox/src/CloudSync/DownloadPresenter.py deleted file mode 100644 index 8a5e763f3c..0000000000 --- a/plugins/Toolbox/src/CloudSync/DownloadPresenter.py +++ /dev/null @@ -1,153 +0,0 @@ -# Copyright (c) 2020 Ultimaker B.V. -# Cura is released under the terms of the LGPLv3 or higher. - -import tempfile -from typing import Dict, List, Any - -from PyQt5.QtNetwork import QNetworkReply - -from UM.i18n import i18nCatalog -from UM.Logger import Logger -from UM.Message import Message -from UM.Signal import Signal -from UM.TaskManagement.HttpRequestManager import HttpRequestManager -from cura.CuraApplication import CuraApplication -from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope -from .SubscribedPackagesModel import SubscribedPackagesModel - -i18n_catalog = i18nCatalog("cura") - - -class DownloadPresenter: - """Downloads a set of packages from the Ultimaker Cloud Marketplace - - use download() exactly once: should not be used for multiple sets of downloads since this class contains state - """ - - DISK_WRITE_BUFFER_SIZE = 256 * 1024 # 256 KB - - def __init__(self, app: CuraApplication) -> None: - # Emits (Dict[str, str], List[str]) # (success_items, error_items) - # Dict{success_package_id, temp_file_path} - # List[errored_package_id] - self.done = Signal() - - self._app = app - self._scope = UltimakerCloudScope(app) - - self._started = False - self._progress_message = self._createProgressMessage() - self._progress = {} # type: Dict[str, Dict[str, Any]] # package_id, Dict - self._error = [] # type: List[str] # package_id - - def download(self, model: SubscribedPackagesModel) -> None: - if self._started: - Logger.error("Download already started. Create a new %s instead", self.__class__.__name__) - return - - manager = HttpRequestManager.getInstance() - for item in model.items: - package_id = item["package_id"] - - def finishedCallback(reply: QNetworkReply, pid = package_id) -> None: - self._onFinished(pid, reply) - - def progressCallback(rx: int, rt: int, pid = package_id) -> None: - self._onProgress(pid, rx, rt) - - def errorCallback(reply: QNetworkReply, error: QNetworkReply.NetworkError, pid = package_id) -> None: - self._onError(pid) - - request_data = manager.get( - item["download_url"], - callback = finishedCallback, - download_progress_callback = progressCallback, - error_callback = errorCallback, - scope = self._scope) - - self._progress[package_id] = { - "received": 0, - "total": 1, # make sure this is not considered done yet. Also divByZero-safe - "file_written": None, - "request_data": request_data, - "package_model": item - } - - self._started = True - self._progress_message.show() - - def abort(self) -> None: - manager = HttpRequestManager.getInstance() - for item in self._progress.values(): - manager.abortRequest(item["request_data"]) - - # Aborts all current operations and returns a copy with the same settings such as app and scope - def resetCopy(self) -> "DownloadPresenter": - self.abort() - self.done.disconnectAll() - return DownloadPresenter(self._app) - - def _createProgressMessage(self) -> Message: - return Message(i18n_catalog.i18nc("@info:generic", "Syncing..."), - lifetime = 0, - use_inactivity_timer = False, - progress = 0.0, - title = i18n_catalog.i18nc("@info:title", "Changes detected from your Ultimaker account")) - - def _onFinished(self, package_id: str, reply: QNetworkReply) -> None: - self._progress[package_id]["received"] = self._progress[package_id]["total"] - - try: - with tempfile.NamedTemporaryFile(mode = "wb+", suffix = ".curapackage", delete = False) as temp_file: - bytes_read = reply.read(self.DISK_WRITE_BUFFER_SIZE) - while bytes_read: - temp_file.write(bytes_read) - bytes_read = reply.read(self.DISK_WRITE_BUFFER_SIZE) - self._app.processEvents() - self._progress[package_id]["file_written"] = temp_file.name - except IOError as e: - Logger.logException("e", "Failed to write downloaded package to temp file", e) - self._onError(package_id) - temp_file.close() - - self._checkDone() - - def _onProgress(self, package_id: str, rx: int, rt: int) -> None: - self._progress[package_id]["received"] = rx - self._progress[package_id]["total"] = rt - - received = 0 - total = 0 - for item in self._progress.values(): - received += item["received"] - total += item["total"] - - if total == 0: # Total download size is 0, or unknown, or there are no progress items at all. - self._progress_message.setProgress(100.0) - return - - self._progress_message.setProgress(100.0 * (received / total)) # [0 .. 100] % - - def _onError(self, package_id: str) -> None: - self._progress.pop(package_id) - self._error.append(package_id) - self._checkDone() - - def _checkDone(self) -> bool: - for item in self._progress.values(): - if not item["file_written"]: - return False - - success_items = { - package_id: - { - "package_path": value["file_written"], - "icon_url": value["package_model"]["icon_url"] - } - for package_id, value in self._progress.items() - } - error_items = [package_id for package_id in self._error] - - self._progress_message.hide() - self.done.emit(success_items, error_items) - return True diff --git a/plugins/Toolbox/src/CloudSync/LicenseModel.py b/plugins/Toolbox/src/CloudSync/LicenseModel.py deleted file mode 100644 index 335a91ef84..0000000000 --- a/plugins/Toolbox/src/CloudSync/LicenseModel.py +++ /dev/null @@ -1,77 +0,0 @@ -from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal -from UM.i18n import i18nCatalog - -catalog = i18nCatalog("cura") - - -# Model for the ToolboxLicenseDialog -class LicenseModel(QObject): - DEFAULT_DECLINE_BUTTON_TEXT = catalog.i18nc("@button", "Decline") - ACCEPT_BUTTON_TEXT = catalog.i18nc("@button", "Agree") - - dialogTitleChanged = pyqtSignal() - packageNameChanged = pyqtSignal() - licenseTextChanged = pyqtSignal() - iconChanged = pyqtSignal() - - def __init__(self, decline_button_text: str = DEFAULT_DECLINE_BUTTON_TEXT) -> None: - super().__init__() - - self._current_page_idx = 0 - self._page_count = 1 - self._dialogTitle = "" - self._license_text = "" - self._package_name = "" - self._icon_url = "" - self._decline_button_text = decline_button_text - - @pyqtProperty(str, constant = True) - def acceptButtonText(self): - return self.ACCEPT_BUTTON_TEXT - - @pyqtProperty(str, constant = True) - def declineButtonText(self): - return self._decline_button_text - - @pyqtProperty(str, notify=dialogTitleChanged) - def dialogTitle(self) -> str: - return self._dialogTitle - - @pyqtProperty(str, notify=packageNameChanged) - def packageName(self) -> str: - return self._package_name - - def setPackageName(self, name: str) -> None: - self._package_name = name - self.packageNameChanged.emit() - - @pyqtProperty(str, notify=iconChanged) - def iconUrl(self) -> str: - return self._icon_url - - def setIconUrl(self, url: str): - self._icon_url = url - self.iconChanged.emit() - - @pyqtProperty(str, notify=licenseTextChanged) - def licenseText(self) -> str: - return self._license_text - - def setLicenseText(self, license_text: str) -> None: - if self._license_text != license_text: - self._license_text = license_text - self.licenseTextChanged.emit() - - def setCurrentPageIdx(self, idx: int) -> None: - self._current_page_idx = idx - self._updateDialogTitle() - - def setPageCount(self, count: int) -> None: - self._page_count = count - self._updateDialogTitle() - - def _updateDialogTitle(self): - self._dialogTitle = catalog.i18nc("@title:window", "Plugin License Agreement") - if self._page_count > 1: - self._dialogTitle = self._dialogTitle + " ({}/{})".format(self._current_page_idx + 1, self._page_count) - self.dialogTitleChanged.emit() diff --git a/plugins/Toolbox/src/CloudSync/LicensePresenter.py b/plugins/Toolbox/src/CloudSync/LicensePresenter.py deleted file mode 100644 index 39ce11c8d3..0000000000 --- a/plugins/Toolbox/src/CloudSync/LicensePresenter.py +++ /dev/null @@ -1,142 +0,0 @@ -# Copyright (c) 2021 Ultimaker B.V. -# Cura is released under the terms of the LGPLv3 or higher. - -import os -from collections import OrderedDict -from typing import Dict, Optional, List, Any - -from PyQt5.QtCore import QObject, pyqtSlot - -from UM.Logger import Logger -from UM.PackageManager import PackageManager -from UM.Signal import Signal -from cura.CuraApplication import CuraApplication -from UM.i18n import i18nCatalog - -from .LicenseModel import LicenseModel - - -class LicensePresenter(QObject): - """Presents licenses for a set of packages for the user to accept or reject. - - Call present() exactly once to show a licenseDialog for a set of packages - Before presenting another set of licenses, create a new instance using resetCopy(). - - licenseAnswers emits a list of Dicts containing answers when the user has made a choice for all provided packages. - """ - - def __init__(self, app: CuraApplication) -> None: - super().__init__() - self._presented = False - """Whether present() has been called and state is expected to be initialized""" - self._catalog = i18nCatalog("cura") - self._dialog = None # type: Optional[QObject] - self._package_manager = app.getPackageManager() # type: PackageManager - # Emits List[Dict[str, [Any]] containing for example - # [{ "package_id": "BarbarianPlugin", "package_path" : "/tmp/dg345as", "accepted" : True }] - self.licenseAnswers = Signal() - - self._current_package_idx = 0 - self._package_models = [] # type: List[Dict] - decline_button_text = self._catalog.i18nc("@button", "Decline and remove from account") - self._license_model = LicenseModel(decline_button_text=decline_button_text) # type: LicenseModel - self._page_count = 0 - - self._app = app - - self._compatibility_dialog_path = "resources/qml/dialogs/ToolboxLicenseDialog.qml" - - def present(self, plugin_path: str, packages: Dict[str, Dict[str, str]]) -> None: - """Show a license dialog for multiple packages where users can read a license and accept or decline them - - :param plugin_path: Root directory of the Toolbox plugin - :param packages: Dict[package id, file path] - """ - if self._presented: - Logger.error("{clazz} is single-use. Create a new {clazz} instead", clazz=self.__class__.__name__) - return - - path = os.path.join(plugin_path, self._compatibility_dialog_path) - - self._initState(packages) - - if self._page_count == 0: - self.licenseAnswers.emit(self._package_models) - return - - if self._dialog is None: - - context_properties = { - "catalog": self._catalog, - "licenseModel": self._license_model, - "handler": self - } - self._dialog = self._app.createQmlComponent(path, context_properties) - self._presentCurrentPackage() - self._presented = True - - def resetCopy(self) -> "LicensePresenter": - """Clean up and return a new copy with the same settings such as app""" - if self._dialog: - self._dialog.close() - self.licenseAnswers.disconnectAll() - return LicensePresenter(self._app) - - @pyqtSlot() - def onLicenseAccepted(self) -> None: - self._package_models[self._current_package_idx]["accepted"] = True - self._checkNextPage() - - @pyqtSlot() - def onLicenseDeclined(self) -> None: - self._package_models[self._current_package_idx]["accepted"] = False - self._checkNextPage() - - def _initState(self, packages: Dict[str, Dict[str, Any]]) -> None: - - implicitly_accepted_count = 0 - - for package_id, item in packages.items(): - item["package_id"] = package_id - try: - item["licence_content"] = self._package_manager.getPackageLicense(item["package_path"]) - except EnvironmentError as e: - Logger.error(f"Could not open downloaded package {package_id} to read license file! {type(e)} - {e}") - continue # Skip this package. - if item["licence_content"] is None: - # Implicitly accept when there is no license - item["accepted"] = True - implicitly_accepted_count = implicitly_accepted_count + 1 - self._package_models.append(item) - else: - item["accepted"] = None #: None: no answer yet - # When presenting the packages, we want to show packages which have a license first. - # In fact, we don't want to show the others at all because they are implicitly accepted - self._package_models.insert(0, item) - CuraApplication.getInstance().processEvents() - self._page_count = len(self._package_models) - implicitly_accepted_count - self._license_model.setPageCount(self._page_count) - - - def _presentCurrentPackage(self) -> None: - package_model = self._package_models[self._current_package_idx] - package_info = self._package_manager.getPackageInfo(package_model["package_path"]) - - self._license_model.setCurrentPageIdx(self._current_package_idx) - self._license_model.setPackageName(package_info["display_name"]) - self._license_model.setIconUrl(package_model["icon_url"]) - self._license_model.setLicenseText(package_model["licence_content"]) - if self._dialog: - self._dialog.open() # Does nothing if already open - - def _checkNextPage(self) -> None: - if self._current_package_idx + 1 < self._page_count: - self._current_package_idx += 1 - self._presentCurrentPackage() - else: - if self._dialog: - self._dialog.close() - self.licenseAnswers.emit(self._package_models) - - - diff --git a/plugins/Toolbox/src/CloudSync/RestartApplicationPresenter.py b/plugins/Toolbox/src/CloudSync/RestartApplicationPresenter.py deleted file mode 100644 index 8776d1782a..0000000000 --- a/plugins/Toolbox/src/CloudSync/RestartApplicationPresenter.py +++ /dev/null @@ -1,32 +0,0 @@ -from UM import i18nCatalog -from UM.Message import Message -from cura.CuraApplication import CuraApplication - - -class RestartApplicationPresenter: - """Presents a dialog telling the user that a restart is required to apply changes - - Since we cannot restart Cura, the app is closed instead when the button is clicked - """ - def __init__(self, app: CuraApplication) -> None: - self._app = app - self._i18n_catalog = i18nCatalog("cura") - - def present(self) -> None: - app_name = self._app.getApplicationDisplayName() - - message = Message(self._i18n_catalog.i18nc("@info:generic", - "You need to quit and restart {} before changes have effect.", - app_name)) - - message.addAction("quit", - name="Quit " + app_name, - icon = "", - description="Close the application", - button_align=Message.ActionButtonAlignment.ALIGN_RIGHT) - - message.actionTriggered.connect(self._quitClicked) - message.show() - - def _quitClicked(self, *_): - self._app.windowClosed() diff --git a/plugins/Toolbox/src/CloudSync/SubscribedPackagesModel.py b/plugins/Toolbox/src/CloudSync/SubscribedPackagesModel.py deleted file mode 100644 index db16c5ea84..0000000000 --- a/plugins/Toolbox/src/CloudSync/SubscribedPackagesModel.py +++ /dev/null @@ -1,74 +0,0 @@ -# Copyright (c) 2020 Ultimaker B.V. -# Cura is released under the terms of the LGPLv3 or higher. - -from PyQt5.QtCore import Qt, pyqtProperty, pyqtSlot - -from UM.PackageManager import PackageManager -from UM.Qt.ListModel import ListModel -from UM.Version import Version - -from cura import ApplicationMetadata -from typing import List, Dict, Any - - -class SubscribedPackagesModel(ListModel): - def __init__(self, parent = None): - super().__init__(parent) - - self._items = [] - self._metadata = None - self._discrepancies = None - self._sdk_version = ApplicationMetadata.CuraSDKVersion - - self.addRoleName(Qt.UserRole + 1, "package_id") - self.addRoleName(Qt.UserRole + 2, "display_name") - self.addRoleName(Qt.UserRole + 3, "icon_url") - self.addRoleName(Qt.UserRole + 4, "is_compatible") - self.addRoleName(Qt.UserRole + 5, "is_dismissed") - - @pyqtProperty(bool, constant=True) - def hasCompatiblePackages(self) -> bool: - for item in self._items: - if item['is_compatible']: - return True - return False - - @pyqtProperty(bool, constant=True) - def hasIncompatiblePackages(self) -> bool: - for item in self._items: - if not item['is_compatible']: - return True - return False - - def addDiscrepancies(self, discrepancy: List[str]) -> None: - self._discrepancies = discrepancy - - def getCompatiblePackages(self) -> List[Dict[str, Any]]: - return [package for package in self._items if package["is_compatible"]] - - def getIncompatiblePackages(self) -> List[str]: - return [package["package_id"] for package in self._items if not package["is_compatible"]] - - def initialize(self, package_manager: PackageManager, subscribed_packages_payload: List[Dict[str, Any]]) -> None: - self._items.clear() - for item in subscribed_packages_payload: - if item["package_id"] not in self._discrepancies: - continue - package = { - "package_id": item["package_id"], - "display_name": item["display_name"], - "sdk_versions": item["sdk_versions"], - "download_url": item["download_url"], - "md5_hash": item["md5_hash"], - "is_dismissed": False, - } - - compatible = any(package_manager.isPackageCompatible(Version(version)) for version in item["sdk_versions"]) - package.update({"is_compatible": compatible}) - - try: - package.update({"icon_url": item["icon_url"]}) - except KeyError: # There is no 'icon_url" in the response payload for this package - package.update({"icon_url": ""}) - self._items.append(package) - self.setItems(self._items) diff --git a/plugins/Toolbox/src/CloudSync/SyncOrchestrator.py b/plugins/Toolbox/src/CloudSync/SyncOrchestrator.py deleted file mode 100644 index bb37c6d4a9..0000000000 --- a/plugins/Toolbox/src/CloudSync/SyncOrchestrator.py +++ /dev/null @@ -1,114 +0,0 @@ -# Copyright (c) 2021 Ultimaker B.V. -# Cura is released under the terms of the LGPLv3 or higher. - -import os -from typing import List, Dict, Any, cast - -from UM import i18n_catalog -from UM.Extension import Extension -from UM.Logger import Logger -from UM.Message import Message -from UM.PluginRegistry import PluginRegistry -from cura.CuraApplication import CuraApplication -from .CloudPackageChecker import CloudPackageChecker -from .CloudApiClient import CloudApiClient -from .DiscrepanciesPresenter import DiscrepanciesPresenter -from .DownloadPresenter import DownloadPresenter -from .LicensePresenter import LicensePresenter -from .RestartApplicationPresenter import RestartApplicationPresenter -from .SubscribedPackagesModel import SubscribedPackagesModel - - -class SyncOrchestrator(Extension): - """Orchestrates the synchronizing of packages from the user account to the installed packages - - Example flow: - - - CloudPackageChecker compares a list of packages the user `subscribed` to in their account - If there are `discrepancies` between the account and locally installed packages, they are emitted - - DiscrepanciesPresenter shows a list of packages to be added or removed to the user. It emits the `packageMutations` - the user selected to be performed - - The SyncOrchestrator uses PackageManager to remove local packages the users wants to see removed - - The DownloadPresenter shows a download progress dialog. It emits A tuple of succeeded and failed downloads - - The LicensePresenter extracts licenses from the downloaded packages and presents a license for each package to - be installed. It emits the `licenseAnswers` signal for accept or declines - - The CloudApiClient removes the declined packages from the account - - The SyncOrchestrator uses PackageManager to install the downloaded packages and delete temp files. - - The RestartApplicationPresenter notifies the user that a restart is required for changes to take effect - """ - - def __init__(self, app: CuraApplication) -> None: - super().__init__() - # Differentiate This PluginObject from the Toolbox. self.getId() includes _name. - # getPluginId() will return the same value for The toolbox extension and this one - self._name = "SyncOrchestrator" - - self._package_manager = app.getPackageManager() - # Keep a reference to the CloudApiClient. it watches for installed packages and subscribes to them - self._cloud_api = CloudApiClient.getInstance(app) # type: CloudApiClient - - self._checker = CloudPackageChecker(app) # type: CloudPackageChecker - self._checker.discrepancies.connect(self._onDiscrepancies) - - self._discrepancies_presenter = DiscrepanciesPresenter(app) # type: DiscrepanciesPresenter - self._discrepancies_presenter.packageMutations.connect(self._onPackageMutations) - - self._download_presenter = DownloadPresenter(app) # type: DownloadPresenter - - self._license_presenter = LicensePresenter(app) # type: LicensePresenter - self._license_presenter.licenseAnswers.connect(self._onLicenseAnswers) - - self._restart_presenter = RestartApplicationPresenter(app) - - def _onDiscrepancies(self, model: SubscribedPackagesModel) -> None: - plugin_path = cast(str, PluginRegistry.getInstance().getPluginPath(self.getPluginId())) - self._discrepancies_presenter.present(plugin_path, model) - - def _onPackageMutations(self, mutations: SubscribedPackagesModel) -> None: - self._download_presenter = self._download_presenter.resetCopy() - self._download_presenter.done.connect(self._onDownloadFinished) - self._download_presenter.download(mutations) - - def _onDownloadFinished(self, success_items: Dict[str, Dict[str, str]], error_items: List[str]) -> None: - """Called when a set of packages have finished downloading - - :param success_items:: Dict[package_id, Dict[str, str]] - :param error_items:: List[package_id] - """ - if error_items: - message = i18n_catalog.i18nc("@info:generic", "{} plugins failed to download".format(len(error_items))) - self._showErrorMessage(message) - - plugin_path = cast(str, PluginRegistry.getInstance().getPluginPath(self.getPluginId())) - self._license_presenter = self._license_presenter.resetCopy() - self._license_presenter.licenseAnswers.connect(self._onLicenseAnswers) - self._license_presenter.present(plugin_path, success_items) - - # Called when user has accepted / declined all licenses for the downloaded packages - def _onLicenseAnswers(self, answers: List[Dict[str, Any]]) -> None: - has_changes = False # True when at least one package is installed - - for item in answers: - if item["accepted"]: - # install and subscribe packages - if not self._package_manager.installPackage(item["package_path"]): - message = "Could not install {}".format(item["package_id"]) - self._showErrorMessage(message) - continue - has_changes = True - else: - self._cloud_api.unsubscribe(item["package_id"]) - # delete temp file - try: - os.remove(item["package_path"]) - except EnvironmentError as e: # File was already removed, no access rights, etc. - Logger.error("Can't delete temporary package file: {err}".format(err = str(e))) - - if has_changes: - self._restart_presenter.present() - - def _showErrorMessage(self, text: str): - """Logs an error and shows it to the user""" - - Logger.error(text) - Message(text, lifetime = 0, message_type = Message.MessageType.ERROR).show() diff --git a/plugins/Toolbox/src/CloudSync/__init__.py b/plugins/Toolbox/src/CloudSync/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 --- a/plugins/Toolbox/src/CloudSync/__init__.py +++ /dev/null diff --git a/plugins/Toolbox/src/ConfigsModel.py b/plugins/Toolbox/src/ConfigsModel.py deleted file mode 100644 index a53817653f..0000000000 --- a/plugins/Toolbox/src/ConfigsModel.py +++ /dev/null @@ -1,38 +0,0 @@ -# Copyright (c) 2018 Ultimaker B.V. -# Cura is released under the terms of the LGPLv3 or higher. - -from PyQt5.QtCore import Qt - -from UM.Qt.ListModel import ListModel - - -class ConfigsModel(ListModel): - """Model that holds supported configurations (for material/quality packages).""" - - def __init__(self, parent = None): - super().__init__(parent) - - self._configs = None - - self.addRoleName(Qt.UserRole + 1, "machine") - self.addRoleName(Qt.UserRole + 2, "print_core") - self.addRoleName(Qt.UserRole + 3, "build_plate") - self.addRoleName(Qt.UserRole + 4, "support_material") - self.addRoleName(Qt.UserRole + 5, "quality") - - def setConfigs(self, configs): - self._configs = configs - self._update() - - def _update(self): - items = [] - for item in self._configs: - items.append({ - "machine": item["machine"], - "print_core": item["print_core"], - "build_plate": item["build_plate"], - "support_material": item["support_material"], - "quality": item["quality"] - }) - - self.setItems(items) diff --git a/plugins/Toolbox/src/PackagesModel.py b/plugins/Toolbox/src/PackagesModel.py deleted file mode 100644 index 97645ae466..0000000000 --- a/plugins/Toolbox/src/PackagesModel.py +++ /dev/null @@ -1,161 +0,0 @@ -# Copyright (c) 2021 Ultimaker B.V. -# Cura is released under the terms of the LGPLv3 or higher. - -import re -from typing import Dict - -from PyQt5.QtCore import Qt, pyqtProperty - -from UM.Logger import Logger -from UM.Qt.ListModel import ListModel - -from .ConfigsModel import ConfigsModel - - -class PackagesModel(ListModel): - """Model that holds Cura packages. - - By setting the filter property the instances held by this model can be changed. - """ - - def __init__(self, parent = None): - super().__init__(parent) - - self._metadata = None - - self.addRoleName(Qt.UserRole + 1, "id") - self.addRoleName(Qt.UserRole + 2, "type") - self.addRoleName(Qt.UserRole + 3, "name") - self.addRoleName(Qt.UserRole + 4, "version") - self.addRoleName(Qt.UserRole + 5, "author_id") - self.addRoleName(Qt.UserRole + 6, "author_name") - self.addRoleName(Qt.UserRole + 7, "author_email") - self.addRoleName(Qt.UserRole + 8, "description") - self.addRoleName(Qt.UserRole + 9, "icon_url") - self.addRoleName(Qt.UserRole + 10, "image_urls") - self.addRoleName(Qt.UserRole + 11, "download_url") - self.addRoleName(Qt.UserRole + 12, "last_updated") - self.addRoleName(Qt.UserRole + 13, "is_bundled") - self.addRoleName(Qt.UserRole + 14, "is_active") - self.addRoleName(Qt.UserRole + 15, "is_installed") # Scheduled pkgs are included in the model but should not be marked as actually installed - self.addRoleName(Qt.UserRole + 16, "has_configs") - self.addRoleName(Qt.UserRole + 17, "supported_configs") - self.addRoleName(Qt.UserRole + 18, "download_count") - self.addRoleName(Qt.UserRole + 19, "tags") - self.addRoleName(Qt.UserRole + 20, "links") - self.addRoleName(Qt.UserRole + 21, "website") - self.addRoleName(Qt.UserRole + 22, "login_required") - - # List of filters for queries. The result is the union of the each list of results. - self._filter = {} # type: Dict[str, str] - - def setMetadata(self, data): - if self._metadata != data: - self._metadata = data - self._update() - - def _update(self): - items = [] - - if self._metadata is None: - self.setItems(items) - return - - for package in self._metadata: - has_configs = False - configs_model = None - - links_dict = {} - if "data" in package: - # Links is a list of dictionaries with "title" and "url". Convert this list into a dict so it's easier - # to process. - link_list = package["data"]["links"] if "links" in package["data"] else [] - links_dict = {d["title"]: d["url"] for d in link_list} - - # This code never gets executed because the API response does not contain "supported_configs" in it - # It is so because 2y ago when this was created - it did contain it. But it was a prototype only - # and never got to production. As agreed with the team, it'll stay here for now, in case we decide to rework and use it - # The response payload has been changed. Please see: - # https://github.com/Ultimaker/Cura/compare/CURA-7072-temp?expand=1 - if "supported_configs" in package["data"]: - if len(package["data"]["supported_configs"]) > 0: - has_configs = True - configs_model = ConfigsModel() - configs_model.setConfigs(package["data"]["supported_configs"]) - - if "author_id" not in package["author"] or "display_name" not in package["author"]: - package["author"]["author_id"] = "" - package["author"]["display_name"] = "" - - items.append({ - "id": package["package_id"], - "type": package["package_type"], - "name": package["display_name"].strip(), - "version": package["package_version"], - "author_id": package["author"]["author_id"], - "author_name": package["author"]["display_name"], - "author_email": package["author"]["email"] if "email" in package["author"] else None, - "description": package["description"] if "description" in package else None, - "icon_url": package["icon_url"] if "icon_url" in package else None, - "image_urls": package["image_urls"] if "image_urls" in package else None, - "download_url": package["download_url"] if "download_url" in package else None, - "last_updated": package["last_updated"] if "last_updated" in package else None, - "is_bundled": package["is_bundled"] if "is_bundled" in package else False, - "is_active": package["is_active"] if "is_active" in package else False, - "is_installed": package["is_installed"] if "is_installed" in package else False, - "has_configs": has_configs, - "supported_configs": configs_model, - "download_count": package["download_count"] if "download_count" in package else 0, - "tags": package["tags"] if "tags" in package else [], - "links": links_dict, - "website": package["website"] if "website" in package else None, - "login_required": "login-required" in package.get("tags", []), - }) - - # Filter on all the key-word arguments. - for key, value in self._filter.items(): - if key == "tags": - key_filter = lambda item, v = value: v in item["tags"] - elif "*" in value: - key_filter = lambda candidate, k = key, v = value: self._matchRegExp(candidate, k, v) - else: - key_filter = lambda candidate, k = key, v = value: self._matchString(candidate, k, v) - items = filter(key_filter, items) - - # Execute all filters. - filtered_items = list(items) - - filtered_items.sort(key = lambda k: k["name"]) - self.setItems(filtered_items) - - def setFilter(self, filter_dict: Dict[str, str]) -> None: - """Set the filter of this model based on a string. - - :param filter_dict: Dictionary to do the filtering by. - """ - if filter_dict != self._filter: - self._filter = filter_dict - self._update() - - @pyqtProperty("QVariantMap", fset = setFilter, constant = True) - def filter(self) -> Dict[str, str]: - return self._filter - - # Check to see if a container matches with a regular expression - def _matchRegExp(self, metadata, property_name, value): - if property_name not in metadata: - return False - value = re.escape(value) #Escape for regex patterns. - value = "^" + value.replace("\\*", ".*") + "$" #Instead of (now escaped) asterisks, match on any string. Also add anchors for a complete match. - if self._ignore_case: - value_pattern = re.compile(value, re.IGNORECASE) - else: - value_pattern = re.compile(value) - - return value_pattern.match(str(metadata[property_name])) - - # Check to see if a container matches with a string - def _matchString(self, metadata, property_name, value): - if property_name not in metadata: - return False - return value.lower() == str(metadata[property_name]).lower() diff --git a/plugins/Toolbox/src/Toolbox.py b/plugins/Toolbox/src/Toolbox.py deleted file mode 100644 index e525a88d89..0000000000 --- a/plugins/Toolbox/src/Toolbox.py +++ /dev/null @@ -1,878 +0,0 @@ -# Copyright (c) 2021 Ultimaker B.V. -# Toolbox is released under the terms of the LGPLv3 or higher. - -import json -import os -import tempfile -from typing import cast, Any, Dict, List, Set, TYPE_CHECKING, Tuple, Optional, Union - -from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal, pyqtSlot -from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply - -from UM.Extension import Extension -from UM.Logger import Logger -from UM.PluginRegistry import PluginRegistry -from UM.TaskManagement.HttpRequestScope import JsonDecoratorScope -from UM.Version import Version -from UM.i18n import i18nCatalog -from cura import ApplicationMetadata -from cura.CuraApplication import CuraApplication -from cura.Machines.ContainerTree import ContainerTree -from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope -from .AuthorsModel import AuthorsModel -from .CloudApiModel import CloudApiModel -from .CloudSync.LicenseModel import LicenseModel -from .PackagesModel import PackagesModel - -if TYPE_CHECKING: - from UM.TaskManagement.HttpRequestData import HttpRequestData - from cura.Settings.GlobalStack import GlobalStack - -i18n_catalog = i18nCatalog("cura") - -DEFAULT_MARKETPLACE_ROOT = "https://marketplace.ultimaker.com" # type: str - -try: - from cura.CuraVersion import CuraMarketplaceRoot -except ImportError: - CuraMarketplaceRoot = DEFAULT_MARKETPLACE_ROOT - - -class Toolbox(QObject, Extension): - """Provides a marketplace for users to download plugins an materials""" - - def __init__(self, application: CuraApplication) -> None: - super().__init__() - - self._application = application # type: CuraApplication - - # Network: - self._download_request_data = None # type: Optional[HttpRequestData] - self._download_progress = 0 # type: float - self._is_downloading = False # type: bool - self._cloud_scope = UltimakerCloudScope(application) # type: UltimakerCloudScope - self._json_scope = JsonDecoratorScope(self._cloud_scope) # type: JsonDecoratorScope - - self._request_urls = {} # type: Dict[str, str] - self._to_update = [] # type: List[str] # Package_ids that are waiting to be updated - self._old_plugin_ids = set() # type: Set[str] - self._old_plugin_metadata = dict() # type: Dict[str, Dict[str, Any]] - - # The responses as given by the server parsed to a list. - self._server_response_data = { - "authors": [], - "packages": [], - "updates": [] - } # type: Dict[str, List[Any]] - - # Models: - self._models = { - "authors": AuthorsModel(self), - "packages": PackagesModel(self), - "updates": PackagesModel(self) - } # type: Dict[str, Union[AuthorsModel, PackagesModel]] - - self._plugins_showcase_model = PackagesModel(self) - self._plugins_available_model = PackagesModel(self) - self._plugins_installed_model = PackagesModel(self) - self._plugins_installed_model.setFilter({"is_bundled": "False"}) - self._plugins_bundled_model = PackagesModel(self) - self._plugins_bundled_model.setFilter({"is_bundled": "True"}) - self._materials_showcase_model = AuthorsModel(self) - self._materials_available_model = AuthorsModel(self) - self._materials_installed_model = PackagesModel(self) - self._materials_installed_model.setFilter({"is_bundled": "False"}) - self._materials_bundled_model = PackagesModel(self) - self._materials_bundled_model.setFilter({"is_bundled": "True"}) - self._materials_generic_model = PackagesModel(self) - - self._license_model = LicenseModel() - - # These properties are for keeping track of the UI state: - # ---------------------------------------------------------------------- - # View category defines which filter to use, and therefore effectively - # which category is currently being displayed. For example, possible - # values include "plugin" or "material", but also "installed". - self._view_category = "plugin" # type: str - - # View page defines which type of page layout to use. For example, - # possible values include "overview", "detail" or "author". - self._view_page = "welcome" # type: str - - # Active package refers to which package is currently being downloaded, - # installed, or otherwise modified. - self._active_package = None # type: Optional[Dict[str, Any]] - - self._dialog = None # type: Optional[QObject] - self._confirm_reset_dialog = None # type: Optional[QObject] - self._resetUninstallVariables() - - self._restart_required = False # type: bool - - # variables for the license agreement dialog - self._license_dialog_plugin_file_location = "" # type: str - - self._application.initializationFinished.connect(self._onAppInitialized) - - # Signals: - # -------------------------------------------------------------------------- - # Downloading changes - activePackageChanged = pyqtSignal() - onDownloadProgressChanged = pyqtSignal() - onIsDownloadingChanged = pyqtSignal() - restartRequiredChanged = pyqtSignal() - installChanged = pyqtSignal() - toolboxEnabledChanged = pyqtSignal() - - # UI changes - viewChanged = pyqtSignal() - detailViewChanged = pyqtSignal() - filterChanged = pyqtSignal() - metadataChanged = pyqtSignal() - showLicenseDialog = pyqtSignal() - closeLicenseDialog = pyqtSignal() - uninstallVariablesChanged = pyqtSignal() - - def _restart(self): - """Go back to the start state (welcome screen or loading if no login required)""" - - # For an Essentials build, login is mandatory - if not self._application.getCuraAPI().account.isLoggedIn and ApplicationMetadata.IsEnterpriseVersion: - self.setViewPage("welcome") - else: - self.setViewPage("loading") - self._fetchPackageData() - - def _resetUninstallVariables(self) -> None: - self._package_id_to_uninstall = None # type: Optional[str] - self._package_name_to_uninstall = "" - self._package_used_materials = [] # type: List[Tuple[GlobalStack, str, str]] - self._package_used_qualities = [] # type: List[Tuple[GlobalStack, str, str]] - - def getLicenseDialogPluginFileLocation(self) -> str: - return self._license_dialog_plugin_file_location - - def openLicenseDialog(self, plugin_name: str, license_content: str, plugin_file_location: str, icon_url: str) -> None: - # Set page 1/1 when opening the dialog for a single package - self._license_model.setCurrentPageIdx(0) - self._license_model.setPageCount(1) - self._license_model.setIconUrl(icon_url) - - self._license_model.setPackageName(plugin_name) - self._license_model.setLicenseText(license_content) - self._license_dialog_plugin_file_location = plugin_file_location - self.showLicenseDialog.emit() - - # This is a plugin, so most of the components required are not ready when - # this is initialized. Therefore, we wait until the application is ready. - def _onAppInitialized(self) -> None: - self._plugin_registry = self._application.getPluginRegistry() - self._package_manager = self._application.getPackageManager() - - # We need to construct a query like installed_packages=ID:VERSION&installed_packages=ID:VERSION, etc. - installed_package_ids_with_versions = [":".join(items) for items in - self._package_manager.getAllInstalledPackageIdsAndVersions()] - installed_packages_query = "&installed_packages=".join(installed_package_ids_with_versions) - - self._request_urls = { - "authors": "{base_url}/authors".format(base_url = CloudApiModel.api_url), - "packages": "{base_url}/packages".format(base_url = CloudApiModel.api_url), - "updates": "{base_url}/packages/package-updates?installed_packages={query}".format( - base_url = CloudApiModel.api_url, query = installed_packages_query) - } - - self._application.getCuraAPI().account.loginStateChanged.connect(self._restart) - - preferences = CuraApplication.getInstance().getPreferences() - - preferences.addPreference("info/automatic_plugin_update_check", True) - - # On boot we check which packages have updates. - if preferences.getValue("info/automatic_plugin_update_check") and len(installed_package_ids_with_versions) > 0: - # Request the latest and greatest! - self._makeRequestByType("updates") - - def _fetchPackageData(self) -> None: - self._makeRequestByType("packages") - self._makeRequestByType("authors") - self._updateInstalledModels() - - # Displays the toolbox - @pyqtSlot() - def launch(self) -> None: - if not self._dialog: - self._dialog = self._createDialog("Toolbox.qml") - - if not self._dialog: - Logger.log("e", "Unexpected error trying to create the 'Marketplace' dialog.") - return - - self._restart() - - self._dialog.show() - # Apply enabled/disabled state to installed plugins - self.toolboxEnabledChanged.emit() - - def _createDialog(self, qml_name: str) -> Optional[QObject]: - Logger.log("d", "Marketplace: Creating dialog [%s].", qml_name) - plugin_path = PluginRegistry.getInstance().getPluginPath(self.getPluginId()) - if not plugin_path: - return None - path = os.path.join(plugin_path, "resources", "qml", qml_name) - - dialog = self._application.createQmlComponent(path, { - "toolbox": self, - "handler": self, - "licenseModel": self._license_model - }) - if not dialog: - return None - return dialog - - def _convertPluginMetadata(self, plugin_data: Dict[str, Any]) -> Optional[Dict[str, Any]]: - try: - highest_sdk_version_supported = Version(0) - for supported_version in plugin_data["plugin"]["supported_sdk_versions"]: - if supported_version > highest_sdk_version_supported: - highest_sdk_version_supported = supported_version - - formatted = { - "package_id": plugin_data["id"], - "package_type": "plugin", - "display_name": plugin_data["plugin"]["name"], - "package_version": plugin_data["plugin"]["version"], - "sdk_version": highest_sdk_version_supported, - "author": { - "author_id": plugin_data["plugin"]["author"], - "display_name": plugin_data["plugin"]["author"] - }, - "is_installed": True, - "description": plugin_data["plugin"]["description"] - } - return formatted - except KeyError: - Logger.log("w", "Unable to convert plugin meta data %s", str(plugin_data)) - return None - - @pyqtSlot() - def _updateInstalledModels(self) -> None: - # This is moved here to avoid code duplication and so that after installing plugins they get removed from the - # list of old plugins - old_plugin_ids = self._plugin_registry.getInstalledPlugins() - installed_package_ids = self._package_manager.getAllInstalledPackageIDs() - scheduled_to_remove_package_ids = self._package_manager.getToRemovePackageIDs() - - self._old_plugin_ids = set() - self._old_plugin_metadata = dict() - - for plugin_id in old_plugin_ids: - # Neither the installed packages nor the packages that are scheduled to remove are old plugins - if plugin_id not in installed_package_ids and plugin_id not in scheduled_to_remove_package_ids: - Logger.log("d", "Found a plugin that was installed with the old plugin browser: %s", plugin_id) - - old_metadata = self._plugin_registry.getMetaData(plugin_id) - new_metadata = self._convertPluginMetadata(old_metadata) - if new_metadata is None: - # Something went wrong converting it. - continue - self._old_plugin_ids.add(plugin_id) - self._old_plugin_metadata[new_metadata["package_id"]] = new_metadata - - all_packages = self._package_manager.getAllInstalledPackagesInfo() - if "plugin" in all_packages: - # For old plugins, we only want to include the old custom plugin that were installed via the old toolbox. - # The bundled plugins will be included in JSON files in the "bundled_packages" folder, so the bundled - # plugins should be excluded from the old plugins list/dict. - all_plugin_package_ids = set(package["package_id"] for package in all_packages["plugin"]) - self._old_plugin_ids = set(plugin_id for plugin_id in self._old_plugin_ids - if plugin_id not in all_plugin_package_ids) - self._old_plugin_metadata = {k: v for k, v in self._old_plugin_metadata.items() if k in self._old_plugin_ids} - - self._plugins_installed_model.setMetadata(all_packages["plugin"] + list(self._old_plugin_metadata.values())) - self._plugins_bundled_model.setMetadata(all_packages["plugin"] + list(self._old_plugin_metadata.values())) - self.metadataChanged.emit() - if "material" in all_packages: - self._materials_installed_model.setMetadata(all_packages["material"]) - self._materials_bundled_model.setMetadata(all_packages["material"]) - self.metadataChanged.emit() - - @pyqtSlot(str) - def install(self, file_path: str) -> Optional[str]: - package_id = self._package_manager.installPackage(file_path) - self.installChanged.emit() - self._updateInstalledModels() - self.metadataChanged.emit() - self._restart_required = True - self.restartRequiredChanged.emit() - return package_id - - @pyqtSlot(str) - def checkPackageUsageAndUninstall(self, package_id: str) -> None: - """Check package usage and uninstall - - If the package is in use, you'll get a confirmation dialog to set everything to default - """ - - package_used_materials, package_used_qualities = self._package_manager.getMachinesUsingPackage(package_id) - if package_used_materials or package_used_qualities: - # Set up "uninstall variables" for resetMaterialsQualitiesAndUninstall - self._package_id_to_uninstall = package_id - package_info = self._package_manager.getInstalledPackageInfo(package_id) - self._package_name_to_uninstall = package_info.get("display_name", package_info.get("package_id")) - self._package_used_materials = package_used_materials - self._package_used_qualities = package_used_qualities - # Ask change to default material / profile - if self._confirm_reset_dialog is None: - self._confirm_reset_dialog = self._createDialog("dialogs/ToolboxConfirmUninstallResetDialog.qml") - self.uninstallVariablesChanged.emit() - if self._confirm_reset_dialog is None: - Logger.log("e", "ToolboxConfirmUninstallResetDialog should have been initialized, but it is not. Not showing dialog and not uninstalling package.") - else: - self._confirm_reset_dialog.show() - else: - # Plain uninstall - self.uninstall(package_id) - - @pyqtProperty(str, notify = uninstallVariablesChanged) - def pluginToUninstall(self) -> str: - return self._package_name_to_uninstall - - @pyqtProperty(str, notify = uninstallVariablesChanged) - def uninstallUsedMaterials(self) -> str: - return "\n".join(["%s (%s)" % (str(global_stack.getName()), material) for global_stack, extruder_nr, material in self._package_used_materials]) - - @pyqtProperty(str, notify = uninstallVariablesChanged) - def uninstallUsedQualities(self) -> str: - return "\n".join(["%s (%s)" % (str(global_stack.getName()), quality) for global_stack, extruder_nr, quality in self._package_used_qualities]) - - @pyqtSlot() - def closeConfirmResetDialog(self) -> None: - if self._confirm_reset_dialog is not None: - self._confirm_reset_dialog.close() - - @pyqtSlot() - def resetMaterialsQualitiesAndUninstall(self) -> None: - """Uses "uninstall variables" to reset qualities and materials, then uninstall - - It's used as an action on Confirm reset on Uninstall - """ - - application = CuraApplication.getInstance() - machine_manager = application.getMachineManager() - container_tree = ContainerTree.getInstance() - - for global_stack, extruder_nr, container_id in self._package_used_materials: - extruder = global_stack.extruderList[int(extruder_nr)] - approximate_diameter = extruder.getApproximateMaterialDiameter() - variant_node = container_tree.machines[global_stack.definition.getId()].variants[extruder.variant.getName()] - default_material_node = variant_node.preferredMaterial(approximate_diameter) - machine_manager.setMaterial(extruder_nr, default_material_node, global_stack = global_stack) - for global_stack, extruder_nr, container_id in self._package_used_qualities: - variant_names = [extruder.variant.getName() for extruder in global_stack.extruderList] - material_bases = [extruder.material.getMetaDataEntry("base_file") for extruder in global_stack.extruderList] - extruder_enabled = [extruder.isEnabled for extruder in global_stack.extruderList] - definition_id = global_stack.definition.getId() - machine_node = container_tree.machines[definition_id] - default_quality_group = machine_node.getQualityGroups(variant_names, material_bases, extruder_enabled)[machine_node.preferred_quality_type] - machine_manager.setQualityGroup(default_quality_group, global_stack = global_stack) - - if self._package_id_to_uninstall is not None: - self._markPackageMaterialsAsToBeUninstalled(self._package_id_to_uninstall) - self.uninstall(self._package_id_to_uninstall) - self._resetUninstallVariables() - self.closeConfirmResetDialog() - - @pyqtSlot() - def onLicenseAccepted(self): - self.closeLicenseDialog.emit() - package_id = self.install(self.getLicenseDialogPluginFileLocation()) - - - @pyqtSlot() - def onLicenseDeclined(self): - self.closeLicenseDialog.emit() - - def _markPackageMaterialsAsToBeUninstalled(self, package_id: str) -> None: - container_registry = self._application.getContainerRegistry() - - all_containers = self._package_manager.getPackageContainerIds(package_id) - for container_id in all_containers: - containers = container_registry.findInstanceContainers(id = container_id) - if not containers: - continue - container = containers[0] - if container.getMetaDataEntry("type") != "material": - continue - root_material_id = container.getMetaDataEntry("base_file") - root_material_containers = container_registry.findInstanceContainers(id = root_material_id) - if not root_material_containers: - continue - root_material_container = root_material_containers[0] - root_material_container.setMetaDataEntry("removed", True) - - @pyqtSlot(str) - def uninstall(self, package_id: str) -> None: - self._package_manager.removePackage(package_id, force_add = True) - self.installChanged.emit() - self._updateInstalledModels() - self.metadataChanged.emit() - self._restart_required = True - self.restartRequiredChanged.emit() - - def _update(self) -> None: - """Actual update packages that are in self._to_update""" - - if self._to_update: - plugin_id = self._to_update.pop(0) - remote_package = self.getRemotePackage(plugin_id) - if remote_package: - download_url = remote_package["download_url"] - Logger.log("d", "Updating package [%s]..." % plugin_id) - self.startDownload(download_url) - else: - Logger.log("e", "Could not update package [%s] because there is no remote package info available.", plugin_id) - - if self._to_update: - self._application.callLater(self._update) - - @pyqtSlot(str) - def update(self, plugin_id: str) -> None: - """Update a plugin by plugin_id""" - - self._to_update.append(plugin_id) - self._application.callLater(self._update) - - @pyqtSlot(str) - def enable(self, plugin_id: str) -> None: - self._plugin_registry.enablePlugin(plugin_id) - self.toolboxEnabledChanged.emit() - Logger.log("i", "%s was set as 'active'.", plugin_id) - self._restart_required = True - self.restartRequiredChanged.emit() - - @pyqtSlot(str) - def disable(self, plugin_id: str) -> None: - self._plugin_registry.disablePlugin(plugin_id) - self.toolboxEnabledChanged.emit() - Logger.log("i", "%s was set as 'deactive'.", plugin_id) - self._restart_required = True - self.restartRequiredChanged.emit() - - @pyqtProperty(bool, notify = metadataChanged) - def dataReady(self) -> bool: - return self._packages_model is not None - - @pyqtProperty(bool, notify = restartRequiredChanged) - def restartRequired(self) -> bool: - return self._restart_required - - @pyqtSlot() - def restart(self) -> None: - self._application.windowClosed() - - def getRemotePackage(self, package_id: str) -> Optional[Dict]: - # TODO: make the lookup in a dict, not a loop. canUpdate is called for every item. - remote_package = None - for package in self._server_response_data["packages"]: - if package["package_id"] == package_id: - remote_package = package - break - return remote_package - - @pyqtSlot(str, result = bool) - def canDowngrade(self, package_id: str) -> bool: - # If the currently installed version is higher than the bundled version (if present), the we can downgrade - # this package. - local_package = self._package_manager.getInstalledPackageInfo(package_id) - if local_package is None: - return False - - bundled_package = self._package_manager.getBundledPackageInfo(package_id) - if bundled_package is None: - return False - - local_version = Version(local_package["package_version"]) - bundled_version = Version(bundled_package["package_version"]) - return bundled_version < local_version - - @pyqtSlot(str, result = bool) - def isInstalled(self, package_id: str) -> bool: - result = self._package_manager.isPackageInstalled(package_id) - # Also check the old plugins list if it's not found in the package manager. - if not result: - result = self.isOldPlugin(package_id) - return result - - @pyqtSlot(str, result = int) - def getNumberOfInstalledPackagesByAuthor(self, author_id: str) -> int: - count = 0 - for package in self._materials_installed_model.items: - if package["author_id"] == author_id: - count += 1 - return count - - # This slot is only used to get the number of material packages by author, not any other type of packages. - @pyqtSlot(str, result = int) - def getTotalNumberOfMaterialPackagesByAuthor(self, author_id: str) -> int: - count = 0 - for package in self._server_response_data["packages"]: - if package["package_type"] == "material": - if package["author"]["author_id"] == author_id: - count += 1 - return count - - @pyqtSlot(str, result = bool) - def isEnabled(self, package_id: str) -> bool: - return package_id in self._plugin_registry.getActivePlugins() - - # Check for plugins that were installed with the old plugin browser - def isOldPlugin(self, plugin_id: str) -> bool: - return plugin_id in self._old_plugin_ids - - def getOldPluginPackageMetadata(self, plugin_id: str) -> Optional[Dict[str, Any]]: - return self._old_plugin_metadata.get(plugin_id) - - def isLoadingComplete(self) -> bool: - populated = 0 - for metadata_list in self._server_response_data.items(): - if metadata_list: - populated += 1 - return populated == len(self._server_response_data.items()) - - # Make API Calls - # -------------------------------------------------------------------------- - def _makeRequestByType(self, request_type: str) -> None: - Logger.debug(f"Requesting {request_type} metadata from server.") - url = self._request_urls[request_type] - - callback = lambda r, rt = request_type: self._onDataRequestFinished(rt, r) - error_callback = lambda r, e, rt = request_type: self._onDataRequestError(rt, r, e) - self._application.getHttpRequestManager().get(url, - callback = callback, - error_callback = error_callback, - scope=self._json_scope) - - @pyqtSlot(str) - def startDownload(self, url: str) -> None: - Logger.info(f"Attempting to download & install package from {url}.") - - callback = lambda r: self._onDownloadFinished(r) - error_callback = lambda r, e: self._onDownloadFailed(r, e) - download_progress_callback = self._onDownloadProgress - request_data = self._application.getHttpRequestManager().get(url, - callback = callback, - error_callback = error_callback, - download_progress_callback = download_progress_callback, - scope=self._cloud_scope - ) - - self._download_request_data = request_data - self.setDownloadProgress(0) - self.setIsDownloading(True) - - @pyqtSlot() - def cancelDownload(self) -> None: - Logger.info(f"User cancelled the download of a package. request {self._download_request_data}") - if self._download_request_data is not None: - self._application.getHttpRequestManager().abortRequest(self._download_request_data) - self._download_request_data = None - self.resetDownload() - - def resetDownload(self) -> None: - self.setDownloadProgress(0) - self.setIsDownloading(False) - - # Handlers for Network Events - # -------------------------------------------------------------------------- - def _onDataRequestError(self, request_type: str, reply: "QNetworkReply", error: "QNetworkReply.NetworkError") -> None: - Logger.error(f"Request {request_type} failed due to error {error}: {reply.errorString()}") - self.setViewPage("errored") - - def _onDataRequestFinished(self, request_type: str, reply: "QNetworkReply") -> None: - if reply.operation() != QNetworkAccessManager.GetOperation: - Logger.log("e", "_onDataRequestFinished() only handles GET requests but got [%s] instead", reply.operation()) - return - - http_status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) - if http_status_code != 200: - Logger.log("e", "Request type [%s] got non-200 HTTP response: [%s]", http_status_code) - self.setViewPage("errored") - return - - data = bytes(reply.readAll()) - try: - json_data = json.loads(data.decode("utf-8")) - except json.decoder.JSONDecodeError: - Logger.log("e", "Failed to decode response data as JSON for request type [%s], response data [%s]", - request_type, data) - self.setViewPage("errored") - return - - # Check for errors: - if "errors" in json_data: - for error in json_data["errors"]: - Logger.log("e", "Request type [%s] got response showing error: %s", error.get("title", "No error title found")) - self.setViewPage("errored") - return - - # Create model and apply metadata: - if not self._models[request_type]: - Logger.log("e", "Could not find the model for request type [%s].", request_type) - self.setViewPage("errored") - return - - self._server_response_data[request_type] = json_data["data"] - self._models[request_type].setMetadata(self._server_response_data[request_type]) - - if request_type == "packages": - self._models[request_type].setFilter({"type": "plugin"}) - self.reBuildMaterialsModels() - self.reBuildPluginsModels() - self._notifyPackageManager() - elif request_type == "authors": - self._models[request_type].setFilter({"package_types": "material"}) - self._models[request_type].setFilter({"tags": "generic"}) - elif request_type == "updates": - # Tell the package manager that there's a new set of updates available. - packages = set([pkg["package_id"] for pkg in self._server_response_data[request_type]]) - self._package_manager.setPackagesWithUpdate(packages) - - self.metadataChanged.emit() - - if self.isLoadingComplete(): - self.setViewPage("overview") - - # This function goes through all known remote versions of a package and notifies the package manager of this change - def _notifyPackageManager(self): - for package in self._server_response_data["packages"]: - self._package_manager.addAvailablePackageVersion(package["package_id"], Version(package["package_version"])) - - def _onDownloadFinished(self, reply: "QNetworkReply") -> None: - self.resetDownload() - - if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) != 200: - try: - reply_error = json.loads(reply.readAll().data().decode("utf-8")) - except Exception as e: - reply_error = str(e) - Logger.log("w", "Failed to download package. The following error was returned: %s", reply_error) - return - # Must not delete the temporary file on Windows - self._temp_plugin_file = tempfile.NamedTemporaryFile(mode = "w+b", suffix = ".curapackage", delete = False) - file_path = self._temp_plugin_file.name - # Write first and close, otherwise on Windows, it cannot read the file - self._temp_plugin_file.write(reply.readAll()) - self._temp_plugin_file.close() - self._onDownloadComplete(file_path) - - def _onDownloadFailed(self, reply: "QNetworkReply", error: "QNetworkReply.NetworkError") -> None: - Logger.log("w", "Failed to download package. The following error was returned: %s", error) - - self.resetDownload() - - def _onDownloadProgress(self, bytes_sent: int, bytes_total: int) -> None: - if bytes_total > 0: - new_progress = bytes_sent / bytes_total * 100 - self.setDownloadProgress(new_progress) - Logger.log("d", "new download progress %s / %s : %s%%", bytes_sent, bytes_total, new_progress) - - def _onDownloadComplete(self, file_path: str) -> None: - Logger.log("i", "Download complete.") - package_info = self._package_manager.getPackageInfo(file_path) - if not package_info: - Logger.log("w", "Package file [%s] was not a valid CuraPackage.", file_path) - return - package_id = package_info["package_id"] - - try: - license_content = self._package_manager.getPackageLicense(file_path) - except EnvironmentError as e: - Logger.error(f"Could not open downloaded package {package_id} to read license file! {type(e)} - {e}") - return - if license_content is not None: - # get the icon url for package_id, make sure the result is a string, never None - icon_url = next((x["icon_url"] for x in self.packagesModel.items if x["id"] == package_id), None) or "" - self.openLicenseDialog(package_info["display_name"], license_content, file_path, icon_url) - return - - installed_id = self.install(file_path) - if installed_id != package_id: - Logger.error("Installed package {} does not match {}".format(installed_id, package_id)) - - # Getter & Setters for Properties: - # -------------------------------------------------------------------------- - def setDownloadProgress(self, progress: float) -> None: - if progress != self._download_progress: - self._download_progress = progress - self.onDownloadProgressChanged.emit() - - @pyqtProperty(int, fset = setDownloadProgress, notify = onDownloadProgressChanged) - def downloadProgress(self) -> float: - return self._download_progress - - def setIsDownloading(self, is_downloading: bool) -> None: - if self._is_downloading != is_downloading: - self._is_downloading = is_downloading - self.onIsDownloadingChanged.emit() - - @pyqtProperty(bool, fset = setIsDownloading, notify = onIsDownloadingChanged) - def isDownloading(self) -> bool: - return self._is_downloading - - def setActivePackage(self, package: QObject) -> None: - if self._active_package != package: - self._active_package = package - self.activePackageChanged.emit() - - @pyqtProperty(QObject, fset = setActivePackage, notify = activePackageChanged) - def activePackage(self) -> Optional[QObject]: - """The active package is the package that is currently being downloaded""" - - return self._active_package - - def setViewCategory(self, category: str = "plugin") -> None: - if self._view_category != category: - self._view_category = category - self.viewChanged.emit() - - # Function explicitly defined so that it can be called through the callExtensionsMethod - # which cannot receive arguments. - def setViewCategoryToMaterials(self) -> None: - self.setViewCategory("material") - - @pyqtProperty(str, fset = setViewCategory, notify = viewChanged) - def viewCategory(self) -> str: - return self._view_category - - def setViewPage(self, page: str = "overview") -> None: - if self._view_page != page: - self._view_page = page - self.viewChanged.emit() - - @pyqtProperty(str, fset = setViewPage, notify = viewChanged) - def viewPage(self) -> str: - return self._view_page - - # Exposed Models: - # -------------------------------------------------------------------------- - @pyqtProperty(QObject, constant = True) - def authorsModel(self) -> AuthorsModel: - return cast(AuthorsModel, self._models["authors"]) - - @pyqtProperty(QObject, constant = True) - def packagesModel(self) -> PackagesModel: - return cast(PackagesModel, self._models["packages"]) - - @pyqtProperty(QObject, constant = True) - def pluginsShowcaseModel(self) -> PackagesModel: - return self._plugins_showcase_model - - @pyqtProperty(QObject, constant = True) - def pluginsAvailableModel(self) -> PackagesModel: - return self._plugins_available_model - - @pyqtProperty(QObject, constant = True) - def pluginsInstalledModel(self) -> PackagesModel: - return self._plugins_installed_model - - @pyqtProperty(QObject, constant = True) - def pluginsBundledModel(self) -> PackagesModel: - return self._plugins_bundled_model - - @pyqtProperty(QObject, constant = True) - def materialsShowcaseModel(self) -> AuthorsModel: - return self._materials_showcase_model - - @pyqtProperty(QObject, constant = True) - def materialsAvailableModel(self) -> AuthorsModel: - return self._materials_available_model - - @pyqtProperty(QObject, constant = True) - def materialsInstalledModel(self) -> PackagesModel: - return self._materials_installed_model - - @pyqtProperty(QObject, constant = True) - def materialsBundledModel(self) -> PackagesModel: - return self._materials_bundled_model - - @pyqtProperty(QObject, constant = True) - def materialsGenericModel(self) -> PackagesModel: - return self._materials_generic_model - - @pyqtSlot(str, result = str) - def getWebMarketplaceUrl(self, page: str) -> str: - root = CuraMarketplaceRoot - if root == "": - root = DEFAULT_MARKETPLACE_ROOT - return root + "/app/cura/" + page - - # Filter Models: - # -------------------------------------------------------------------------- - @pyqtSlot(str, str, str) - def filterModelByProp(self, model_type: str, filter_type: str, parameter: str) -> None: - if not self._models[model_type]: - Logger.log("w", "Couldn't filter %s model because it doesn't exist.", model_type) - return - self._models[model_type].setFilter({filter_type: parameter}) - self.filterChanged.emit() - - @pyqtSlot(str, "QVariantMap") - def setFilters(self, model_type: str, filter_dict: dict) -> None: - if not self._models[model_type]: - Logger.log("w", "Couldn't filter %s model because it doesn't exist.", model_type) - return - self._models[model_type].setFilter(filter_dict) - self.filterChanged.emit() - - @pyqtSlot(str) - def removeFilters(self, model_type: str) -> None: - if not self._models[model_type]: - Logger.log("w", "Couldn't remove filters on %s model because it doesn't exist.", model_type) - return - self._models[model_type].setFilter({}) - self.filterChanged.emit() - - # HACK(S): - # -------------------------------------------------------------------------- - def reBuildMaterialsModels(self) -> None: - materials_showcase_metadata = [] - materials_available_metadata = [] - materials_generic_metadata = [] - - processed_authors = [] # type: List[str] - - for item in self._server_response_data["packages"]: - if item["package_type"] == "material": - - author = item["author"] - if author["author_id"] in processed_authors: - continue - - # Generic materials to be in the same section - if "generic" in item["tags"]: - materials_generic_metadata.append(item) - else: - if "showcase" in item["tags"]: - materials_showcase_metadata.append(author) - else: - materials_available_metadata.append(author) - - processed_authors.append(author["author_id"]) - - self._materials_showcase_model.setMetadata(materials_showcase_metadata) - self._materials_available_model.setMetadata(materials_available_metadata) - self._materials_generic_model.setMetadata(materials_generic_metadata) - - def reBuildPluginsModels(self) -> None: - plugins_showcase_metadata = [] - plugins_available_metadata = [] - - for item in self._server_response_data["packages"]: - if item["package_type"] == "plugin": - if "showcase" in item["tags"]: - plugins_showcase_metadata.append(item) - else: - plugins_available_metadata.append(item) - - self._plugins_showcase_model.setMetadata(plugins_showcase_metadata) - self._plugins_available_model.setMetadata(plugins_available_metadata) diff --git a/plugins/Toolbox/src/__init__.py b/plugins/Toolbox/src/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 --- a/plugins/Toolbox/src/__init__.py +++ /dev/null diff --git a/plugins/UltimakerMachineActions/UMOUpgradeSelectionMachineAction.qml b/plugins/UltimakerMachineActions/UMOUpgradeSelectionMachineAction.qml index 565ba2fa0e..3d7b4a054f 100644 --- a/plugins/UltimakerMachineActions/UMOUpgradeSelectionMachineAction.qml +++ b/plugins/UltimakerMachineActions/UMOUpgradeSelectionMachineAction.qml @@ -4,7 +4,7 @@ import QtQuick 2.10 import QtQuick.Controls 2.3 -import UM 1.3 as UM +import UM 1.5 as UM import Cura 1.1 as Cura @@ -33,7 +33,7 @@ Cura.MachineAction renderType: Text.NativeRendering } - Cura.CheckBox + UM.CheckBox { anchors.top: pageDescription.bottom anchors.topMargin: UM.Theme.getSize("default_margin").height |