Merge branch 'marketplace_redesign'

This commit is contained in:
Ghostkeeper 2022-02-02 15:39:21 +01:00
commit 06bbeff21a
No known key found for this signature in database
GPG key ID: D2A8871EE34EC59A
102 changed files with 3056 additions and 4945 deletions

View file

@ -494,7 +494,7 @@ class CuraApplication(QtApplication):
"CuraEngineBackend", #Cura is useless without this one since you can't slice.
"FileLogger", #You want to be able to read the log if something goes wrong.
"XmlMaterialProfile", #Cura crashes without this one.
"Toolbox", #This contains the interface to enable/disable plug-ins, so if you disable it you can't enable it back.
"Marketplace", #This contains the interface to enable/disable plug-ins, so if you disable it you can't enable it back.
"PrepareStage", #Cura is useless without this one since you can't load models.
"PreviewStage", #This shows the list of the plugin views that are installed in Cura.
"MonitorStage", #Major part of Cura's functionality.
@ -573,6 +573,10 @@ class CuraApplication(QtApplication):
preferences.addPreference("general/accepted_user_agreement", False)
preferences.addPreference("cura/market_place_show_plugin_banner", True)
preferences.addPreference("cura/market_place_show_material_banner", True)
preferences.addPreference("cura/market_place_show_manage_packages_banner", True)
for key in [
"dialog_load_path", # dialog_save_path is in LocalFileOutputDevicePlugin
"dialog_profile_path",

View file

@ -1,13 +1,15 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import List, Tuple, TYPE_CHECKING, Optional
from typing import Any, cast, Dict, List, Set, Tuple, TYPE_CHECKING, Optional
from cura.CuraApplication import CuraApplication #To find some resource types.
from cura.CuraApplication import CuraApplication # To find some resource types.
from cura.Settings.GlobalStack import GlobalStack
from UM.PackageManager import PackageManager #The class we're extending.
from UM.Resources import Resources #To find storage paths for some resource types.
from UM.PackageManager import PackageManager # The class we're extending.
from UM.Resources import Resources # To find storage paths for some resource types.
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
if TYPE_CHECKING:
from UM.Qt.QtApplication import QtApplication
@ -17,6 +19,31 @@ if TYPE_CHECKING:
class CuraPackageManager(PackageManager):
def __init__(self, application: "QtApplication", parent: Optional["QObject"] = None) -> None:
super().__init__(application, parent)
self._local_packages: Optional[List[Dict[str, Any]]] = None
self._local_packages_ids: Optional[Set[str]] = None
self.installedPackagesChanged.connect(self._updateLocalPackages)
def _updateLocalPackages(self) -> None:
self._local_packages = self.getAllLocalPackages()
self._local_packages_ids = set(pkg["package_id"] for pkg in self._local_packages)
@property
def local_packages(self) -> List[Dict[str, Any]]:
"""locally installed packages, lazy execution"""
if self._local_packages is None:
self._updateLocalPackages()
# _updateLocalPackages always results in a list of packages, not None.
# It's guaranteed to be a list now.
return cast(List[Dict[str, Any]], self._local_packages)
@property
def local_packages_ids(self) -> Set[str]:
"""locally installed packages, lazy execution"""
if self._local_packages_ids is None:
self._updateLocalPackages()
# _updateLocalPackages always results in a list of packages, not None.
# It's guaranteed to be a list now.
return cast(Set[str], self._local_packages_ids)
def initialize(self) -> None:
self._installation_dirs_dict["materials"] = Resources.getStoragePath(CuraApplication.ResourceTypes.MaterialInstanceContainer)
@ -47,3 +74,12 @@ class CuraPackageManager(PackageManager):
machine_with_qualities.append((global_stack, str(extruder_nr), container_id))
return machine_with_materials, machine_with_qualities
def getAllLocalPackages(self) -> List[Dict[str, Any]]:
""" Returns an unordered list of all the package_info of installed, to be installed, or bundled packages"""
packages: List[Dict[str, Any]] = []
for packages_to_add in self.getAllInstalledPackagesInfo().values():
packages.extend(packages_to_add)
return packages

View file

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

View file

@ -1,4 +1,4 @@
// Copyright (C) 2021 Ultimaker B.V.
// Copyright (C) 2022 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.10
@ -9,7 +9,7 @@ import QtQuick.Controls.Styles 1.4
import QtQuick.Layouts 1.1
import UM 1.2 as UM
import Cura 1.6 as Cura
import Cura 1.7 as Cura
import DigitalFactory 1.0 as DF
@ -44,33 +44,14 @@ Item
height: childrenRect.height
spacing: UM.Theme.getSize("default_margin").width
Cura.TextField
Cura.SearchBar
{
id: searchBar
Layout.fillWidth: true
implicitHeight: createNewProjectButton.height
leftPadding: searchIcon.width + UM.Theme.getSize("default_margin").width * 2
focus: true
onTextEdited: manager.projectFilter = text //Update the search filter when editing this text field.
placeholderText: "Search"
UM.RecolorImage
{
id: searchIcon
anchors
{
verticalCenter: parent.verticalCenter
left: parent.left
leftMargin: UM.Theme.getSize("default_margin").width
}
source: UM.Theme.getIcon("search")
height: UM.Theme.getSize("small_button_icon").height
width: height
color: UM.Theme.getColor("text")
}
}
Cura.SecondaryButton
@ -222,7 +203,7 @@ Item
LoadMoreProjectsCard
{
id: loadMoreProjectsCard
height: UM.Theme.getSize("toolbox_thumbnail_small").height
height: UM.Theme.getSize("card_icon").height
width: parent.width
visible: manager.digitalFactoryProjectModel.count > 0
hasMoreProjectsToLoad: manager.hasMoreProjectsToLoad

View file

@ -0,0 +1,12 @@
# Copyright (c) 2021 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from cura.UltimakerCloud import UltimakerCloudConstants
from cura.ApplicationMetadata import CuraSDKVersion
ROOT_URL = f"{UltimakerCloudConstants.CuraCloudAPIRoot}/cura-packages/v{UltimakerCloudConstants.CuraCloudAPIVersion}"
ROOT_CURA_URL = f"{ROOT_URL}/cura/v{CuraSDKVersion}" # Root of all Marketplace API requests.
ROOT_USER_URL = f"{ROOT_URL}/user"
PACKAGES_URL = f"{ROOT_CURA_URL}/packages" # URL to use for requesting the list of packages.
PACKAGE_UPDATES_URL = f"{PACKAGES_URL}/package-updates" # URL to use for requesting the list of packages that can be updated.
USER_PACKAGES_URL = f"{ROOT_USER_URL}/packages"

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

Before

Width:  |  Height:  |  Size: 184 B

After

Width:  |  Height:  |  Size: 184 B

Before After
Before After

View file

@ -0,0 +1,91 @@
//Copyright (c) 2021 Ultimaker B.V.
//Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.10
import QtQuick.Dialogs 1.1
import QtQuick.Window 2.2
import QtQuick.Controls 2.3
import QtQuick.Layouts 1.3
import UM 1.6 as UM
import Cura 1.6 as Cura
UM.Dialog
{
id: licenseDialog
title: catalog.i18nc("@button", "Plugin license agreement")
minimumWidth: UM.Theme.getSize("license_window_minimum").width
minimumHeight: UM.Theme.getSize("license_window_minimum").height
width: minimumWidth
height: minimumHeight
backgroundColor: UM.Theme.getColor("main_background")
property variant catalog: UM.I18nCatalog { name: "cura" }
ColumnLayout
{
anchors.fill: parent
spacing: UM.Theme.getSize("thick_margin").height
Row
{
Layout.fillWidth: true
height: childrenRect.height
spacing: UM.Theme.getSize("default_margin").width
leftPadding: UM.Theme.getSize("narrow_margin").width
UM.RecolorImage
{
id: icon
width: UM.Theme.getSize("marketplace_large_icon").width
height: UM.Theme.getSize("marketplace_large_icon").height
color: UM.Theme.getColor("text")
source: UM.Theme.getIcon("Certificate", "high")
}
Label
{
text: catalog.i18nc("@text", "Please read and agree with the plugin licence.")
color: UM.Theme.getColor("text")
font: UM.Theme.getFont("large")
anchors.verticalCenter: icon.verticalCenter
height: UM.Theme.getSize("marketplace_large_icon").height
verticalAlignment: Qt.AlignVCenter
wrapMode: Text.Wrap
renderType: Text.NativeRendering
}
}
Cura.ScrollableTextArea
{
Layout.fillWidth: true
Layout.fillHeight: true
anchors.topMargin: UM.Theme.getSize("default_margin").height
textArea.text: licenseContent
textArea.readOnly: true
}
}
rightButtons:
[
Cura.PrimaryButton
{
text: catalog.i18nc("@button", "Accept")
onClicked: handler.onLicenseAccepted(packageId)
}
]
leftButtons:
[
Cura.SecondaryButton
{
text: catalog.i18nc("@button", "Decline")
onClicked: handler.onLicenseDeclined(packageId)
}
]
onAccepted: handler.onLicenseAccepted(packageId)
onRejected: handler.onLicenseDeclined(packageId)
onClosing: handler.onLicenseDeclined(packageId)
}

View file

@ -0,0 +1,114 @@
// Copyright (c) 2021 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.1
import UM 1.6 as UM
import Cura 1.6 as Cura
Item
{
id: manageButton
property bool button_style: true
property string text
property bool busy: false
property bool confirmed: false
implicitWidth: childrenRect.width
implicitHeight: childrenRect.height
signal clicked()
property Component primaryButton: Component
{
Cura.PrimaryButton
{
text: manageButton.text
onClicked: manageButton.clicked()
}
}
property Component secondaryButton: Component
{
Cura.SecondaryButton
{
text: manageButton.text
onClicked: manageButton.clicked()
}
}
property Component busyButton: Component
{
Item
{
height: UM.Theme.getSize("action_button").height
width: childrenRect.width
UM.RecolorImage
{
id: busyIndicator
visible: parent.visible
height: UM.Theme.getSize("action_button").height - 2 * UM.Theme.getSize("narrow_margin").height
width: height
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
source: UM.Theme.getIcon("Spinner")
color: UM.Theme.getColor("primary")
RotationAnimator
{
target: busyIndicator
running: parent.visible
from: 0
to: 360
loops: Animation.Infinite
duration: 2500
}
}
Label
{
visible: parent.visible
anchors.left: busyIndicator.right
anchors.leftMargin: UM.Theme.getSize("narrow_margin").width
anchors.verticalCenter: parent.verticalCenter
text: manageButton.text
font: UM.Theme.getFont("medium_bold")
color: UM.Theme.getColor("primary")
}
}
}
property Component confirmButton: Component
{
Item
{
height: UM.Theme.getSize("action_button").height
width: childrenRect.width
Label
{
anchors.verticalCenter: parent.verticalCenter
text: manageButton.text
font: UM.Theme.getFont("medium_bold")
color: UM.Theme.getColor("primary")
}
}
}
Loader
{
sourceComponent:
{
if (busy) { return manageButton.busyButton; }
else if (confirmed) { return manageButton.confirmButton; }
else if (manageButton.button_style) { return manageButton.primaryButton; }
else { return manageButton.secondaryButton; }
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,295 @@
// Copyright (c) 2021 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.1
import UM 1.6 as UM
import Cura 1.6 as Cura
Rectangle
{
id: root
property alias packageData: packageCardHeader.packageData
height: childrenRect.height
color: UM.Theme.getColor("main_background")
radius: UM.Theme.getSize("default_radius").width
Column
{
width: parent.width
spacing: 0
Item
{
width: parent.width
height: UM.Theme.getSize("card").height
PackageCardHeader
{
id: packageCardHeader
showManageButtons: true
anchors.fill: parent
Row
{
id: downloadCount
Layout.preferredWidth: parent.width
Layout.fillHeight: true
UM.RecolorImage
{
id: downloadsIcon
width: UM.Theme.getSize("card_tiny_icon").width
height: UM.Theme.getSize("card_tiny_icon").height
source: UM.Theme.getIcon("Download")
color: UM.Theme.getColor("text")
}
Label
{
anchors.verticalCenter: downloadsIcon.verticalCenter
color: UM.Theme.getColor("text")
font: UM.Theme.getFont("default")
text: packageData.downloadCount
}
}
}
}
Column
{
id: extendedDescription
width: parent.width
padding: UM.Theme.getSize("default_margin").width
topPadding: 0
spacing: UM.Theme.getSize("default_margin").height
Label
{
width: parent.width - parent.padding * 2
text: catalog.i18nc("@header", "Description")
font: UM.Theme.getFont("medium_bold")
color: UM.Theme.getColor("text")
elide: Text.ElideRight
}
Label
{
width: parent.width - parent.padding * 2
text: packageData.formattedDescription
font: UM.Theme.getFont("medium")
color: UM.Theme.getColor("text")
linkColor: UM.Theme.getColor("text_link")
wrapMode: Text.Wrap
textFormat: Text.RichText
onLinkActivated: UM.UrlUtil.openUrl(link, ["http", "https"])
}
Column //Separate column to have no spacing between compatible printers.
{
id: compatiblePrinterColumn
width: parent.width - parent.padding * 2
visible: packageData.packageType === "material"
spacing: 0
Label
{
width: parent.width
text: catalog.i18nc("@header", "Compatible printers")
font: UM.Theme.getFont("medium_bold")
color: UM.Theme.getColor("text")
elide: Text.ElideRight
}
Repeater
{
model: packageData.compatiblePrinters
Label
{
width: compatiblePrinterColumn.width
text: modelData
font: UM.Theme.getFont("medium")
color: UM.Theme.getColor("text")
elide: Text.ElideRight
}
}
Label
{
width: parent.width
visible: packageData.compatiblePrinters.length == 0
text: "(" + catalog.i18nc("@info", "No compatibility information") + ")"
font: UM.Theme.getFont("medium")
color: UM.Theme.getColor("text")
elide: Text.ElideRight
}
}
Column
{
id: compatibleSupportMaterialColumn
width: parent.width - parent.padding * 2
visible: packageData.packageType === "material"
spacing: 0
Label
{
width: parent.width
text: catalog.i18nc("@header", "Compatible support materials")
font: UM.Theme.getFont("medium_bold")
color: UM.Theme.getColor("text")
elide: Text.ElideRight
}
Repeater
{
model: packageData.compatibleSupportMaterials
Label
{
width: compatibleSupportMaterialColumn.width
text: modelData
font: UM.Theme.getFont("medium")
color: UM.Theme.getColor("text")
elide: Text.ElideRight
}
}
Label
{
width: parent.width
visible: packageData.compatibleSupportMaterials.length == 0
text: "(" + catalog.i18nc("@info No materials", "None") + ")"
font: UM.Theme.getFont("medium")
color: UM.Theme.getColor("text")
elide: Text.ElideRight
}
}
Column
{
width: parent.width - parent.padding * 2
visible: packageData.packageType === "material"
spacing: 0
Label
{
width: parent.width
text: catalog.i18nc("@header", "Compatible with Material Station")
font: UM.Theme.getFont("medium_bold")
color: UM.Theme.getColor("text")
elide: Text.ElideRight
}
Label
{
width: parent.width
text: packageData.isCompatibleMaterialStation ? catalog.i18nc("@info", "Yes") : catalog.i18nc("@info", "No")
font: UM.Theme.getFont("medium")
color: UM.Theme.getColor("text")
elide: Text.ElideRight
}
}
Column
{
width: parent.width - parent.padding * 2
visible: packageData.packageType === "material"
spacing: 0
Label
{
width: parent.width
text: catalog.i18nc("@header", "Optimized for Air Manager")
font: UM.Theme.getFont("medium_bold")
color: UM.Theme.getColor("text")
elide: Text.ElideRight
}
Label
{
width: parent.width
text: packageData.isCompatibleAirManager ? catalog.i18nc("@info", "Yes") : catalog.i18nc("@info", "No")
font: UM.Theme.getFont("medium")
color: UM.Theme.getColor("text")
elide: Text.ElideRight
}
}
Row
{
id: externalButtonRow
anchors.horizontalCenter: parent.horizontalCenter
spacing: UM.Theme.getSize("narrow_margin").width
Cura.SecondaryButton
{
text: packageData.packageType === "plugin" ? catalog.i18nc("@button", "Visit plug-in website") : catalog.i18nc("@button", "Website")
iconSource: UM.Theme.getIcon("Globe")
outlineColor: "transparent"
onClicked: Qt.openUrlExternally(packageData.packageInfoUrl)
}
Cura.SecondaryButton
{
visible: packageData.packageType === "material"
text: catalog.i18nc("@button", "Buy spool")
iconSource: UM.Theme.getIcon("ShoppingCart")
outlineColor: "transparent"
onClicked: Qt.openUrlExternally(packageData.whereToBuy)
}
Cura.SecondaryButton
{
visible: packageData.packageType === "material"
text: catalog.i18nc("@button", "Safety datasheet")
iconSource: UM.Theme.getIcon("Warning")
outlineColor: "transparent"
onClicked: Qt.openUrlExternally(packageData.safetyDataSheet)
}
Cura.SecondaryButton
{
visible: packageData.packageType === "material"
text: catalog.i18nc("@button", "Technical datasheet")
iconSource: UM.Theme.getIcon("DocumentFilled")
outlineColor: "transparent"
onClicked: Qt.openUrlExternally(packageData.technicalDataSheet)
}
}
}
}
FontMetrics
{
id: fontMetrics
font: UM.Theme.getFont("default")
}
}

View file

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

View file

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

View file

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

View file

@ -0,0 +1,45 @@
// Copyright (c) 2021 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.1
import UM 1.6 as UM
import Cura 1.6 as Cura
Control
{
implicitWidth: UM.Theme.getSize("card_tiny_icon").width
implicitHeight: UM.Theme.getSize("card_tiny_icon").height
Cura.ToolTip
{
tooltipText:
{
switch(packageData.packageType)
{
case "plugin": return catalog.i18nc("@info", "Ultimaker Verified Plug-in");
case "material": return catalog.i18nc("@info", "Ultimaker Certified Material");
default: return catalog.i18nc("@info", "Ultimaker Verified Package");
}
}
visible: parent.hovered
targetPoint: Qt.point(0, Math.round(parent.y + parent.height / 4))
}
Rectangle
{
anchors.fill: parent
color: UM.Theme.getColor("action_button_hovered")
radius: width
UM.RecolorImage
{
anchors.fill: parent
color: UM.Theme.getColor("primary")
source: packageData.packageType == "plugin" ? UM.Theme.getIcon("CheckCircle") : UM.Theme.getIcon("Certified")
}
}
//NOTE: Can we link to something here? (Probably a static link explaining what verified is):
// onClicked: Qt.openUrlExternally( XXXXXX )
}

View file

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

View file

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

View file

@ -1,7 +0,0 @@
{
"name": "Toolbox",
"author": "Ultimaker B.V.",
"version": "1.0.1",
"api": 7,
"description": "Find, manage and install new Cura packages."
}

View file

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

Before

Width:  |  Height:  |  Size: 458 B

View file

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

Before

Width:  |  Height:  |  Size: 667 B

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -526,13 +526,13 @@
}
}
},
"Toolbox": {
"Marketplace": {
"package_info": {
"package_id": "Toolbox",
"package_id": "Marketplace",
"package_type": "plugin",
"display_name": "Toolbox",
"display_name": "Marketplace",
"description": "Find, manage and install new Cura packages.",
"package_version": "1.0.1",
"package_version": "1.0.0",
"sdk_version": "7.9.0",
"website": "https://ultimaker.com",
"author": {

View file

@ -15,6 +15,7 @@ Button
property bool isIconOnRightSide: false
property alias iconSource: buttonIconLeft.source
property real iconSize: UM.Theme.getSize("action_button_icon").height
property alias textFont: buttonText.font
property alias cornerRadius: backgroundRect.radius
property alias tooltip: tooltip.tooltipText
@ -109,7 +110,7 @@ Button
{
id: buttonIconLeft
source: ""
height: visible ? UM.Theme.getSize("action_button_icon").height : 0
height: visible ? button.iconSize : 0
width: visible ? height : 0
sourceSize.width: width
sourceSize.height: height
@ -158,7 +159,7 @@ Button
{
id: buttonIconRight
source: buttonIconLeft.source
height: visible ? UM.Theme.getSize("action_button_icon").height : 0
height: visible ? button.iconSize : 0
width: visible ? height : 0
sourceSize.width: width
sourceSize.height: height

View file

@ -72,6 +72,7 @@ Item
property alias configureSettingVisibility: configureSettingVisibilityAction
property alias browsePackages: browsePackagesAction
property alias openMarketplace: openMarketplaceAction
UM.I18nCatalog{id: catalog; name: "cura"}
@ -483,4 +484,11 @@ Item
text: "&Marketplace"
iconName: "plugins_browse"
}
Action
{
id: openMarketplaceAction
text: catalog.i18nc("@action:menu", "&Marketplace")
iconName: "plugins_browse"
}
}

View file

@ -196,13 +196,13 @@ Item
}
}
// show the Toolbox
// show the Marketplace
Connections
{
target: Cura.Actions.browsePackages
target: Cura.Actions.openMarketplace
function onTriggered()
{
curaExtensions.callExtensionMethod("Toolbox", "launch")
curaExtensions.callExtensionMethod("Marketplace", "show")
}
}
@ -212,8 +212,8 @@ Item
target: Cura.Actions.marketplaceMaterials
function onTriggered()
{
curaExtensions.callExtensionMethod("Toolbox", "launch")
curaExtensions.callExtensionMethod("Toolbox", "setViewCategoryToMaterials")
curaExtensions.callExtensionMethod("Marketplace", "show")
curaExtensions.callExtensionMethod("Marketplace", "setVisibleTabToMaterials")
}
}
}
}

View file

@ -83,19 +83,31 @@ Item
ExclusiveGroup { id: mainWindowHeaderMenuGroup }
}
// Shortcut button to quick access the Toolbox
Controls2.Button
{
id: marketplaceButton
text: catalog.i18nc("@action:button", "Marketplace")
height: Math.round(0.5 * UM.Theme.getSize("main_window_header").height)
onClicked: Cura.Actions.browsePackages.trigger()
anchors
{
verticalCenter: parent.verticalCenter
right: applicationSwitcher.left
rightMargin: UM.Theme.getSize("default_margin").width
}
hoverEnabled: true
onClicked: Cura.Actions.openMarketplace.trigger()
contentItem: Label
{
text: "Marketplace" //Ultimaker considers this a product name, so it shouldn't be translated.
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("primary_text")
width: contentWidth
verticalAlignment: Text.AlignVCenter
}
background: Rectangle
{
id: marketplaceButtonBorder
radius: UM.Theme.getSize("action_button_radius").width
color: UM.Theme.getColor("main_window_header_background")
border.width: UM.Theme.getSize("default_lining").width
@ -103,7 +115,6 @@ Item
Rectangle
{
id: marketplaceButtonFill
anchors.fill: parent
radius: parent.radius
color: UM.Theme.getColor("primary_text")
@ -112,33 +123,12 @@ Item
}
}
contentItem: Label
{
id: label
text: marketplaceButton.text
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("primary_text")
width: contentWidth
verticalAlignment: Text.AlignVCenter
renderType: Text.NativeRendering
}
anchors
{
right: applicationSwitcher.left
rightMargin: UM.Theme.getSize("default_margin").width
verticalCenter: parent.verticalCenter
}
Cura.NotificationIcon
{
id: marketplaceNotificationIcon
anchors
{
top: parent.top
right: parent.right
rightMargin: (-0.5 * width) | 0
topMargin: (-0.5 * height) | 0
horizontalCenter: parent.right
verticalCenter: parent.top
}
visible: CuraApplication.getPackageManager().packagesWithUpdate.length > 0

View file

@ -0,0 +1,35 @@
// Copyright (C) 2021 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.1
import UM 1.6 as UM
import Cura 1.7 as Cura
Cura.TextField
{
UM.I18nCatalog { id: catalog; name: "cura" }
leftPadding: searchIcon.width + UM.Theme.getSize("default_margin").width * 2
placeholderText: catalog.i18nc("@placeholder", "Search")
font.italic: true
UM.RecolorImage
{
id: searchIcon
anchors
{
verticalCenter: parent.verticalCenter
left: parent.left
leftMargin: UM.Theme.getSize("default_margin").width
}
source: UM.Theme.getIcon("Magnifier")
height: UM.Theme.getSize("small_button_icon").height
width: height
color: UM.Theme.getColor("text")
}
}

View file

@ -41,39 +41,19 @@ Item
repeat: false
}
Cura.TextField
Cura.SearchBar
{
id: filter
height: parent.height
anchors.left: parent.left
anchors.right: parent.right
leftPadding: searchIcon.width + UM.Theme.getSize("default_margin").width * 2
placeholderText: catalog.i18nc("@label:textbox", "Search settings")
font.italic: true
placeholderText: catalog.i18nc("@label:textbox", "Search settings") // Overwrite
property var expandedCategories
property bool lastFindingSettings: false
UM.RecolorImage
{
id: searchIcon
anchors
{
verticalCenter: parent.verticalCenter
left: parent.left
leftMargin: UM.Theme.getSize("default_margin").width
}
source: UM.Theme.getIcon("search")
height: UM.Theme.getSize("small_button_icon").height
width: height
color: UM.Theme.getColor("text")
}
onTextChanged:
{
settingsSearchTimer.restart()
}
onTextChanged: settingsSearchTimer.restart()
onEditingFinished:
{
@ -86,10 +66,7 @@ Item
}
}
Keys.onEscapePressed:
{
filter.text = ""
}
Keys.onEscapePressed: filter.text = ""
function updateDefinitionModel()
{

View file

@ -16,4 +16,5 @@ Cura.ActionButton
textDisabledColor: UM.Theme.getColor("action_button_disabled_text")
hoverColor: "transparent"
underlineTextOnHover: true
iconSize: UM.Theme.getSize("action_button_icon_small").height
}

View file

@ -38,7 +38,7 @@ ToolTip
onAboutToHide: hide()
// If the text is not set, just set the height to 0 to prevent it from showing
height: text != "" ? label.contentHeight + 2 * UM.Theme.getSize("thin_margin").width: 0
height: label.contentHeight + 2 * UM.Theme.getSize("thin_margin").width
x:
{
@ -74,7 +74,7 @@ ToolTip
}
function show() {
opacity = 1
opacity = text != "" ? 1 : 0
}
function hide() {

View file

@ -1,5 +1,5 @@
// Copyright (c) 2020 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.10
import QtQuick.Controls 2.3

View file

@ -6,6 +6,7 @@
"colors": {
"main_background": [39, 44, 48, 255],
"detail_background": [63, 63, 63, 255],
"message_background": [39, 44, 48, 255],
"wide_lining": [31, 36, 39, 255],
"thick_lining": [255, 255, 255, 60],
@ -175,10 +176,6 @@
"quality_slider_available": [255, 255, 255, 255],
"toolbox_header_button_text_active": [255, 255, 255, 255],
"toolbox_header_button_text_inactive": [128, 128, 128, 255],
"toolbox_premium_packages_background": [57, 57, 57, 255],
"monitor_printer_family_tag": [86, 86, 106, 255],
"monitor_text_disabled": [102, 102, 102, 255],
"monitor_icon_primary": [229, 229, 229, 255],

View file

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M12 21L4.293 13.293L5.707 11.8789L11 17.1719V3H13V17.1719L18.293 11.8789L19.707 13.293L12 21Z" />
</svg>

After

Width:  |  Height:  |  Size: 198 B

View file

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M10.707 4.293L12.1211 5.707L6.8281 11H21V13H6.8281L12.1211 18.293L10.707 19.707L3 12L10.707 4.293Z" />
</svg>

After

Width:  |  Height:  |  Size: 203 B

View file

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M12.0005 22.0122H12C11.07 21.9324 10.1927 21.5466 9.50539 20.915C9.21375 20.6772 8.89653 20.4726 8.55959 20.305C8.20295 20.1856 7.83416 20.1061 7.45999 20.0679C6.5274 20.0285 5.63416 19.6811 4.91999 19.0801C4.33502 18.3806 3.9948 17.509 3.95119 16.5981C3.90897 16.2037 3.82297 15.8153 3.69479 15.44C3.52022 15.0837 3.3065 14.748 3.05759 14.4391C2.44454 13.7645 2.06886 12.9079 1.98779 12C2.06757 11.07 2.45344 10.1927 3.08499 9.5054C3.32281 9.21376 3.5274 8.89653 3.69499 8.5596C3.81442 8.20295 3.89394 7.83417 3.93209 7.46C3.97149 6.52741 4.31886 5.63417 4.91989 4.92C5.61927 4.33475 6.49098 3.99449 7.40189 3.9512C7.79626 3.90907 8.1847 3.82308 8.55999 3.6948C8.91626 3.52044 9.25197 3.30689 9.56089 3.0581C10.2354 2.44481 11.092 2.06894 12 1.9878H12.001C12.9306 2.0678 13.8076 2.45365 14.4946 3.085C14.7862 3.32281 15.1035 3.52741 15.4404 3.695C15.7969 3.81446 16.1656 3.89405 16.5396 3.9323C17.4722 3.97169 18.3654 4.31907 19.0796 4.9201C19.6646 5.61964 20.0048 6.49125 20.0484 7.4021C20.0908 7.7964 20.1769 8.18476 20.3052 8.56C20.4798 8.91626 20.6935 9.25196 20.9424 9.5609C21.5557 10.2357 21.9315 11.0926 22.0124 12.0009C21.9325 12.9307 21.5466 13.8078 20.9152 14.495C20.6774 14.7866 20.4728 15.1039 20.3052 15.4408C20.1857 15.7973 20.1061 16.166 20.0679 16.54C20.0285 17.4726 19.6811 18.3658 19.0801 19.08C18.3807 19.6653 17.509 20.0055 16.5981 20.0488C16.2038 20.0912 15.8155 20.1775 15.4404 20.3061C15.0841 20.4805 14.7484 20.694 14.4395 20.9428C13.7649 21.5557 12.9084 21.9313 12.0005 22.0122ZM12 3.9878C11.5315 4.1067 11.1003 4.34102 10.7456 4.6694C10.309 5.01634 9.83223 5.30948 9.32559 5.5425C8.80239 5.73609 8.25776 5.86593 7.70349 5.9292C7.22072 5.94798 6.7503 6.08713 6.33499 6.334C6.07267 6.76174 5.9264 7.25048 5.91059 7.752C5.8537 8.28949 5.73005 8.81779 5.54249 9.3247C5.31656 9.81621 5.03019 10.2776 4.68999 10.6982C4.34665 11.0638 4.10465 11.5127 3.98789 12.0005C4.10683 12.4686 4.34097 12.8994 4.66899 13.2539C5.01645 13.6907 5.30978 14.168 5.54259 14.6753C5.73618 15.1983 5.86602 15.7428 5.92929 16.2969C5.94811 16.7796 6.08744 17.2499 6.33459 17.665C6.76211 17.9274 7.25071 18.0737 7.75209 18.0894C8.28958 18.1463 8.81789 18.2699 9.32479 18.4575C9.81631 18.6835 10.2777 18.9699 10.6983 19.3101C11.0639 19.6534 11.5128 19.8954 12.0006 20.0122C12.4691 19.8933 12.9003 19.659 13.255 19.3306C13.6916 18.9837 14.1684 18.6905 14.675 18.4575C15.1982 18.2639 15.7428 18.1341 16.2971 18.0708C16.7797 18.052 17.25 17.9128 17.6652 17.666C17.9275 17.2383 18.0738 16.7495 18.0896 16.248C18.1465 15.7105 18.2701 15.1822 18.4577 14.6753C18.6837 14.1838 18.9701 13.7224 19.3103 13.3018C19.6535 12.9363 19.8954 12.4876 20.0122 12C19.8933 11.5319 19.6591 11.1011 19.3311 10.7466C18.9836 10.3098 18.6903 9.83249 18.4575 9.3252C18.2639 8.80217 18.1341 8.25771 18.0708 7.7036C18.052 7.22089 17.9126 6.75057 17.6655 6.3355C17.238 6.0731 16.7494 5.92681 16.248 5.9111C15.7105 5.85421 15.1822 5.73055 14.6753 5.543C14.1838 5.31692 13.7224 5.03037 13.3018 4.69C12.9363 4.3467 12.4876 4.10467 12 3.9878ZM15.5361 15.5361L14.1221 14.1211C13.7026 14.5407 13.168 14.8265 12.5861 14.9423C12.0041 15.0581 11.4009 14.9987 10.8526 14.7717C10.3044 14.5447 9.83586 14.1601 9.50619 13.6668C9.17652 13.1734 9.00055 12.5934 9.00055 12C9.00055 11.4066 9.17652 10.8266 9.50619 10.3332C9.83586 9.83986 10.3044 9.45535 10.8526 9.2283C11.4009 9.00126 12.0041 8.94188 12.5861 9.05769C13.168 9.1735 13.7026 9.45928 14.1221 9.8789L15.5361 8.4639C14.8368 7.7648 13.9458 7.28875 12.976 7.09596C12.0061 6.90316 11.0009 7.00227 10.0873 7.38076C9.1738 7.75924 8.393 8.4001 7.84367 9.22232C7.29433 10.0445 7.00113 11.0112 7.00113 12C7.00113 12.9888 7.29433 13.9555 7.84367 14.7777C8.393 15.5999 9.1738 16.2408 10.0873 16.6192C11.0009 16.9977 12.0061 17.0968 12.976 16.904C13.9458 16.7112 14.8368 16.2352 15.5361 15.5361Z" />
</svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

View file

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M14 2H7C6.20435 2 5.44129 2.31607 4.87868 2.87868C4.31607 3.44129 4 4.20435 4 5V19C4 19.7956 4.31607 20.5587 4.87868 21.1213C5.44129 21.6839 6.20435 22 7 22H17C17.7956 22 18.5587 21.6839 19.1213 21.1213C19.6839 20.5587 20 19.7956 20 19V8L14 2ZM14 4.8284L17.1716 8H14V4.8284ZM18 20H6V4H12V10H18V20ZM16 14H8V12H16V14ZM13 18H8V16H13V18Z" />
</svg>

After

Width:  |  Height:  |  Size: 438 B

View file

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18.707 10.293L12 17L5.293 10.293L6.707 8.8789L11 13.1719V3H13V13.1719L17.293 8.8789L18.707 10.293ZM21 19H3V21H21V19Z" fill="#000E1A"/>
</svg>

After

Width:  |  Height:  |  Size: 248 B

View file

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M12 2C10.0222 2 8.08879 2.58649 6.4443 3.6853C4.79981 4.78412 3.51809 6.3459 2.76121 8.17317C2.00433 10.0004 1.8063 12.0111 2.19215 13.9509C2.578 15.8907 3.53041 17.6725 4.92894 19.0711C6.32746 20.4696 8.10929 21.422 10.0491 21.8079C11.9889 22.1937 13.9996 21.9957 15.8268 21.2388C17.6541 20.4819 19.2159 19.2002 20.3147 17.5557C21.4135 15.9112 22 13.9778 22 12C22 10.6868 21.7413 9.38642 21.2388 8.17317C20.7363 6.95991 19.9997 5.85752 19.0711 4.92893C18.1425 4.00035 17.0401 3.26375 15.8268 2.7612C14.6136 2.25866 13.3132 2 12 2ZM19.9309 11H16.9594C16.8101 8.655 16.0204 6.39588 14.6763 4.4685C16.0708 4.96434 17.3003 5.83742 18.228 6.99057C19.1557 8.14372 19.7452 9.5317 19.9309 11ZM9.06 13H14.94C14.7622 15.4921 13.7228 17.8448 12 19.6543C10.2772 17.8448 9.2378 15.4921 9.06 13ZM9.06 11C9.23776 8.50785 10.2772 6.15522 12 4.3457C13.7226 6.15537 14.762 8.50792 14.94 11H9.06ZM9.3241 4.4685C7.9799 6.39584 7.19008 8.65496 7.0406 11H4.0691C4.25479 9.5317 4.8443 8.14372 5.77201 6.99057C6.69972 5.83742 7.92924 4.96434 9.3237 4.4685H9.3241ZM4.0691 13H7.0406C7.18996 15.345 7.97964 17.6041 9.3237 19.5315C7.92924 19.0357 6.69972 18.1626 5.77201 17.0094C4.8443 15.8563 4.25479 14.4683 4.0691 13ZM14.6763 19.5315C16.0204 17.6041 16.8101 15.345 16.9594 13H19.9309C19.7452 14.4683 19.1557 15.8563 18.228 17.0094C17.3003 18.1626 16.0708 19.0357 14.6763 19.5315Z" />
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19 12C19 11.895 18.9888 11.7928 18.9843 11.6889L21.2354 9.9966L18.3525 5.0034L15.7525 6.1005C15.5837 5.9927 15.4112 5.8905 15.2325 5.7972L14.8828 3H9.1172L8.7672 5.7972C8.5855 5.8923 8.4096 5.9964 8.2372 6.1066L5.6472 5.0034L2.7646 9.9966L5.0152 11.7C5.0109 11.8 5 11.8988 5 12C5 12.105 5.0112 12.2072 5.0157 12.3111L2.7646 14.0034L5.6475 18.9966L8.2475 17.8994C8.4163 18.0072 8.5888 18.1094 8.7675 18.2028L9.1172 21H14.8828L15.2328 18.2028C15.4145 18.1077 15.5904 18.0036 15.7628 17.8934L18.3528 18.9966L21.2357 14.0034L18.9848 12.3C18.9891 12.2 19 12.1012 19 12ZM18.62 14.5327L17.5028 16.4673L15.4513 15.6018C14.8702 16.1531 14.1648 16.5564 13.3949 16.7773L13.1172 19H10.8828L10.605 16.7773C9.83505 16.5564 9.12968 16.1531 8.5486 15.6018L6.4971 16.4673L5.38 14.5327L7.1579 13.1865C6.94752 12.4095 6.94752 11.5905 7.1579 10.8135L5.38 9.4673L6.4971 7.5327L8.5486 8.3982C9.12968 7.84686 9.83505 7.44364 10.605 7.2227L10.8828 5H13.1172L13.395 7.2227C14.1649 7.44364 14.8703 7.84686 15.4514 8.3982L17.5029 7.5327L18.62 9.4673L16.8421 10.8135C17.0525 11.5905 17.0525 12.4095 16.8421 13.1865L18.62 14.5327ZM12 9C11.4067 9 10.8266 9.17595 10.3333 9.50559C9.83994 9.83524 9.45542 10.3038 9.22836 10.8519C9.0013 11.4001 8.94189 12.0033 9.05764 12.5853C9.1734 13.1672 9.45912 13.7018 9.87868 14.1213C10.2982 14.5409 10.8328 14.8266 11.4147 14.9424C11.9967 15.0581 12.5999 14.9987 13.148 14.7716C13.6962 14.5446 14.1648 14.1601 14.4944 13.6667C14.8241 13.1734 15 12.5933 15 12C14.9991 11.2046 14.6828 10.4421 14.1204 9.87964C13.5579 9.31722 12.7954 9.00087 12 9ZM12 13C11.8022 13 11.6089 12.9414 11.4444 12.8315C11.28 12.7216 11.1518 12.5654 11.0761 12.3827C11.0004 12.2 10.9806 11.9989 11.0192 11.8049C11.0578 11.6109 11.153 11.4327 11.2929 11.2929C11.4327 11.153 11.6109 11.0578 11.8049 11.0192C11.9989 10.9806 12.2 11.0004 12.3827 11.0761C12.5654 11.1518 12.7216 11.28 12.8315 11.4444C12.9413 11.6089 13 11.8022 13 12C12.9998 12.2651 12.8943 12.5194 12.7068 12.7068C12.5194 12.8943 12.2651 12.9998 12 13Z" fill="#000E1A"/>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View file

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M5.99631 13C5.99631 13.7956 6.31238 14.5587 6.87499 15.1213C7.4376 15.6839 8.20066 16 8.99631 16H17.9963C18.792 16 19.555 15.6839 20.1176 15.1213C20.6802 14.5587 20.9963 13.7956 20.9963 13V7H7.99631V4H1.99631V6H5.99631V13ZM18.9963 9V14H7.99631V9H18.9963ZM21.9963 19C21.9963 19.3956 21.879 19.7822 21.6592 20.1111C21.4395 20.44 21.1271 20.6964 20.7617 20.8478C20.3962 20.9991 19.9941 21.0387 19.6061 20.9616C19.2182 20.8844 18.8618 20.6939 18.5821 20.4142C18.3024 20.1345 18.1119 19.7781 18.0347 19.3902C17.9576 19.0022 17.9972 18.6001 18.1485 18.2346C18.2999 17.8692 18.5563 17.5568 18.8852 17.3371C19.2141 17.1173 19.6007 17 19.9963 17C20.5266 17.0006 21.0349 17.2115 21.4098 17.5865C21.7848 17.9614 21.9957 18.4698 21.9963 19ZM9.99631 19C9.99631 19.3956 9.87901 19.7822 9.65925 20.1111C9.43948 20.44 9.12713 20.6964 8.76167 20.8478C8.39622 20.9991 7.99409 21.0387 7.60613 20.9616C7.21816 20.8844 6.8618 20.6939 6.58209 20.4142C6.30239 20.1345 6.11191 19.7781 6.03474 19.3902C5.95757 19.0022 5.99717 18.6001 6.14855 18.2346C6.29992 17.8692 6.55627 17.5568 6.88517 17.3371C7.21407 17.1173 7.60074 17 7.99631 17C8.52655 17.0006 9.03491 17.2115 9.40985 17.5865C9.78479 17.9614 9.9957 18.4698 9.99631 19Z" />
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

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

After

Width:  |  Height:  |  Size: 703 B

View file

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

After

Width:  |  Height:  |  Size: 959 B

View file

@ -0,0 +1,3 @@
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M31.6058 21.5314C31.4745 21.131 31.3116 20.7417 31.1185 20.3671C30.3501 18.8691 29.131 17.6501 27.6331 16.8816C26.5097 16.3023 25.2641 16.0001 24.0001 16.0001C22.7361 16.0001 21.4905 16.3023 20.3671 16.8816C18.8691 17.6501 17.65 18.8692 16.8815 20.3672C16.3021 21.4906 15.9998 22.7362 15.9998 24.0002C15.9998 25.2642 16.3021 26.5098 16.8815 27.6332C17.65 29.1311 18.869 30.3502 20.367 31.1186C21.4904 31.698 22.736 32.0003 24 32.0003C25.264 32.0003 26.5096 31.698 27.633 31.1186C29.1309 30.3502 30.3499 29.1312 31.1183 27.6333C31.5999 26.6993 31.8911 25.6789 31.9747 24.6314C32.0584 23.5839 31.933 22.5302 31.6057 21.5316L31.6058 21.5314ZM28.3781 28.09C28.2852 28.1894 28.189 28.2855 28.0897 28.3784C27.0333 29.3689 25.6529 29.9417 24.2056 29.99C24.1367 29.9923 24.0694 30 24 30C23.9306 30 23.8633 29.9919 23.7944 29.99C22.3469 29.9416 20.9664 29.3688 19.91 28.3781C19.8106 28.2852 19.7145 28.189 19.6216 28.0897C18.6312 27.0335 18.0585 25.6533 18.01 24.2062C18.0081 24.1371 18 24.07 18 24C18 23.93 18.0081 23.8629 18.01 23.7938C18.0585 22.3466 18.6314 20.9663 19.6219 19.91C19.7148 19.8106 19.811 19.7145 19.9103 19.6216C20.9665 18.6312 22.3467 18.0585 23.7938 18.01C23.8629 18.0077 23.9304 18 24 18C24.0696 18 24.1371 18.0081 24.2062 18.01C25.6534 18.0585 27.0337 18.6314 28.09 19.6219C28.1894 19.7148 28.2855 19.811 28.3784 19.9103C29.3689 20.9667 29.9417 22.3471 29.99 23.7944C29.9923 23.8633 30 23.9306 30 24C30 24.0694 29.9919 24.1367 29.99 24.2056C29.9416 25.6531 29.3688 27.0336 28.3781 28.09ZM42 27.8467V20.1528L37.193 19.3516C37.0257 18.8773 36.8327 18.4124 36.6149 17.9591L39.4482 13.9927L34.0082 8.5518L30.0413 11.3851C29.5879 11.1673 29.1229 10.9742 28.6485 10.8069L27.8467 6H20.1528L19.3516 10.807C18.8774 10.9743 18.4126 11.1673 17.9593 11.385L13.9927 8.5518L8.5518 13.9927L11.385 17.9593C11.1673 18.4126 10.9743 18.8774 10.807 19.3516L6 20.1528V27.8467L10.8069 28.6481C10.9742 29.1225 11.1673 29.5875 11.3851 30.0409L8.5518 34.0078L13.9927 39.4478L17.9591 36.6145C18.4124 36.8323 18.8773 37.0253 19.3516 37.1926L20.1528 42H27.8467L28.6481 37.1931C29.1226 37.0257 29.5876 36.8327 30.0411 36.6149L34.0078 39.4482L39.4478 34.0082L36.6145 30.0415C36.8323 29.588 37.0253 29.123 37.1927 28.6485L42 27.8467ZM35.6375 26.88C35.5326 27.305 35.4043 27.7238 35.2532 28.1346C35.1409 28.4401 35.0184 28.7399 34.8824 29.0331C34.6981 29.4301 34.4921 29.8166 34.2654 30.191L35.52 31.947L36.8372 33.791L33.791 36.8369L31.947 35.52L30.1912 34.2657C29.8168 34.4924 29.4303 34.6984 29.0333 34.8827C28.74 35.0186 28.4403 35.1412 28.1348 35.2535C27.724 35.4046 27.3052 35.5329 26.8802 35.6378L26.5264 37.7609L26.1533 40H21.8472L21.474 37.7607L21.12 35.6376C20.695 35.5326 20.2761 35.4043 19.8652 35.2532C19.5594 35.1408 19.2597 35.0182 18.9662 34.8823C18.5692 34.6978 18.1825 34.4917 17.808 34.265L16.052 35.5191L14.2083 36.8359L11.1641 33.791L12.4808 31.9476L13.735 30.1918C13.5081 29.8173 13.302 29.4305 13.1176 29.0333C12.9816 28.7399 12.8591 28.4402 12.7467 28.1345C12.5956 27.7238 12.4674 27.3051 12.3625 26.8802L10.2394 26.5264L8 26.1533V21.8472L10.2393 21.474L12.3624 21.1202C12.4673 20.6953 12.5956 20.2764 12.7466 19.8656C12.8591 19.5598 12.9817 19.2599 13.1177 18.9664C13.3021 18.5693 13.5082 18.1825 13.735 17.808L12.481 16.0522L11.1641 14.2085L14.2085 11.1641L16.0522 12.481L17.808 13.735C18.1825 13.5082 18.5693 13.3021 18.9664 13.1177C19.2599 12.9817 19.5598 12.8591 19.8656 12.7466C20.2764 12.5956 20.6953 12.4673 21.1202 12.3624L21.474 10.2393L21.8472 8H26.1533L26.5264 10.2394L26.8802 12.3625C27.3051 12.4673 27.7238 12.5955 28.1345 12.7466C28.4401 12.859 28.7399 12.9816 29.0333 13.1176C29.4305 13.302 29.8173 13.5081 30.1918 13.735L31.9476 12.4808L33.791 11.1641L36.8359 14.2085L35.5191 16.0522L34.265 17.8082C34.4918 18.1827 34.6979 18.5693 34.8823 18.9664C35.0183 19.2599 35.1409 19.5597 35.2533 19.8654C35.4044 20.2763 35.5327 20.6952 35.6376 21.1202L37.7607 21.474L40 21.8472V26.1533L37.7606 26.5264L35.6375 26.88Z" fill="#000E1A"/>
</svg>

After

Width:  |  Height:  |  Size: 4 KiB

View file

@ -602,62 +602,6 @@ QtObject
}
}
property Component toolbox_action_button: Component
{
ButtonStyle
{
background: Rectangle
{
implicitWidth: UM.Theme.getSize("toolbox_action_button").width
implicitHeight: UM.Theme.getSize("toolbox_action_button").height
color:
{
if (control.installed)
{
return UM.Theme.getColor("action_button_disabled");
}
else
{
if (control.hovered)
{
return UM.Theme.getColor("primary_hover");
}
else
{
return UM.Theme.getColor("primary");
}
}
}
}
label: Label
{
text: control.text
color:
{
if (control.installed)
{
return UM.Theme.getColor("action_button_disabled_text");
}
else
{
if (control.hovered)
{
return UM.Theme.getColor("button_text_hover");
}
else
{
return UM.Theme.getColor("button_text");
}
}
}
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
font: UM.Theme.getFont("default_bold")
}
}
}
property Component monitor_button_style: Component
{
ButtonStyle

Some files were not shown because too many files have changed in this diff Show more