Merge pull request #3 from Ultimaker/master

update
This commit is contained in:
MaukCC 2020-02-17 15:27:39 +01:00 committed by GitHub
commit 3c6f31b81f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
272 changed files with 76023 additions and 68799 deletions

View file

@ -23,6 +23,7 @@ set(CURA_BUILDTYPE "" CACHE STRING "Build type of Cura, eg. 'PPA'")
set(CURA_CLOUD_API_ROOT "" CACHE STRING "Alternative Cura cloud API root") set(CURA_CLOUD_API_ROOT "" CACHE STRING "Alternative Cura cloud API root")
set(CURA_CLOUD_API_VERSION "" CACHE STRING "Alternative Cura cloud API version") set(CURA_CLOUD_API_VERSION "" CACHE STRING "Alternative Cura cloud API version")
set(CURA_CLOUD_ACCOUNT_API_ROOT "" CACHE STRING "Alternative Cura cloud account API version") set(CURA_CLOUD_ACCOUNT_API_ROOT "" CACHE STRING "Alternative Cura cloud account API version")
set(CURA_MARKETPLACE_ROOT "" CACHE STRING "Alternative Marketplace location")
configure_file(${CMAKE_SOURCE_DIR}/cura.desktop.in ${CMAKE_BINARY_DIR}/cura.desktop @ONLY) configure_file(${CMAKE_SOURCE_DIR}/cura.desktop.in ${CMAKE_BINARY_DIR}/cura.desktop @ONLY)

View file

@ -56,6 +56,13 @@ function(cura_add_test)
endif() endif()
endfunction() endfunction()
#Add test for import statements which are not compatible with all builds
add_test(
NAME "invalid-imports"
COMMAND ${Python3_EXECUTABLE} scripts/check_invalid_imports.py
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
cura_add_test(NAME pytest-main DIRECTORY ${CMAKE_SOURCE_DIR}/tests PYTHONPATH "${CMAKE_SOURCE_DIR}|${URANIUM_DIR}") cura_add_test(NAME pytest-main DIRECTORY ${CMAKE_SOURCE_DIR}/tests PYTHONPATH "${CMAKE_SOURCE_DIR}|${URANIUM_DIR}")
file(GLOB_RECURSE _plugins plugins/*/__init__.py) file(GLOB_RECURSE _plugins plugins/*/__init__.py)

View file

@ -28,11 +28,12 @@ class CuraAPI(QObject):
# The main reason for this is that we want to prevent consumers of API to have a dependency on CuraApplication. # The main reason for this is that we want to prevent consumers of API to have a dependency on CuraApplication.
# Since the API is intended to be used by plugins, the cura application should have already created this. # Since the API is intended to be used by plugins, the cura application should have already created this.
def __new__(cls, application: Optional["CuraApplication"] = None): def __new__(cls, application: Optional["CuraApplication"] = None):
if cls.__instance is None: if cls.__instance is not None:
if application is None: raise RuntimeError("Tried to create singleton '{class_name}' more than once.".format(class_name = CuraAPI.__name__))
raise Exception("Upon first time creation, the application must be set.") if application is None:
cls.__instance = super(CuraAPI, cls).__new__(cls) raise RuntimeError("Upon first time creation, the application must be set.")
cls._application = application cls.__instance = super(CuraAPI, cls).__new__(cls)
cls._application = application
return cls.__instance return cls.__instance
def __init__(self, application: Optional["CuraApplication"] = None) -> None: def __init__(self, application: Optional["CuraApplication"] = None) -> None:

View file

@ -1,4 +1,4 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2020 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
# --------- # ---------
@ -13,7 +13,7 @@ DEFAULT_CURA_DEBUG_MODE = False
# Each release has a fixed SDK version coupled with it. It doesn't make sense to make it configurable because, for # Each release has a fixed SDK version coupled with it. It doesn't make sense to make it configurable because, for
# example Cura 3.2 with SDK version 6.1 will not work. So the SDK version is hard-coded here and left out of the # example Cura 3.2 with SDK version 6.1 will not work. So the SDK version is hard-coded here and left out of the
# CuraVersion.py.in template. # CuraVersion.py.in template.
CuraSDKVersion = "7.0.0" CuraSDKVersion = "7.1.0"
try: try:
from cura.CuraVersion import CuraAppName # type: ignore from cura.CuraVersion import CuraAppName # type: ignore

View file

@ -145,6 +145,14 @@ class Backup:
# \return Whether we had success or not. # \return Whether we had success or not.
@staticmethod @staticmethod
def _extractArchive(archive: "ZipFile", target_path: str) -> bool: def _extractArchive(archive: "ZipFile", target_path: str) -> bool:
# Implement security recommendations: Sanity check on zip files will make it harder to spoof.
from cura.CuraApplication import CuraApplication
config_filename = CuraApplication.getInstance().getApplicationName() + ".cfg" # Should be there if valid.
if config_filename not in [file.filename for file in archive.filelist]:
Logger.logException("e", "Unable to extract the backup due to corruption of compressed file(s).")
return False
Logger.log("d", "Removing current data in location: %s", target_path) Logger.log("d", "Removing current data in location: %s", target_path)
Resources.factoryReset() Resources.factoryReset()
Logger.log("d", "Extracting backup to location: %s", target_path) Logger.log("d", "Extracting backup to location: %s", target_path)

View file

@ -1,4 +1,4 @@
# Copyright (c) 2019 Ultimaker B.V. # Copyright (c) 2020 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
import os import os
@ -191,8 +191,6 @@ class CuraApplication(QtApplication):
self._cura_formula_functions = None # type: Optional[CuraFormulaFunctions] self._cura_formula_functions = None # type: Optional[CuraFormulaFunctions]
self._cura_package_manager = None
self._machine_action_manager = None # type: Optional[MachineActionManager.MachineActionManager] self._machine_action_manager = None # type: Optional[MachineActionManager.MachineActionManager]
self.empty_container = None # type: EmptyInstanceContainer self.empty_container = None # type: EmptyInstanceContainer
@ -350,6 +348,9 @@ class CuraApplication(QtApplication):
for dir_name in ["extruders", "machine_instances", "materials", "plugins", "quality", "quality_changes", "user", "variants", "intent"]: for dir_name in ["extruders", "machine_instances", "materials", "plugins", "quality", "quality_changes", "user", "variants", "intent"]:
Resources.addExpectedDirNameInData(dir_name) Resources.addExpectedDirNameInData(dir_name)
app_root = os.path.abspath(os.path.join(os.path.dirname(sys.executable)))
Resources.addSearchPath(os.path.join(app_root, "share", "cura", "resources"))
Resources.addSearchPath(os.path.join(self._app_install_dir, "share", "cura", "resources")) Resources.addSearchPath(os.path.join(self._app_install_dir, "share", "cura", "resources"))
if not hasattr(sys, "frozen"): if not hasattr(sys, "frozen"):
resource_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), "..", "resources") resource_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), "..", "resources")
@ -393,6 +394,8 @@ class CuraApplication(QtApplication):
SettingFunction.registerOperator("extruderValues", self._cura_formula_functions.getValuesInAllExtruders) SettingFunction.registerOperator("extruderValues", self._cura_formula_functions.getValuesInAllExtruders)
SettingFunction.registerOperator("resolveOrValue", self._cura_formula_functions.getResolveOrValue) SettingFunction.registerOperator("resolveOrValue", self._cura_formula_functions.getResolveOrValue)
SettingFunction.registerOperator("defaultExtruderPosition", self._cura_formula_functions.getDefaultExtruderPosition) SettingFunction.registerOperator("defaultExtruderPosition", self._cura_formula_functions.getDefaultExtruderPosition)
SettingFunction.registerOperator("valueFromContainer", self._cura_formula_functions.getValueFromContainerAtIndex)
SettingFunction.registerOperator("extruderValueFromContainer", self._cura_formula_functions.getValueFromContainerAtIndexInExtruder)
# Adds all resources and container related resources. # Adds all resources and container related resources.
def __addAllResourcesAndContainerResources(self) -> None: def __addAllResourcesAndContainerResources(self) -> None:
@ -632,6 +635,12 @@ class CuraApplication(QtApplication):
def showPreferences(self) -> None: def showPreferences(self) -> None:
self.showPreferencesWindow.emit() self.showPreferencesWindow.emit()
# This is called by drag-and-dropping curapackage files.
@pyqtSlot(QUrl)
def installPackageViaDragAndDrop(self, file_url: str) -> Optional[str]:
filename = QUrl(file_url).toLocalFile()
return self._package_manager.installPackage(filename)
@override(Application) @override(Application)
def getGlobalContainerStack(self) -> Optional["GlobalStack"]: def getGlobalContainerStack(self) -> Optional["GlobalStack"]:
return self._global_container_stack return self._global_container_stack
@ -1827,15 +1836,21 @@ class CuraApplication(QtApplication):
def _onContextMenuRequested(self, x: float, y: float) -> None: def _onContextMenuRequested(self, x: float, y: float) -> None:
# Ensure we select the object if we request a context menu over an object without having a selection. # Ensure we select the object if we request a context menu over an object without having a selection.
if not Selection.hasSelection(): if Selection.hasSelection():
node = self.getController().getScene().findObject(cast(SelectionPass, self.getRenderer().getRenderPass("selection")).getIdAtPosition(x, y)) return
if node: selection_pass = cast(SelectionPass, self.getRenderer().getRenderPass("selection"))
parent = node.getParent() if not selection_pass: # If you right-click before the rendering has been initialised there might not be a selection pass yet.
while(parent and parent.callDecoration("isGroup")): print("--------------ding! Got the crash.")
node = parent return
parent = node.getParent() node = self.getController().getScene().findObject(selection_pass.getIdAtPosition(x, y))
if not node:
return
parent = node.getParent()
while parent and parent.callDecoration("isGroup"):
node = parent
parent = node.getParent()
Selection.add(node) Selection.add(node)
@pyqtSlot() @pyqtSlot()
def showMoreInformationDialogForAnonymousDataCollection(self): def showMoreInformationDialogForAnonymousDataCollection(self):

View file

@ -1,4 +1,4 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2020 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
CuraAppName = "@CURA_APP_NAME@" CuraAppName = "@CURA_APP_NAME@"
@ -9,3 +9,4 @@ CuraDebugMode = True if "@_cura_debugmode@" == "ON" else False
CuraCloudAPIRoot = "@CURA_CLOUD_API_ROOT@" CuraCloudAPIRoot = "@CURA_CLOUD_API_ROOT@"
CuraCloudAPIVersion = "@CURA_CLOUD_API_VERSION@" CuraCloudAPIVersion = "@CURA_CLOUD_API_VERSION@"
CuraCloudAccountAPIRoot = "@CURA_CLOUD_ACCOUNT_API_ROOT@" CuraCloudAccountAPIRoot = "@CURA_CLOUD_ACCOUNT_API_ROOT@"
CuraMarketplaceRoot = "@CURA_MARKETPLACE_ROOT@"

View file

@ -115,9 +115,10 @@ class AuthorizationHelpers:
) )
@staticmethod @staticmethod
## Generate a 16-character verification code. ## Generate a verification code of arbitrary length.
# \param code_length: How long should the code be? # \param code_length: How long should the code be? This should never be lower than 16, but it's probably better to
def generateVerificationCode(code_length: int = 16) -> str: # leave it at 32
def generateVerificationCode(code_length: int = 32) -> str:
return "".join(random.choice("0123456789ABCDEF") for i in range(code_length)) return "".join(random.choice("0123456789ABCDEF") for i in range(code_length))
@staticmethod @staticmethod

View file

@ -25,6 +25,8 @@ class AuthorizationRequestHandler(BaseHTTPRequestHandler):
self.authorization_callback = None # type: Optional[Callable[[AuthenticationResponse], None]] self.authorization_callback = None # type: Optional[Callable[[AuthenticationResponse], None]]
self.verification_code = None # type: Optional[str] self.verification_code = None # type: Optional[str]
self.state = None # type: Optional[str]
# CURA-6609: Some browser seems to issue a HEAD instead of GET request as the callback. # CURA-6609: Some browser seems to issue a HEAD instead of GET request as the callback.
def do_HEAD(self) -> None: def do_HEAD(self) -> None:
self.do_GET() self.do_GET()
@ -58,7 +60,14 @@ class AuthorizationRequestHandler(BaseHTTPRequestHandler):
# \return HTTP ResponseData containing a success page to show to the user. # \return HTTP ResponseData containing a success page to show to the user.
def _handleCallback(self, query: Dict[Any, List]) -> Tuple[ResponseData, Optional[AuthenticationResponse]]: def _handleCallback(self, query: Dict[Any, List]) -> Tuple[ResponseData, Optional[AuthenticationResponse]]:
code = self._queryGet(query, "code") code = self._queryGet(query, "code")
if code and self.authorization_helpers is not None and self.verification_code is not None: state = self._queryGet(query, "state")
if state != self.state:
token_response = AuthenticationResponse(
success = False,
err_message=catalog.i18nc("@message",
"The provided state is not correct.")
)
elif code and self.authorization_helpers is not None and self.verification_code is not None:
# If the code was returned we get the access token. # If the code was returned we get the access token.
token_response = self.authorization_helpers.getAccessTokenUsingAuthorizationCode( token_response = self.authorization_helpers.getAccessTokenUsingAuthorizationCode(
code, self.verification_code) code, self.verification_code)

View file

@ -25,3 +25,6 @@ class AuthorizationRequestServer(HTTPServer):
## Set the verification code on the request handler. ## Set the verification code on the request handler.
def setVerificationCode(self, verification_code: str) -> None: def setVerificationCode(self, verification_code: str) -> None:
self.RequestHandlerClass.verification_code = verification_code # type: ignore self.RequestHandlerClass.verification_code = verification_code # type: ignore
def setState(self, state: str) -> None:
self.RequestHandlerClass.state = state # type: ignore

View file

@ -153,13 +153,15 @@ class AuthorizationService:
verification_code = self._auth_helpers.generateVerificationCode() verification_code = self._auth_helpers.generateVerificationCode()
challenge_code = self._auth_helpers.generateVerificationCodeChallenge(verification_code) challenge_code = self._auth_helpers.generateVerificationCodeChallenge(verification_code)
state = AuthorizationHelpers.generateVerificationCode()
# Create the query string needed for the OAuth2 flow. # Create the query string needed for the OAuth2 flow.
query_string = urlencode({ query_string = urlencode({
"client_id": self._settings.CLIENT_ID, "client_id": self._settings.CLIENT_ID,
"redirect_uri": self._settings.CALLBACK_URL, "redirect_uri": self._settings.CALLBACK_URL,
"scope": self._settings.CLIENT_SCOPES, "scope": self._settings.CLIENT_SCOPES,
"response_type": "code", "response_type": "code",
"state": "(.Y.)", "state": state, # Forever in our Hearts, RIP "(.Y.)" (2018-2020)
"code_challenge": challenge_code, "code_challenge": challenge_code,
"code_challenge_method": "S512" "code_challenge_method": "S512"
}) })
@ -168,7 +170,7 @@ class AuthorizationService:
QDesktopServices.openUrl(QUrl("{}?{}".format(self._auth_url, query_string))) QDesktopServices.openUrl(QUrl("{}?{}".format(self._auth_url, query_string)))
# Start a local web server to receive the callback URL on. # Start a local web server to receive the callback URL on.
self._server.start(verification_code) self._server.start(verification_code, state)
## Callback method for the authentication flow. ## Callback method for the authentication flow.
def _onAuthStateChanged(self, auth_response: AuthenticationResponse) -> None: def _onAuthStateChanged(self, auth_response: AuthenticationResponse) -> None:

View file

@ -36,7 +36,8 @@ class LocalAuthorizationServer:
## Starts the local web server to handle the authorization callback. ## Starts the local web server to handle the authorization callback.
# \param verification_code The verification code part of the OAuth2 client identification. # \param verification_code The verification code part of the OAuth2 client identification.
def start(self, verification_code: str) -> None: # \param state The unique state code (to ensure that the request we get back is really from the server.
def start(self, verification_code: str, state: str) -> None:
if self._web_server: if self._web_server:
# If the server is already running (because of a previously aborted auth flow), we don't have to start it. # If the server is already running (because of a previously aborted auth flow), we don't have to start it.
# We still inject the new verification code though. # We still inject the new verification code though.
@ -53,6 +54,7 @@ class LocalAuthorizationServer:
self._web_server.setAuthorizationHelpers(self._auth_helpers) self._web_server.setAuthorizationHelpers(self._auth_helpers)
self._web_server.setAuthorizationCallback(self._auth_state_changed_callback) self._web_server.setAuthorizationCallback(self._auth_state_changed_callback)
self._web_server.setVerificationCode(verification_code) self._web_server.setVerificationCode(verification_code)
self._web_server.setState(state)
# Start the server on a new thread. # Start the server on a new thread.
self._web_server_thread = threading.Thread(None, self._web_server.serve_forever, daemon = self._daemon) self._web_server_thread = threading.Thread(None, self._web_server.serve_forever, daemon = self._daemon)

View file

@ -133,6 +133,38 @@ class CuraFormulaFunctions:
context = self.createContextForDefaultValueEvaluation(global_stack) context = self.createContextForDefaultValueEvaluation(global_stack)
return self.getResolveOrValue(property_key, context = context) return self.getResolveOrValue(property_key, context = context)
# Gets the value for the given setting key starting from the given container index.
def getValueFromContainerAtIndex(self, property_key: str, container_index: int,
context: Optional["PropertyEvaluationContext"] = None) -> Any:
machine_manager = self._application.getMachineManager()
global_stack = machine_manager.activeMachine
context = self.createContextForDefaultValueEvaluation(global_stack)
context.context["evaluate_from_container_index"] = container_index
return global_stack.getProperty(property_key, "value", context = context)
# Gets the extruder value for the given setting key starting from the given container index.
def getValueFromContainerAtIndexInExtruder(self, extruder_position: int, property_key: str, container_index: int,
context: Optional["PropertyEvaluationContext"] = None) -> Any:
machine_manager = self._application.getMachineManager()
global_stack = machine_manager.activeMachine
if extruder_position == -1:
extruder_position = int(machine_manager.defaultExtruderPosition)
global_stack = machine_manager.activeMachine
try:
extruder_stack = global_stack.extruderList[int(extruder_position)]
except IndexError:
Logger.log("w", "Value for %s of extruder %s was requested, but that extruder is not available. " % (property_key, extruder_position))
return None
context = self.createContextForDefaultValueEvaluation(extruder_stack)
context.context["evaluate_from_container_index"] = container_index
return self.getValueInExtruder(extruder_position, property_key, context)
# Creates a context for evaluating default values (skip the user_changes container). # Creates a context for evaluating default values (skip the user_changes container).
def createContextForDefaultValueEvaluation(self, source_stack: "CuraContainerStack") -> "PropertyEvaluationContext": def createContextForDefaultValueEvaluation(self, source_stack: "CuraContainerStack") -> "PropertyEvaluationContext":
context = PropertyEvaluationContext(source_stack) context = PropertyEvaluationContext(source_stack)

View file

@ -747,6 +747,11 @@ class MachineManager(QObject):
result = [] # type: List[str] result = [] # type: List[str]
for setting_instance in container.findInstances(): for setting_instance in container.findInstances():
setting_key = setting_instance.definition.key setting_key = setting_instance.definition.key
if setting_key == "print_sequence":
old_value = container.getProperty(setting_key, "value")
Logger.log("d", "Reset setting [%s] in [%s] because its old value [%s] is no longer valid", setting_key, container, old_value)
result.append(setting_key)
continue
if not self._global_container_stack.getProperty(setting_key, "type") in ("extruder", "optional_extruder"): if not self._global_container_stack.getProperty(setting_key, "type") in ("extruder", "optional_extruder"):
continue continue
@ -795,7 +800,7 @@ class MachineManager(QObject):
definition_changes_container.setProperty("machine_extruder_count", "value", extruder_count) definition_changes_container.setProperty("machine_extruder_count", "value", extruder_count)
self.updateDefaultExtruder() self.updateDefaultExtruder()
self.updateNumberExtrudersEnabled() self.numberExtrudersEnabledChanged.emit()
self.correctExtruderSettings() self.correctExtruderSettings()
# Check to see if any objects are set to print with an extruder that will no longer exist # Check to see if any objects are set to print with an extruder that will no longer exist

View file

@ -29,9 +29,13 @@ parser.add_argument("--debug",
known_args = vars(parser.parse_known_args()[0]) known_args = vars(parser.parse_known_args()[0])
if with_sentry_sdk: if with_sentry_sdk:
sentry_env = "production" sentry_env = "unknown" # Start off with a "IDK"
if ApplicationMetadata.CuraVersion == "master": if hasattr(sys, "frozen"):
sentry_env = "production" # A frozen build is a "real" distribution.
elif ApplicationMetadata.CuraVersion == "master":
sentry_env = "development" sentry_env = "development"
elif "beta" in ApplicationMetadata.CuraVersion or "BETA" in ApplicationMetadata.CuraVersion:
sentry_env = "beta"
try: try:
if ApplicationMetadata.CuraVersion.split(".")[2] == "99": if ApplicationMetadata.CuraVersion.split(".")[2] == "99":
sentry_env = "nightly" sentry_env = "nightly"
@ -167,6 +171,7 @@ else:
# tries to create PyQt objects on a non-main thread. # tries to create PyQt objects on a non-main thread.
import Arcus #@UnusedImport import Arcus #@UnusedImport
import Savitar #@UnusedImport import Savitar #@UnusedImport
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication

View file

@ -13,6 +13,8 @@ export PKG_CONFIG_PATH="${CURA_BUILD_ENV_PATH}/lib/pkgconfig:${PKG_CONFIG_PATH}"
cd "${PROJECT_DIR}" cd "${PROJECT_DIR}"
# #
# Clone Uranium and set PYTHONPATH first # Clone Uranium and set PYTHONPATH first
# #

View file

@ -4,7 +4,8 @@
from configparser import ConfigParser from configparser import ConfigParser
import zipfile import zipfile
import os import os
from typing import cast, Dict, List, Optional, Tuple import json
from typing import cast, Dict, List, Optional, Tuple, Any
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
@ -732,7 +733,25 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
base_file_name = os.path.basename(file_name) base_file_name = os.path.basename(file_name)
self.setWorkspaceName(base_file_name) self.setWorkspaceName(base_file_name)
return nodes
return nodes, self._loadMetadata(file_name)
@staticmethod
def _loadMetadata(file_name: str) -> Dict[str, Dict[str, Any]]:
archive = zipfile.ZipFile(file_name, "r")
metadata_files = [name for name in archive.namelist() if name.endswith("plugin_metadata.json")]
result = dict()
for metadata_file in metadata_files:
try:
plugin_id = metadata_file.split("/")[0]
result[plugin_id] = json.loads(archive.open("%s/plugin_metadata.json" % plugin_id).read().decode("utf-8"))
except Exception:
Logger.logException("w", "Unable to retrieve metadata for %s", metadata_file)
return result
def _processQualityChanges(self, global_stack): def _processQualityChanges(self, global_stack):
if self._machine_info.quality_changes_info is None: if self._machine_info.quality_changes_info is None:

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.1", "version": "1.0.1",
"description": "Provides support for reading 3MF files.", "description": "Provides support for reading 3MF files.",
"api": "7.0", "api": "7.1",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -73,11 +73,25 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
version_config_parser.write(version_file_string) version_config_parser.write(version_file_string)
archive.writestr(version_file, version_file_string.getvalue()) archive.writestr(version_file, version_file_string.getvalue())
self._writePluginMetadataToArchive(archive)
# Close the archive & reset states. # Close the archive & reset states.
archive.close() archive.close()
mesh_writer.setStoreArchive(False) mesh_writer.setStoreArchive(False)
return True return True
@staticmethod
def _writePluginMetadataToArchive(archive: zipfile.ZipFile) -> None:
file_name_template = "%s/plugin_metadata.json"
for plugin_id, metadata in Application.getInstance().getWorkspaceMetadataStorage().getAllData().items():
file_name = file_name_template % plugin_id
file_in_archive = zipfile.ZipInfo(file_name)
# We have to set the compress type of each file as well (it doesn't keep the type of the entire archive)
file_in_archive.compress_type = zipfile.ZIP_DEFLATED
import json
archive.writestr(file_in_archive, json.dumps(metadata, separators = (", ", ": "), indent = 4, skipkeys = True))
## Helper function that writes ContainerStacks, InstanceContainers and DefinitionContainers to the archive. ## Helper function that writes ContainerStacks, InstanceContainers and DefinitionContainers to the archive.
# \param container That follows the \type{ContainerInterface} to archive. # \param container That follows the \type{ContainerInterface} to archive.
# \param archive The archive to write to. # \param archive The archive to write to.

View file

@ -1,5 +1,6 @@
# Copyright (c) 2015 Ultimaker B.V. # Copyright (c) 2015 Ultimaker B.V.
# Uranium is released under the terms of the LGPLv3 or higher. # Uranium is released under the terms of the LGPLv3 or higher.
from typing import Optional
from UM.Mesh.MeshWriter import MeshWriter from UM.Mesh.MeshWriter import MeshWriter
from UM.Math.Vector import Vector from UM.Math.Vector import Vector
@ -40,7 +41,7 @@ class ThreeMFWriter(MeshWriter):
} }
self._unit_matrix_string = self._convertMatrixToString(Matrix()) self._unit_matrix_string = self._convertMatrixToString(Matrix())
self._archive = None self._archive = None # type: Optional[zipfile.ZipFile]
self._store_archive = False self._store_archive = False
def _convertMatrixToString(self, matrix): def _convertMatrixToString(self, matrix):

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.1", "version": "1.0.1",
"description": "Provides support for writing 3MF files.", "description": "Provides support for writing 3MF files.",
"api": "7.0", "api": "7.1",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -3,5 +3,5 @@
"author": "fieldOfView", "author": "fieldOfView",
"version": "1.0.0", "version": "1.0.0",
"description": "Provides support for reading AMF files.", "description": "Provides support for reading AMF files.",
"api": "7.0.0" "api": "7.1.0"
} }

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"description": "Backup and restore your configuration.", "description": "Backup and restore your configuration.",
"version": "1.2.0", "version": "1.2.0",
"api": "7.0", "api": "7.1",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -55,10 +55,22 @@ class CuraEngineBackend(QObject, Backend):
if Platform.isWindows(): if Platform.isWindows():
executable_name += ".exe" executable_name += ".exe"
default_engine_location = executable_name default_engine_location = executable_name
if os.path.exists(os.path.join(CuraApplication.getInstallPrefix(), "bin", executable_name)):
default_engine_location = os.path.join(CuraApplication.getInstallPrefix(), "bin", executable_name) search_path = [
if hasattr(sys, "frozen"): os.path.abspath(os.path.dirname(sys.executable)),
default_engine_location = os.path.join(os.path.dirname(os.path.abspath(sys.executable)), executable_name) os.path.abspath(os.path.join(os.path.dirname(sys.executable), "bin")),
os.path.abspath(os.path.join(os.path.dirname(sys.executable), "..")),
os.path.join(CuraApplication.getInstallPrefix(), "bin"),
os.path.dirname(os.path.abspath(sys.executable)),
]
for path in search_path:
engine_path = os.path.join(path, executable_name)
if os.path.isfile(engine_path):
default_engine_location = engine_path
break
if Platform.isLinux() and not default_engine_location: if Platform.isLinux() and not default_engine_location:
if not os.getenv("PATH"): if not os.getenv("PATH"):
raise OSError("There is something wrong with your Linux installation.") raise OSError("There is something wrong with your Linux installation.")

View file

@ -2,7 +2,7 @@
"name": "CuraEngine Backend", "name": "CuraEngine Backend",
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"description": "Provides the link to the CuraEngine slicing backend.", "description": "Provides the link to the CuraEngine slicing backend.",
"api": "7.0", "api": "7.1",
"version": "1.0.1", "version": "1.0.1",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.1", "version": "1.0.1",
"description": "Provides support for importing Cura profiles.", "description": "Provides support for importing Cura profiles.",
"api": "7.0", "api": "7.1",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.1", "version": "1.0.1",
"description": "Provides support for exporting Cura profiles.", "description": "Provides support for exporting Cura profiles.",
"api": "7.0", "api": "7.1",
"i18n-catalog":"cura" "i18n-catalog":"cura"
} }

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.1", "version": "1.0.1",
"description": "Checks for firmware updates.", "description": "Checks for firmware updates.",
"api": "7.0", "api": "7.1",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.1", "version": "1.0.1",
"description": "Provides a machine actions for updating firmware.", "description": "Provides a machine actions for updating firmware.",
"api": "7.0", "api": "7.1",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.1", "version": "1.0.1",
"description": "Reads g-code from a compressed archive.", "description": "Reads g-code from a compressed archive.",
"api": "7.0", "api": "7.1",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.1", "version": "1.0.1",
"description": "Writes g-code to a compressed archive.", "description": "Writes g-code to a compressed archive.",
"api": "7.0", "api": "7.1",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.1", "version": "1.0.1",
"description": "Provides support for importing profiles from g-code files.", "description": "Provides support for importing profiles from g-code files.",
"api": "7.0", "api": "7.1",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -1,4 +1,4 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2020 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
import math import math
@ -258,16 +258,19 @@ class FlavorParser:
continue continue
if item.startswith(";"): if item.startswith(";"):
continue continue
if item[0] == "X": try:
x = float(item[1:]) if item[0] == "X":
if item[0] == "Y": x = float(item[1:])
y = float(item[1:]) if item[0] == "Y":
if item[0] == "Z": y = float(item[1:])
z = float(item[1:]) if item[0] == "Z":
if item[0] == "F": z = float(item[1:])
f = float(item[1:]) / 60 if item[0] == "F":
if item[0] == "E": f = float(item[1:]) / 60
e = float(item[1:]) if item[0] == "E":
e = float(item[1:])
except ValueError: # Improperly formatted g-code: Coordinates are not floats.
continue # Skip the command then.
params = PositionOptional(x, y, z, f, e) params = PositionOptional(x, y, z, f, e)
return func(position, params, path) return func(position, params, path)
return position return position

View file

@ -1,8 +1,8 @@
{ {
"name": "G-code Reader", "name": "G-code Reader",
"author": "Victor Larchenko, Ultimaker", "author": "Victor Larchenko, Ultimaker B.V.",
"version": "1.0.1", "version": "1.0.1",
"description": "Allows loading and displaying G-code files.", "description": "Allows loading and displaying G-code files.",
"api": "7.0", "api": "7.1",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.1", "version": "1.0.1",
"description": "Writes g-code to a file.", "description": "Writes g-code to a file.",
"api": "7.0", "api": "7.1",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.1", "version": "1.0.1",
"description": "Enables ability to generate printable geometry from 2D image files.", "description": "Enables ability to generate printable geometry from 2D image files.",
"api": "7.0", "api": "7.1",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.1", "version": "1.0.1",
"description": "Provides support for importing profiles from legacy Cura versions.", "description": "Provides support for importing profiles from legacy Cura versions.",
"api": "7.0", "api": "7.1",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -1,8 +1,8 @@
{ {
"name": "Machine Settings action", "name": "Machine Settings Action",
"author": "fieldOfView", "author": "fieldOfView, Ultimaker B.V.",
"version": "1.0.1", "version": "1.0.1",
"description": "Provides a way to change machine settings (such as build volume, nozzle size, etc.).", "description": "Provides a way to change machine settings (such as build volume, nozzle size, etc.).",
"api": "7.0", "api": "7.1",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -2,7 +2,7 @@
"name": "Model Checker", "name": "Model Checker",
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.1", "version": "1.0.1",
"api": "7.0", "api": "7.1",
"description": "Checks models and print configuration for possible printing issues and give suggestions.", "description": "Checks models and print configuration for possible printing issues and give suggestions.",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.1", "version": "1.0.1",
"description": "Provides a monitor stage in Cura.", "description": "Provides a monitor stage in Cura.",
"api": "7.0", "api": "7.1",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -1,10 +1,11 @@
# Copyright (c) 2016 Ultimaker B.V. # Copyright (c) 2020 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import pyqtProperty from PyQt5.QtCore import pyqtProperty
from UM.FlameProfiler import pyqtSlot from UM.FlameProfiler import pyqtSlot
from UM.Application import Application from UM.Application import Application
from UM.PluginRegistry import PluginRegistry
from UM.Settings.ContainerRegistry import ContainerRegistry from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.Settings.SettingInstance import SettingInstance from UM.Settings.SettingInstance import SettingInstance
from UM.Logger import Logger from UM.Logger import Logger
@ -24,6 +25,8 @@ class PerObjectSettingVisibilityHandler(UM.Settings.Models.SettingVisibilityHand
self._node = None self._node = None
self._stack = None self._stack = None
PluginRegistry.getInstance().getPluginObject("PerObjectSettingsTool").visibility_handler = self
# this is a set of settings that will be skipped if the user chooses to reset. # this is a set of settings that will be skipped if the user chooses to reset.
self._skip_reset_setting_set = set() self._skip_reset_setting_set = set()
@ -68,7 +71,7 @@ class PerObjectSettingVisibilityHandler(UM.Settings.Models.SettingVisibilityHand
# Add all instances that are not added, but are in visibility list # Add all instances that are not added, but are in visibility list
for item in visible: for item in visible:
if not settings.getInstance(item): # Setting was not added already. if settings.getInstance(item) is not None: # Setting was not added already.
definition = self._stack.getSettingDefinition(item) definition = self._stack.getSettingDefinition(item)
if definition: if definition:
new_instance = SettingInstance(definition, settings) new_instance = SettingInstance(definition, settings)

View file

@ -1,5 +1,6 @@
# Copyright (c) 2016 Ultimaker B.V. # Copyright (c) 2020 Ultimaker B.V.
# Uranium is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from UM.Logger import Logger from UM.Logger import Logger
from UM.Tool import Tool from UM.Tool import Tool
from UM.Scene.Selection import Selection from UM.Scene.Selection import Selection
@ -22,14 +23,13 @@ class PerObjectSettingsTool(Tool):
self._multi_extrusion = False self._multi_extrusion = False
self._single_model_selected = False self._single_model_selected = False
self.visibility_handler = None
Selection.selectionChanged.connect(self.propertyChanged) Selection.selectionChanged.connect(self.propertyChanged)
Application.getInstance().globalContainerStackChanged.connect(self._onGlobalContainerChanged) Application.getInstance().globalContainerStackChanged.connect(self._onGlobalContainerChanged)
self._onGlobalContainerChanged() self._onGlobalContainerChanged()
Selection.selectionChanged.connect(self._updateEnabled) Selection.selectionChanged.connect(self._updateEnabled)
def event(self, event): def event(self, event):
super().event(event) super().event(event)
if event.type == Event.MousePressEvent and self._controller.getToolsEnabled(): if event.type == Event.MousePressEvent and self._controller.getToolsEnabled():
@ -68,7 +68,8 @@ class PerObjectSettingsTool(Tool):
## Returns True when the mesh_type was changed, False when current mesh_type == mesh_type ## Returns True when the mesh_type was changed, False when current mesh_type == mesh_type
def setMeshType(self, mesh_type: str) -> bool: def setMeshType(self, mesh_type: str) -> bool:
if self.getMeshType() == mesh_type: old_mesh_type = self.getMeshType()
if old_mesh_type == mesh_type:
return False return False
selected_object = Selection.getSelectedObject(0) selected_object = Selection.getSelectedObject(0)
@ -94,6 +95,20 @@ class PerObjectSettingsTool(Tool):
new_instance.resetState() # Ensure that the state is not seen as a user state. new_instance.resetState() # Ensure that the state is not seen as a user state.
settings.addInstance(new_instance) settings.addInstance(new_instance)
for property_key in ["top_bottom_thickness", "wall_thickness"]:
if mesh_type == "infill_mesh":
if not settings.getInstance(property_key):
definition = stack.getSettingDefinition(property_key)
new_instance = SettingInstance(definition, settings)
new_instance.setProperty("value", 0)
new_instance.resetState() # Ensure that the state is not seen as a user state.
settings.addInstance(new_instance)
visible = self.visibility_handler.getVisible()
visible.add(property_key)
self.visibility_handler.setVisible(visible)
elif old_mesh_type == "infill_mesh" and settings.getInstance(property_key) and settings.getProperty(property_key, "value") == 0:
settings.removeInstance(property_key)
self.propertyChanged.emit() self.propertyChanged.emit()
return True return True

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.1", "version": "1.0.1",
"description": "Provides the Per Model Settings.", "description": "Provides the Per Model Settings.",
"api": "7.0", "api": "7.1",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -2,7 +2,7 @@
"name": "Post Processing", "name": "Post Processing",
"author": "Ultimaker", "author": "Ultimaker",
"version": "2.2.1", "version": "2.2.1",
"api": "7.0", "api": "7.1",
"description": "Extension that allows for user created scripts for post processing", "description": "Extension that allows for user created scripts for post processing",
"catalog": "cura" "catalog": "cura"
} }

View file

@ -72,18 +72,25 @@ class DisplayFilenameAndLayerOnLCD(Script):
lcd_text = "M117 Printing " + name + " - Layer " lcd_text = "M117 Printing " + name + " - Layer "
i = self.getSettingValueByKey("startNum") i = self.getSettingValueByKey("startNum")
for layer in data: for layer in data:
display_text = lcd_text + str(i) + " " + name display_text = lcd_text + str(i)
layer_index = data.index(layer) layer_index = data.index(layer)
lines = layer.split("\n") lines = layer.split("\n")
for line in lines: for line in lines:
if line.startswith(";LAYER_COUNT:"): if line.startswith(";LAYER_COUNT:"):
max_layer = line max_layer = line
max_layer = max_layer.split(":")[1] max_layer = max_layer.split(":")[1]
if self.getSettingValueByKey("startNum") == 0:
max_layer = str(int(max_layer) - 1)
if line.startswith(";LAYER:"): if line.startswith(";LAYER:"):
if self.getSettingValueByKey("maxlayer"): if self.getSettingValueByKey("maxlayer"):
display_text = display_text + " of " + max_layer display_text = display_text + " of " + max_layer
if not self.getSettingValueByKey("scroll"):
display_text = display_text + " " + name
else: else:
display_text = display_text + "!" if not self.getSettingValueByKey("scroll"):
display_text = display_text + " " + name + "!"
else:
display_text = display_text + "!"
line_index = lines.index(line) line_index = lines.index(line)
lines.insert(line_index + 1, display_text) lines.insert(line_index + 1, display_text)
i += 1 i += 1

View file

@ -2,6 +2,7 @@
from ..Script import Script from ..Script import Script
class TimeLapse(Script): class TimeLapse(Script):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
@ -75,21 +76,29 @@ class TimeLapse(Script):
trigger_command = self.getSettingValueByKey("trigger_command") trigger_command = self.getSettingValueByKey("trigger_command")
pause_length = self.getSettingValueByKey("pause_length") pause_length = self.getSettingValueByKey("pause_length")
gcode_to_append = ";TimeLapse Begin\n" gcode_to_append = ";TimeLapse Begin\n"
last_x = 0
last_y = 0
if park_print_head: if park_print_head:
gcode_to_append += self.putValue(G = 1, F = feed_rate, X = x_park, Y = y_park) + " ;Park print head\n" gcode_to_append += self.putValue(G=1, F=feed_rate,
gcode_to_append += self.putValue(M = 400) + " ;Wait for moves to finish\n" X=x_park, Y=y_park) + " ;Park print head\n"
gcode_to_append += self.putValue(M=400) + " ;Wait for moves to finish\n"
gcode_to_append += trigger_command + " ;Snap Photo\n" gcode_to_append += trigger_command + " ;Snap Photo\n"
gcode_to_append += self.putValue(G = 4, P = pause_length) + " ;Wait for camera\n" gcode_to_append += self.putValue(G=4, P=pause_length) + " ;Wait for camera\n"
gcode_to_append += ";TimeLapse End\n"
for layer in data: for idx, layer in enumerate(data):
for line in layer.split("\n"):
if self.getValue(line, "G") in {0, 1}: # Track X,Y location.
last_x = self.getValue(line, "X", last_x)
last_y = self.getValue(line, "Y", last_y)
# Check that a layer is being printed # Check that a layer is being printed
lines = layer.split("\n") lines = layer.split("\n")
for line in lines: for line in lines:
if ";LAYER:" in line: if ";LAYER:" in line:
index = data.index(layer)
layer += gcode_to_append layer += gcode_to_append
data[index] = layer layer += "G0 X%s Y%s\n" % (last_x, last_y)
data[idx] = layer
break break
return data return data

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.1", "version": "1.0.1",
"description": "Provides a prepare stage in Cura.", "description": "Provides a prepare stage in Cura.",
"api": "7.0", "api": "7.1",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.1", "version": "1.0.1",
"description": "Provides a preview stage in Cura.", "description": "Provides a preview stage in Cura.",
"api": "7.0", "api": "7.1",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"description": "Provides removable drive hotplugging and writing support.", "description": "Provides removable drive hotplugging and writing support.",
"version": "1.0.1", "version": "1.0.1",
"api": "7.0", "api": "7.1",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.0", "version": "1.0.0",
"description": "Logs certain events so that they can be used by the crash reporter", "description": "Logs certain events so that they can be used by the crash reporter",
"api": "7.0", "api": "7.1",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -1,4 +1,4 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2020 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
import sys import sys
@ -116,8 +116,9 @@ class SimulationView(CuraView):
self._only_show_top_layers = bool(Application.getInstance().getPreferences().getValue("view/only_show_top_layers")) self._only_show_top_layers = bool(Application.getInstance().getPreferences().getValue("view/only_show_top_layers"))
self._compatibility_mode = self._evaluateCompatibilityMode() self._compatibility_mode = self._evaluateCompatibilityMode()
self._wireprint_warning_message = Message(catalog.i18nc("@info:status", "Cura does not accurately display layers when Wire Printing is enabled"), self._wireprint_warning_message = Message(catalog.i18nc("@info:status", "Cura does not accurately display layers when Wire Printing is enabled."),
title = catalog.i18nc("@info:title", "Simulation View")) title = catalog.i18nc("@info:title", "Simulation View"))
self._slice_first_warning_message = Message(catalog.i18nc("@info:status", "Nothing is shown because you need to slice first."), title = catalog.i18nc("@info:title", "No layers to show"))
QtApplication.getInstance().engineCreatedSignal.connect(self._onEngineCreated) QtApplication.getInstance().engineCreatedSignal.connect(self._onEngineCreated)
@ -149,6 +150,7 @@ class SimulationView(CuraView):
if self._activity == activity: if self._activity == activity:
return return
self._activity = activity self._activity = activity
self._updateSliceWarningVisibility()
self.activityChanged.emit() self.activityChanged.emit()
def getSimulationPass(self) -> SimulationPass: def getSimulationPass(self) -> SimulationPass:
@ -543,11 +545,13 @@ class SimulationView(CuraView):
self._composite_pass.getLayerBindings().append("simulationview") self._composite_pass.getLayerBindings().append("simulationview")
self._old_composite_shader = self._composite_pass.getCompositeShader() self._old_composite_shader = self._composite_pass.getCompositeShader()
self._composite_pass.setCompositeShader(self._simulationview_composite_shader) self._composite_pass.setCompositeShader(self._simulationview_composite_shader)
self._updateSliceWarningVisibility()
elif event.type == Event.ViewDeactivateEvent: elif event.type == Event.ViewDeactivateEvent:
self._controller.getScene().getRoot().childrenChanged.disconnect(self._onSceneChanged) self._controller.getScene().getRoot().childrenChanged.disconnect(self._onSceneChanged)
Application.getInstance().getPreferences().preferenceChanged.disconnect(self._onPreferencesChanged) Application.getInstance().getPreferences().preferenceChanged.disconnect(self._onPreferencesChanged)
self._wireprint_warning_message.hide() self._wireprint_warning_message.hide()
self._slice_first_warning_message.hide()
Application.getInstance().globalContainerStackChanged.disconnect(self._onGlobalStackChanged) Application.getInstance().globalContainerStackChanged.disconnect(self._onGlobalStackChanged)
if self._global_container_stack: if self._global_container_stack:
self._global_container_stack.propertyChanged.disconnect(self._onPropertyChanged) self._global_container_stack.propertyChanged.disconnect(self._onPropertyChanged)
@ -661,6 +665,12 @@ class SimulationView(CuraView):
self._updateWithPreferences() self._updateWithPreferences()
def _updateSliceWarningVisibility(self):
if not self.getActivity():
self._slice_first_warning_message.show()
else:
self._slice_first_warning_message.hide()
class _CreateTopLayersJob(Job): class _CreateTopLayersJob(Job):
def __init__(self, scene: "Scene", layer_number: int, solid_layers: int) -> None: def __init__(self, scene: "Scene", layer_number: int, solid_layers: int) -> None:

View file

@ -112,7 +112,7 @@ Cura.ExpandableComponent
type_id: 1 type_id: 1
}) })
layerViewTypes.append({ layerViewTypes.append({
text: catalog.i18nc("@label:listbox", "Feedrate"), text: catalog.i18nc("@label:listbox", "Speed"),
type_id: 2 type_id: 2
}) })
layerViewTypes.append({ layerViewTypes.append({

View file

@ -80,7 +80,7 @@ vertex41core =
case 1: // "Line type" case 1: // "Line type"
v_color = a_color; v_color = a_color;
break; break;
case 2: // "Feedrate" case 2: // "Speed", or technically 'Feedrate'
v_color = feedrateGradientColor(a_feedrate, u_min_feedrate, u_max_feedrate); v_color = feedrateGradientColor(a_feedrate, u_min_feedrate, u_max_feedrate);
break; break;
case 3: // "Layer thickness" case 3: // "Layer thickness"

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.1", "version": "1.0.1",
"description": "Provides the Simulation view.", "description": "Provides the Simulation view.",
"api": "7.0", "api": "7.1",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -72,6 +72,7 @@ Window
right: parent.right right: parent.right
} }
text: catalog.i18nc("@text:window", "Ultimaker Cura collects anonymous data in order to improve the print quality and user experience. Below is an example of all the data that is shared:") text: catalog.i18nc("@text:window", "Ultimaker Cura collects anonymous data in order to improve the print quality and user experience. Below is an example of all the data that is shared:")
color: UM.Theme.getColor("text")
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
renderType: Text.NativeRendering renderType: Text.NativeRendering
} }

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.1", "version": "1.0.1",
"description": "Submits anonymous slice info. Can be disabled through preferences.", "description": "Submits anonymous slice info. Can be disabled through preferences.",
"api": "7.0", "api": "7.1",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.1", "version": "1.0.1",
"description": "Provides a normal solid mesh view.", "description": "Provides a normal solid mesh view.",
"api": "7.0", "api": "7.1",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.1", "version": "1.0.1",
"description": "Creates an eraser mesh to block the printing of support in certain places", "description": "Creates an eraser mesh to block the printing of support in certain places",
"api": "7.0", "api": "7.1",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -2,6 +2,6 @@
"name": "Toolbox", "name": "Toolbox",
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.1", "version": "1.0.1",
"api": "7.0", "api": "7.1",
"description": "Find, manage and install new Cura packages." "description": "Find, manage and install new Cura packages."
} }

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24">
<path d="M19,3H5A2.9,2.9,0,0,0,2,6V9a3.9,3.9,0,0,0,2,3.4V22H20V12.4A3.9,3.9,0,0,0,22,9V6A2.9,2.9,0,0,0,19,3ZM10,5h4V9a2,2,0,0,1-4,0ZM4,9V5H8V9A2,2,0,0,1,4,9ZM18,20H14V15H10v5H6V13a3.7,3.7,0,0,0,3-1.4A3.7,3.7,0,0,0,12,13a3.7,3.7,0,0,0,3-1.4A3.7,3.7,0,0,0,18,13ZM20,9a2,2,0,0,1-4,0V5h4Z" />
</svg>

After

Width:  |  Height:  |  Size: 364 B

View file

@ -14,17 +14,44 @@ Rectangle
Column Column
{ {
height: childrenRect.height + 2 * padding height: childrenRect.height + 2 * padding
spacing: UM.Theme.getSize("default_margin").width spacing: UM.Theme.getSize("default_margin").height
width: parent.width width: parent.width
padding: UM.Theme.getSize("wide_margin").height padding: UM.Theme.getSize("wide_margin").height
Label Item
{ {
id: heading width: parent.width - parent.padding * 2
text: catalog.i18nc("@label", "Featured") height: childrenRect.height
width: parent.width Label
color: UM.Theme.getColor("text_medium") {
font: UM.Theme.getFont("large") id: heading
renderType: Text.NativeRendering text: catalog.i18nc("@label", "Featured")
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")) + 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 Grid
{ {

View file

@ -1,4 +1,4 @@
// Copyright (c) 2018 Ultimaker B.V. // Copyright (c) 2020 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher. // Toolbox is released under the terms of the LGPLv3 or higher.
import QtQuick 2.10 import QtQuick 2.10
@ -51,32 +51,25 @@ Item
toolbox.viewPage = "overview" toolbox.viewPage = "overview"
} }
} }
}
ToolboxTabButton ToolboxTabButton
{
id: installedTabButton
text: catalog.i18nc("@title:tab", "Installed")
active: toolbox.viewCategory == "installed"
enabled: !toolbox.isDownloading
anchors
{ {
right: parent.right id: installedTabButton
rightMargin: UM.Theme.getSize("default_margin").width 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
} }
onClicked: toolbox.viewCategory = "installed"
width: UM.Theme.getSize("toolbox_header_tab").width + marketplaceNotificationIcon.width - UM.Theme.getSize("default_margin").width
} }
Cura.NotificationIcon Cura.NotificationIcon
{ {
id: marketplaceNotificationIcon id: marketplaceNotificationIcon
visible: CuraApplication.getPackageManager().packagesWithUpdate.length > 0 visible: CuraApplication.getPackageManager().packagesWithUpdate.length > 0
anchors.right: bar.right
anchors.right: installedTabButton.right
anchors.verticalCenter: installedTabButton.verticalCenter
labelText: labelText:
{ {
const itemCount = CuraApplication.getPackageManager().packagesWithUpdate.length const itemCount = CuraApplication.getPackageManager().packagesWithUpdate.length
@ -84,6 +77,33 @@ Item
} }
} }
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
}
onClicked: Qt.openUrlExternally(toolbox.getWebMarketplaceUrl("plugins"))
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 ToolboxShadow
{ {
anchors.top: bar.bottom anchors.top: bar.bottom

View file

@ -20,6 +20,8 @@ UM.Dialog{
maximumHeight: minimumHeight maximumHeight: minimumHeight
margin: 0 margin: 0
property string actionButtonText: subscribedPackagesModel.hasIncompatiblePackages && !subscribedPackagesModel.hasCompatiblePackages ? catalog.i18nc("@button", "Dismiss") : catalog.i18nc("@button", "Next")
Rectangle Rectangle
{ {
id: root id: root
@ -90,7 +92,7 @@ UM.Dialog{
Label Label
{ {
font: UM.Theme.getFont("default") font: UM.Theme.getFont("default")
text: catalog.i18nc("@label", "The following packages can not be installed because of incompatible Cura version:") text: catalog.i18nc("@label", "The following packages can not be installed because of an incompatible Cura version:")
visible: subscribedPackagesModel.hasIncompatiblePackages visible: subscribedPackagesModel.hasIncompatiblePackages
color: UM.Theme.getColor("text") color: UM.Theme.getColor("text")
height: contentHeight + UM.Theme.getSize("default_margin").height height: contentHeight + UM.Theme.getSize("default_margin").height
@ -125,26 +127,6 @@ UM.Dialog{
color: UM.Theme.getColor("text") color: UM.Theme.getColor("text")
elide: Text.ElideRight elide: Text.ElideRight
} }
UM.TooltipArea
{
width: childrenRect.width;
height: childrenRect.height;
text: catalog.i18nc("@info:tooltip", "Dismisses the package and won't be shown in this dialog anymore")
anchors.right: parent.right
anchors.verticalCenter: packageIcon.verticalCenter
Label
{
text: "(Dismiss)"
font: UM.Theme.getFont("small")
color: UM.Theme.getColor("text")
MouseArea
{
cursorShape: Qt.PointingHandCursor
anchors.fill: parent
onClicked: handler.dismissIncompatiblePackage(subscribedPackagesModel, model.package_id)
}
}
}
} }
} }
} }
@ -152,14 +134,16 @@ UM.Dialog{
} // End of ScrollView } // End of ScrollView
Cura.ActionButton Cura.PrimaryButton
{ {
id: nextButton id: nextButton
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.right: parent.right anchors.right: parent.right
anchors.margins: UM.Theme.getSize("default_margin").height anchors.margins: UM.Theme.getSize("default_margin").height
text: catalog.i18nc("@button", "Next") text: actionButtonText
onClicked: accept() onClicked: accept()
leftPadding: UM.Theme.getSize("dialog_primary_button_padding").width
rightPadding: UM.Theme.getSize("dialog_primary_button_padding").width
} }
} }
} }

View file

@ -4,12 +4,12 @@
import QtQuick 2.10 import QtQuick 2.10
import QtQuick.Dialogs 1.1 import QtQuick.Dialogs 1.1
import QtQuick.Window 2.2 import QtQuick.Window 2.2
import QtQuick.Controls 1.4 import QtQuick.Controls 2.3
import QtQuick.Layouts 1.3
import QtQuick.Controls.Styles 1.4 import QtQuick.Controls.Styles 1.4
// TODO: Switch to QtQuick.Controls 2.x and remove QtQuick.Controls.Styles
import UM 1.1 as UM import UM 1.1 as UM
import Cura 1.6 as Cura
UM.Dialog UM.Dialog
{ {
@ -19,50 +19,90 @@ UM.Dialog
minimumHeight: UM.Theme.getSize("license_window_minimum").height minimumHeight: UM.Theme.getSize("license_window_minimum").height
width: minimumWidth width: minimumWidth
height: minimumHeight height: minimumHeight
backgroundColor: UM.Theme.getColor("main_background")
margin: screenScaleFactor * 10
Item ColumnLayout
{ {
anchors.fill: parent anchors.fill: parent
spacing: UM.Theme.getSize("thick_margin").height
UM.I18nCatalog{id: catalog; name: "cura"} UM.I18nCatalog{id: catalog; name: "cura"}
Label Label
{ {
id: licenseHeader id: licenseHeader
anchors.top: parent.top Layout.fillWidth: true
anchors.left: parent.left text: catalog.i18nc("@label", "You need to accept the license to install the package")
anchors.right: parent.right color: UM.Theme.getColor("text")
text: licenseModel.headerText
wrapMode: Text.Wrap wrapMode: Text.Wrap
renderType: Text.NativeRendering renderType: Text.NativeRendering
} }
TextArea
{ Row {
id: licenseText id: packageRow
anchors.top: licenseHeader.bottom
anchors.bottom: parent.bottom
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.topMargin: UM.Theme.getSize("default_margin").height height: childrenRect.height
readOnly: true spacing: UM.Theme.getSize("default_margin").width
text: licenseModel.licenseText leftPadding: UM.Theme.getSize("narrow_margin").width
Image
{
id: icon
width: 30 * screenScaleFactor
height: width
fillMode: Image.PreserveAspectFit
source: licenseModel.iconUrl || "../../images/logobot.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: rightButtons:
[ [
Button Cura.PrimaryButton
{ {
id: acceptButton leftPadding: UM.Theme.getSize("dialog_primary_button_padding").width
anchors.margins: UM.Theme.getSize("default_margin").width rightPadding: UM.Theme.getSize("dialog_primary_button_padding").width
text: catalog.i18nc("@action:button", "Accept")
text: licenseModel.acceptButtonText
onClicked: { handler.onLicenseAccepted() } onClicked: { handler.onLicenseAccepted() }
}, }
Button ]
leftButtons:
[
Cura.SecondaryButton
{ {
id: declineButton id: declineButton
anchors.margins: UM.Theme.getSize("default_margin").width text: licenseModel.declineButtonText
text: catalog.i18nc("@action:button", "Decline")
onClicked: { handler.onLicenseDeclined() } onClicked: { handler.onLicenseDeclined() }
} }
] ]

View file

@ -18,3 +18,11 @@ class CloudApiModel:
cloud_api_root=cloud_api_root, cloud_api_root=cloud_api_root,
cloud_api_version=cloud_api_version, cloud_api_version=cloud_api_version,
) )
## https://api.ultimaker.com/cura-packages/v1/user/packages/{package_id}
@classmethod
def userPackageUrl(cls, package_id: str) -> str:
return (CloudApiModel.api_url_user_packages + "/{package_id}").format(
package_id=package_id
)

View file

@ -0,0 +1,51 @@
from UM.Logger import Logger
from UM.TaskManagement.HttpRequestManager import HttpRequestManager
from cura.CuraApplication import CuraApplication
from ..CloudApiModel import CloudApiModel
from ..UltimakerCloudScope import UltimakerCloudScope
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 = UltimakerCloudScope(app) # type: UltimakerCloudScope
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

@ -8,11 +8,12 @@ from UM import i18nCatalog
from UM.Logger import Logger from UM.Logger import Logger
from UM.Message import Message from UM.Message import Message
from UM.Signal import Signal from UM.Signal import Signal
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication, ApplicationMetadata
from ..CloudApiModel import CloudApiModel from ..CloudApiModel import CloudApiModel
from .SubscribedPackagesModel import SubscribedPackagesModel from .SubscribedPackagesModel import SubscribedPackagesModel
from ..UltimakerCloudScope import UltimakerCloudScope from ..UltimakerCloudScope import UltimakerCloudScope
from typing import List, Dict, Any
class CloudPackageChecker(QObject): class CloudPackageChecker(QObject):
def __init__(self, application: CuraApplication) -> None: def __init__(self, application: CuraApplication) -> None:
@ -25,12 +26,12 @@ class CloudPackageChecker(QObject):
self._application.initializationFinished.connect(self._onAppInitialized) self._application.initializationFinished.connect(self._onAppInitialized)
self._i18n_catalog = i18nCatalog("cura") self._i18n_catalog = i18nCatalog("cura")
self._sdk_version = ApplicationMetadata.CuraSDKVersion
# This is a plugin, so most of the components required are not ready when # 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. # this is initialized. Therefore, we wait until the application is ready.
def _onAppInitialized(self) -> None: def _onAppInitialized(self) -> None:
self._package_manager = self._application.getPackageManager() self._package_manager = self._application.getPackageManager()
# initial check # initial check
self._fetchUserSubscribedPackages() self._fetchUserSubscribedPackages()
# check again whenever the login state changes # check again whenever the login state changes
@ -38,25 +39,51 @@ class CloudPackageChecker(QObject):
def _fetchUserSubscribedPackages(self) -> None: def _fetchUserSubscribedPackages(self) -> None:
if self._application.getCuraAPI().account.isLoggedIn: if self._application.getCuraAPI().account.isLoggedIn:
self._getUserPackages() self._getUserSubscribedPackages()
def _handleCompatibilityData(self, json_data) -> None: def _getUserSubscribedPackages(self) -> None:
user_subscribed_packages = [plugin["package_id"] for plugin in json_data] Logger.debug("Requesting subscribed packages metadata from server.")
url = CloudApiModel.api_url_user_packages
self._application.getHttpRequestManager().get(url,
callback = self._onUserPackagesRequestFinished,
error_callback = self._onUserPackagesRequestFinished,
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())
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"])
return
self._handleCompatibilityData(json_data["data"])
except json.decoder.JSONDecodeError:
Logger.log("w", "Received invalid JSON for user subscribed packages from the Web Marketplace")
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.getUserInstalledPackages() user_installed_packages = self._package_manager.getUserInstalledPackages()
# 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() user_dismissed_packages = self._package_manager.getDismissedPackages()
if user_dismissed_packages: if user_dismissed_packages:
user_installed_packages += user_dismissed_packages user_installed_packages += user_dismissed_packages
# We check if there are packages installed in Cloud Marketplace but not in Cura marketplace
# We check if there are packages installed in Web Marketplace but not in Cura marketplace
package_discrepancy = list(set(user_subscribed_packages).difference(user_installed_packages)) package_discrepancy = list(set(user_subscribed_packages).difference(user_installed_packages))
self._model.setMetadata(json_data)
self._model.addDiscrepancies(package_discrepancy)
self._model.initialize()
if not self._model.hasCompatiblePackages:
return None
if package_discrepancy: if package_discrepancy:
self._model.addDiscrepancies(package_discrepancy)
self._model.initialize(subscribed_packages_payload)
self._handlePackageDiscrepancies() self._handlePackageDiscrepancies()
def _handlePackageDiscrepancies(self) -> None: def _handlePackageDiscrepancies(self) -> None:
@ -64,7 +91,6 @@ class CloudPackageChecker(QObject):
sync_message = Message(self._i18n_catalog.i18nc( sync_message = Message(self._i18n_catalog.i18nc(
"@info:generic", "@info:generic",
"\nDo you want to sync material and software packages with your account?"), "\nDo you want to sync material and software packages with your account?"),
lifetime=0,
title=self._i18n_catalog.i18nc("@info:title", "Changes detected from your Ultimaker account", )) title=self._i18n_catalog.i18nc("@info:title", "Changes detected from your Ultimaker account", ))
sync_message.addAction("sync", sync_message.addAction("sync",
name=self._i18n_catalog.i18nc("@action:button", "Sync"), name=self._i18n_catalog.i18nc("@action:button", "Sync"),
@ -76,35 +102,4 @@ class CloudPackageChecker(QObject):
def _onSyncButtonClicked(self, sync_message: Message, sync_message_action: str) -> None: def _onSyncButtonClicked(self, sync_message: Message, sync_message_action: str) -> None:
sync_message.hide() sync_message.hide()
self.discrepancies.emit(self._model) self.discrepancies.emit(self._model)
def _getUserPackages(self) -> None:
Logger.log("d", "Requesting subscribed packages metadata from server.")
url = CloudApiModel.api_url_user_packages
self._application.getHttpRequestManager().get(url,
callback = self._onUserPackagesRequestFinished,
error_callback = self._onUserPackagesRequestFinished,
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())
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"])
return
self._handleCompatibilityData(json_data["data"])
except json.decoder.JSONDecodeError:
Logger.log("w", "Received invalid JSON for user packages")

View file

@ -1,18 +0,0 @@
from cura.CuraApplication import CuraApplication
from ..CloudApiModel import CloudApiModel
from ..UltimakerCloudScope import UltimakerCloudScope
## 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
class CloudPackageManager:
def __init__(self, app: CuraApplication) -> None:
self._request_manager = app.getHttpRequestManager()
self._scope = UltimakerCloudScope(app)
def subscribe(self, package_id: str) -> None:
data = "{\"data\": {\"package_id\": \"%s\", \"sdk_version\": \"%s\"}}" % (package_id, CloudApiModel.sdk_version)
self._request_manager.put(url=CloudApiModel.api_url_user_packages,
data=data.encode(),
scope=self._scope
)

View file

@ -28,13 +28,12 @@ class DiscrepanciesPresenter(QObject):
assert self._dialog assert self._dialog
self._dialog.accepted.connect(lambda: self._onConfirmClicked(model)) self._dialog.accepted.connect(lambda: self._onConfirmClicked(model))
@pyqtSlot("QVariant", str)
def dismissIncompatiblePackage(self, model: SubscribedPackagesModel, package_id: str) -> None:
model.dismissPackage(package_id) # update the model to update the view
self._package_manager.dismissPackage(package_id) # adds this package_id as dismissed in the user config file
def _onConfirmClicked(self, model: SubscribedPackagesModel) -> None: 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. # For now, all compatible packages presented to the user should be installed.
# Later, we might remove items for which the user unselected the package # Later, we might remove items for which the user unselected the package
model.setItems(model.getCompatiblePackages()) if model.getCompatiblePackages():
self.packageMutations.emit(model) model.setItems(model.getCompatiblePackages())
self.packageMutations.emit(model)

View file

@ -62,7 +62,8 @@ class DownloadPresenter:
"received": 0, "received": 0,
"total": 1, # make sure this is not considered done yet. Also divByZero-safe "total": 1, # make sure this is not considered done yet. Also divByZero-safe
"file_written": None, "file_written": None,
"request_data": request_data "request_data": request_data,
"package_model": item
} }
self._started = True self._started = True
@ -128,7 +129,14 @@ class DownloadPresenter:
if not item["file_written"]: if not item["file_written"]:
return False return False
success_items = {package_id : value["file_written"] for package_id, value in self._progress.items()} 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] error_items = [package_id for package_id in self._error]
self._progress_message.hide() self._progress_message.hide()

View file

@ -6,31 +6,52 @@ catalog = i18nCatalog("cura")
# Model for the ToolboxLicenseDialog # Model for the ToolboxLicenseDialog
class LicenseModel(QObject): class LicenseModel(QObject):
dialogTitleChanged = pyqtSignal() DEFAULT_DECLINE_BUTTON_TEXT = catalog.i18nc("@button", "Decline")
headerChanged = pyqtSignal() ACCEPT_BUTTON_TEXT = catalog.i18nc("@button", "Agree")
licenseTextChanged = pyqtSignal()
def __init__(self) -> None: dialogTitleChanged = pyqtSignal()
packageNameChanged = pyqtSignal()
licenseTextChanged = pyqtSignal()
iconChanged = pyqtSignal()
def __init__(self, decline_button_text: str = DEFAULT_DECLINE_BUTTON_TEXT) -> None:
super().__init__() super().__init__()
self._current_page_idx = 0 self._current_page_idx = 0
self._page_count = 1 self._page_count = 1
self._dialogTitle = "" self._dialogTitle = ""
self._header_text = ""
self._license_text = "" self._license_text = ""
self._package_name = "" 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) @pyqtProperty(str, notify=dialogTitleChanged)
def dialogTitle(self) -> str: def dialogTitle(self) -> str:
return self._dialogTitle return self._dialogTitle
@pyqtProperty(str, notify=headerChanged) @pyqtProperty(str, notify=packageNameChanged)
def headerText(self) -> str: def packageName(self) -> str:
return self._header_text return self._package_name
def setPackageName(self, name: str) -> None: def setPackageName(self, name: str) -> None:
self._header_text = name + ": " + catalog.i18nc("@label", "This plugin contains a license.\nYou need to accept this license to install this plugin.\nDo you agree with the terms below?") self._package_name = name
self.headerChanged.emit() 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) @pyqtProperty(str, notify=licenseTextChanged)
def licenseText(self) -> str: def licenseText(self) -> str:
@ -50,6 +71,7 @@ class LicenseModel(QObject):
self._updateDialogTitle() self._updateDialogTitle()
def _updateDialogTitle(self): def _updateDialogTitle(self):
self._dialogTitle = catalog.i18nc("@title:window", "Plugin License Agreement ({}/{})" self._dialogTitle = catalog.i18nc("@title:window", "Plugin License Agreement")
.format(self._current_page_idx + 1, self._page_count)) if self._page_count > 1:
self._dialogTitle = self._dialogTitle + " ({}/{})".format(self._current_page_idx + 1, self._page_count)
self.dialogTitleChanged.emit() self.dialogTitleChanged.emit()

View file

@ -1,5 +1,6 @@
import os import os
from typing import Dict, Optional, List from collections import OrderedDict
from typing import Dict, Optional, List, Any
from PyQt5.QtCore import QObject, pyqtSlot from PyQt5.QtCore import QObject, pyqtSlot
@ -17,6 +18,7 @@ class LicensePresenter(QObject):
def __init__(self, app: CuraApplication) -> None: def __init__(self, app: CuraApplication) -> None:
super().__init__() super().__init__()
self._catalog = i18nCatalog("cura")
self._dialog = None # type: Optional[QObject] self._dialog = None # type: Optional[QObject]
self._package_manager = app.getPackageManager() # type: PackageManager self._package_manager = app.getPackageManager() # type: PackageManager
# Emits List[Dict[str, [Any]] containing for example # Emits List[Dict[str, [Any]] containing for example
@ -25,7 +27,9 @@ class LicensePresenter(QObject):
self._current_package_idx = 0 self._current_package_idx = 0
self._package_models = [] # type: List[Dict] self._package_models = [] # type: List[Dict]
self._license_model = LicenseModel() # type: LicenseModel 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._app = app
@ -34,20 +38,23 @@ class LicensePresenter(QObject):
## Show a license dialog for multiple packages where users can read a license and accept or decline them ## 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 plugin_path: Root directory of the Toolbox plugin
# \param packages: Dict[package id, file path] # \param packages: Dict[package id, file path]
def present(self, plugin_path: str, packages: Dict[str, str]) -> None: def present(self, plugin_path: str, packages: Dict[str, Dict[str, str]]) -> None:
path = os.path.join(plugin_path, self._compatibility_dialog_path) path = os.path.join(plugin_path, self._compatibility_dialog_path)
self._initState(packages) self._initState(packages)
if self._page_count == 0:
self.licenseAnswers.emit(self._package_models)
return
if self._dialog is None: if self._dialog is None:
context_properties = { context_properties = {
"catalog": i18nCatalog("cura"), "catalog": self._catalog,
"licenseModel": self._license_model, "licenseModel": self._license_model,
"handler": self "handler": self
} }
self._dialog = self._app.createQmlComponent(path, context_properties) self._dialog = self._app.createQmlComponent(path, context_properties)
self._license_model.setPageCount(len(self._package_models))
self._presentCurrentPackage() self._presentCurrentPackage()
@pyqtSlot() @pyqtSlot()
@ -60,32 +67,41 @@ class LicensePresenter(QObject):
self._package_models[self._current_package_idx]["accepted"] = False self._package_models[self._current_package_idx]["accepted"] = False
self._checkNextPage() self._checkNextPage()
def _initState(self, packages: Dict[str, str]) -> None: def _initState(self, packages: Dict[str, Dict[str, Any]]) -> None:
self._package_models = [
{ implicitly_accepted_count = 0
"package_id" : package_id,
"package_path" : package_path, for package_id, item in packages.items():
"accepted" : None #: None: no answer yet item["package_id"] = package_id
} item["licence_content"] = self._package_manager.getPackageLicense(item["package_path"])
for package_id, package_path in packages.items() 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: def _presentCurrentPackage(self) -> None:
package_model = self._package_models[self._current_package_idx] package_model = self._package_models[self._current_package_idx]
license_content = self._package_manager.getPackageLicense(package_model["package_path"]) package_info = self._package_manager.getPackageInfo(package_model["package_path"])
if license_content is None:
# Implicitly accept when there is no license
self.onLicenseAccepted()
return
self._license_model.setCurrentPageIdx(self._current_package_idx) self._license_model.setCurrentPageIdx(self._current_package_idx)
self._license_model.setPackageName(package_model["package_id"]) self._license_model.setPackageName(package_info["display_name"])
self._license_model.setLicenseText(license_content) self._license_model.setIconUrl(package_model["icon_url"])
self._license_model.setLicenseText(package_model["licence_content"])
if self._dialog: if self._dialog:
self._dialog.open() # Does nothing if already open self._dialog.open() # Does nothing if already open
def _checkNextPage(self) -> None: def _checkNextPage(self) -> None:
if self._current_package_idx + 1 < len(self._package_models): if self._current_package_idx + 1 < self._page_count:
self._current_package_idx += 1 self._current_package_idx += 1
self._presentCurrentPackage() self._presentCurrentPackage()
else: else:

View file

@ -37,27 +37,18 @@ class SubscribedPackagesModel(ListModel):
return True return True
return False return False
# Sets the "is_compatible" to True for the given package, in memory
@pyqtSlot()
def dismissPackage(self, package_id: str) -> None:
package = self.find(key="package_id", value=package_id)
if package != -1:
self.setProperty(package, property="is_dismissed", value=True)
Logger.debug("Package {} has been dismissed".format(package_id))
def setMetadata(self, data: List[Dict[str, List[Any]]]) -> None:
self._metadata = data
def addDiscrepancies(self, discrepancy: List[str]) -> None: def addDiscrepancies(self, discrepancy: List[str]) -> None:
self._discrepancies = discrepancy self._discrepancies = discrepancy
def getCompatiblePackages(self): def getCompatiblePackages(self) -> List[Dict[str, Any]]:
return [x for x in self._items if x["is_compatible"]] return [package for package in self._items if package["is_compatible"]]
def initialize(self) -> None: def getIncompatiblePackages(self) -> List[str]:
return [package["package_id"] for package in self._items if not package["is_compatible"]]
def initialize(self, subscribed_packages_payload: List[Dict[str, Any]]) -> None:
self._items.clear() self._items.clear()
for item in self._metadata: for item in subscribed_packages_payload:
if item["package_id"] not in self._discrepancies: if item["package_id"] not in self._discrepancies:
continue continue
package = { package = {

View file

@ -1,12 +1,14 @@
import os import os
from typing import List, Dict, Any, cast from typing import List, Dict, Any, cast
from UM import i18n_catalog
from UM.Extension import Extension from UM.Extension import Extension
from UM.Logger import Logger from UM.Logger import Logger
from UM.Message import Message
from UM.PluginRegistry import PluginRegistry from UM.PluginRegistry import PluginRegistry
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
from .CloudPackageChecker import CloudPackageChecker from .CloudPackageChecker import CloudPackageChecker
from .CloudPackageManager import CloudPackageManager from .CloudApiClient import CloudApiClient
from .DiscrepanciesPresenter import DiscrepanciesPresenter from .DiscrepanciesPresenter import DiscrepanciesPresenter
from .DownloadPresenter import DownloadPresenter from .DownloadPresenter import DownloadPresenter
from .LicensePresenter import LicensePresenter from .LicensePresenter import LicensePresenter
@ -24,7 +26,7 @@ from .SubscribedPackagesModel import SubscribedPackagesModel
# - The DownloadPresenter shows a download progress dialog. It emits A tuple of succeeded and failed downloads # - 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 # - 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 # be installed. It emits the `licenseAnswers` signal for accept or declines
# - The CloudPackageManager removes the declined packages from the account # - The CloudApiClient removes the declined packages from the account
# - The SyncOrchestrator uses PackageManager to install the downloaded packages and delete temp files. # - 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 # - The RestartApplicationPresenter notifies the user that a restart is required for changes to take effect
class SyncOrchestrator(Extension): class SyncOrchestrator(Extension):
@ -36,7 +38,8 @@ class SyncOrchestrator(Extension):
self._name = "SyncOrchestrator" self._name = "SyncOrchestrator"
self._package_manager = app.getPackageManager() self._package_manager = app.getPackageManager()
self._cloud_package_manager = CloudPackageManager(app) # 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 = CloudPackageChecker(app) # type: CloudPackageChecker
self._checker.discrepancies.connect(self._onDiscrepancies) self._checker.discrepancies.connect(self._onDiscrepancies)
@ -61,32 +64,37 @@ class SyncOrchestrator(Extension):
self._download_presenter.download(mutations) self._download_presenter.download(mutations)
## Called when a set of packages have finished downloading ## Called when a set of packages have finished downloading
# \param success_items: Dict[package_id, file_path] # \param success_items: Dict[package_id, Dict[str, str]]
# \param error_items: List[package_id] # \param error_items: List[package_id]
def _onDownloadFinished(self, success_items: Dict[str, str], error_items: List[str]) -> None: def _onDownloadFinished(self, success_items: Dict[str, Dict[str, str]], error_items: List[str]) -> None:
# todo handle error items 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())) plugin_path = cast(str, PluginRegistry.getInstance().getPluginPath(self.getPluginId()))
self._license_presenter.present(plugin_path, success_items) self._license_presenter.present(plugin_path, success_items)
# Called when user has accepted / declined all licenses for the downloaded packages # Called when user has accepted / declined all licenses for the downloaded packages
def _onLicenseAnswers(self, answers: List[Dict[str, Any]]) -> None: def _onLicenseAnswers(self, answers: List[Dict[str, Any]]) -> None:
Logger.debug("Got license answers: {}", answers)
has_changes = False # True when at least one package is installed has_changes = False # True when at least one package is installed
for item in answers: for item in answers:
if item["accepted"]: if item["accepted"]:
# install and subscribe packages # install and subscribe packages
if not self._package_manager.installPackage(item["package_path"]): if not self._package_manager.installPackage(item["package_path"]):
Logger.error("could not install {}".format(item["package_id"])) message = "Could not install {}".format(item["package_id"])
self._showErrorMessage(message)
continue continue
self._cloud_package_manager.subscribe(item["package_id"])
has_changes = True has_changes = True
else: else:
# todo unsubscribe declined packages self._cloud_api.unsubscribe(item["package_id"])
pass
# delete temp file # delete temp file
os.remove(item["package_path"]) os.remove(item["package_path"])
if has_changes: if has_changes:
self._restart_presenter.present() self._restart_presenter.present()
## Logs an error and shows it to the user
def _showErrorMessage(self, text: str):
Logger.error(text)
Message(text, lifetime=0).show()

View file

@ -16,12 +16,12 @@ from UM.i18n import i18nCatalog
from UM.Version import Version from UM.Version import Version
from cura import ApplicationMetadata from cura import ApplicationMetadata
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
from cura.Machines.ContainerTree import ContainerTree from cura.Machines.ContainerTree import ContainerTree
from .CloudApiModel import CloudApiModel from .CloudApiModel import CloudApiModel
from .AuthorsModel import AuthorsModel from .AuthorsModel import AuthorsModel
from .CloudSync.CloudPackageManager import CloudPackageManager
from .CloudSync.LicenseModel import LicenseModel from .CloudSync.LicenseModel import LicenseModel
from .PackagesModel import PackagesModel from .PackagesModel import PackagesModel
from .UltimakerCloudScope import UltimakerCloudScope from .UltimakerCloudScope import UltimakerCloudScope
@ -32,6 +32,13 @@ if TYPE_CHECKING:
i18n_catalog = i18nCatalog("cura") 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
# todo Remove license and download dialog, use SyncOrchestrator instead # todo Remove license and download dialog, use SyncOrchestrator instead
## Provides a marketplace for users to download plugins an materials ## Provides a marketplace for users to download plugins an materials
@ -44,7 +51,6 @@ class Toolbox(QObject, Extension):
self._sdk_version = ApplicationMetadata.CuraSDKVersion # type: Union[str, int] self._sdk_version = ApplicationMetadata.CuraSDKVersion # type: Union[str, int]
# Network: # Network:
self._cloud_package_manager = CloudPackageManager(application) # type: CloudPackageManager
self._download_request_data = None # type: Optional[HttpRequestData] self._download_request_data = None # type: Optional[HttpRequestData]
self._download_progress = 0 # type: float self._download_progress = 0 # type: float
self._is_downloading = False # type: bool self._is_downloading = False # type: bool
@ -147,17 +153,14 @@ class Toolbox(QObject, Extension):
self._application.getHttpRequestManager().put(url, data = data.encode(), scope = self._scope) self._application.getHttpRequestManager().put(url, data = data.encode(), scope = self._scope)
@pyqtSlot(str)
def subscribe(self, package_id: str) -> None:
self._cloud_package_manager.subscribe(package_id)
def getLicenseDialogPluginFileLocation(self) -> str: def getLicenseDialogPluginFileLocation(self) -> str:
return self._license_dialog_plugin_file_location return self._license_dialog_plugin_file_location
def openLicenseDialog(self, plugin_name: str, license_content: str, plugin_file_location: str) -> None: 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 # Set page 1/1 when opening the dialog for a single package
self._license_model.setCurrentPageIdx(0) self._license_model.setCurrentPageIdx(0)
self._license_model.setPageCount(1) self._license_model.setPageCount(1)
self._license_model.setIconUrl(icon_url)
self._license_model.setPackageName(plugin_name) self._license_model.setPackageName(plugin_name)
self._license_model.setLicenseText(license_content) self._license_model.setLicenseText(license_content)
@ -376,7 +379,6 @@ class Toolbox(QObject, Extension):
def onLicenseAccepted(self): def onLicenseAccepted(self):
self.closeLicenseDialog.emit() self.closeLicenseDialog.emit()
package_id = self.install(self.getLicenseDialogPluginFileLocation()) package_id = self.install(self.getLicenseDialogPluginFileLocation())
self.subscribe(package_id)
@pyqtSlot() @pyqtSlot()
@ -670,14 +672,16 @@ class Toolbox(QObject, Extension):
return return
license_content = self._package_manager.getPackageLicense(file_path) license_content = self._package_manager.getPackageLicense(file_path)
package_id = package_info["package_id"]
if license_content is not None: if license_content is not None:
self.openLicenseDialog(package_info["package_id"], license_content, file_path) # 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 return
package_id = self.install(file_path) installed_id = self.install(file_path)
if package_id != package_info["package_id"]: if installed_id != package_id:
Logger.error("Installed package {} does not match {}".format(package_id, package_info["package_id"])) Logger.error("Installed package {} does not match {}".format(installed_id, package_id))
self.subscribe(package_id)
# Getter & Setters for Properties: # Getter & Setters for Properties:
# -------------------------------------------------------------------------- # --------------------------------------------------------------------------
@ -699,14 +703,14 @@ class Toolbox(QObject, Extension):
def isDownloading(self) -> bool: def isDownloading(self) -> bool:
return self._is_downloading return self._is_downloading
def setActivePackage(self, package: Dict[str, Any]) -> None: def setActivePackage(self, package: QObject) -> None:
if self._active_package != package: if self._active_package != package:
self._active_package = package self._active_package = package
self.activePackageChanged.emit() self.activePackageChanged.emit()
## The active package is the package that is currently being downloaded ## The active package is the package that is currently being downloaded
@pyqtProperty(QObject, fset = setActivePackage, notify = activePackageChanged) @pyqtProperty(QObject, fset = setActivePackage, notify = activePackageChanged)
def activePackage(self) -> Optional[Dict[str, Any]]: def activePackage(self) -> Optional[QObject]:
return self._active_package return self._active_package
def setViewCategory(self, category: str = "plugin") -> None: def setViewCategory(self, category: str = "plugin") -> None:
@ -770,6 +774,13 @@ class Toolbox(QObject, Extension):
def materialsGenericModel(self) -> PackagesModel: def materialsGenericModel(self) -> PackagesModel:
return self._materials_generic_model 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: # Filter Models:
# -------------------------------------------------------------------------- # --------------------------------------------------------------------------
@pyqtSlot(str, str, str) @pyqtSlot(str, str, str)

View file

@ -6,6 +6,9 @@ from cura.API import Account
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
## Add a Authorization header to the request for Ultimaker Cloud Api requests.
# When the user is not logged in or a token is not available, a warning will be logged
# Also add the user agent headers (see DefaultUserAgentScope)
class UltimakerCloudScope(DefaultUserAgentScope): class UltimakerCloudScope(DefaultUserAgentScope):
def __init__(self, application: CuraApplication): def __init__(self, application: CuraApplication):
super().__init__(application) super().__init__(application)

View file

@ -3,5 +3,5 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.0", "version": "1.0.0",
"description": "Provides support for reading model files.", "description": "Provides support for reading model files.",
"api": "7.0.0" "api": "7.1.0"
} }

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.0", "version": "1.0.0",
"description": "Provides support for reading Ultimaker Format Packages.", "description": "Provides support for reading Ultimaker Format Packages.",
"supported_sdk_versions": ["7.0.0"], "supported_sdk_versions": ["7.1.0"],
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.1", "version": "1.0.1",
"description": "Provides support for writing Ultimaker Format Packages.", "description": "Provides support for writing Ultimaker Format Packages.",
"api": "7.0", "api": "7.1",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"description": "Manages network connections to Ultimaker networked printers.", "description": "Manages network connections to Ultimaker networked printers.",
"version": "2.0.0", "version": "2.0.0",
"api": "7.0", "api": "7.1",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -2,7 +2,7 @@
"name": "USB printing", "name": "USB printing",
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.2", "version": "1.0.2",
"api": "7.0", "api": "7.1",
"description": "Accepts G-Code and sends them to a printer. Plugin can also update firmware.", "description": "Accepts G-Code and sends them to a printer. Plugin can also update firmware.",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.1", "version": "1.0.1",
"description": "Provides machine actions for Ultimaker machines (such as bed leveling wizard, selecting upgrades, etc.).", "description": "Provides machine actions for Ultimaker machines (such as bed leveling wizard, selecting upgrades, etc.).",
"api": "7.0", "api": "7.1",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.1", "version": "1.0.1",
"description": "Upgrades configurations from Cura 2.1 to Cura 2.2.", "description": "Upgrades configurations from Cura 2.1 to Cura 2.2.",
"api": "7.0", "api": "7.1",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.1", "version": "1.0.1",
"description": "Upgrades configurations from Cura 2.2 to Cura 2.4.", "description": "Upgrades configurations from Cura 2.2 to Cura 2.4.",
"api": "7.0", "api": "7.1",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.1", "version": "1.0.1",
"description": "Upgrades configurations from Cura 2.5 to Cura 2.6.", "description": "Upgrades configurations from Cura 2.5 to Cura 2.6.",
"api": "7.0", "api": "7.1",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.1", "version": "1.0.1",
"description": "Upgrades configurations from Cura 2.6 to Cura 2.7.", "description": "Upgrades configurations from Cura 2.6 to Cura 2.7.",
"api": "7.0", "api": "7.1",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.1", "version": "1.0.1",
"description": "Upgrades configurations from Cura 2.7 to Cura 3.0.", "description": "Upgrades configurations from Cura 2.7 to Cura 3.0.",
"api": "7.0", "api": "7.1",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.1", "version": "1.0.1",
"description": "Upgrades configurations from Cura 3.0 to Cura 3.1.", "description": "Upgrades configurations from Cura 3.0 to Cura 3.1.",
"api": "7.0", "api": "7.1",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.1", "version": "1.0.1",
"description": "Upgrades configurations from Cura 3.2 to Cura 3.3.", "description": "Upgrades configurations from Cura 3.2 to Cura 3.3.",
"api": "7.0", "api": "7.1",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.1", "version": "1.0.1",
"description": "Upgrades configurations from Cura 3.3 to Cura 3.4.", "description": "Upgrades configurations from Cura 3.3 to Cura 3.4.",
"api": "7.0", "api": "7.1",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.1", "version": "1.0.1",
"description": "Upgrades configurations from Cura 3.4 to Cura 3.5.", "description": "Upgrades configurations from Cura 3.4 to Cura 3.5.",
"api": "7.0", "api": "7.1",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.0", "version": "1.0.0",
"description": "Upgrades configurations from Cura 3.5 to Cura 4.0.", "description": "Upgrades configurations from Cura 3.5 to Cura 4.0.",
"api": "7.0", "api": "7.1",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.1", "version": "1.0.1",
"description": "Upgrades configurations from Cura 4.0 to Cura 4.1.", "description": "Upgrades configurations from Cura 4.0 to Cura 4.1.",
"api": "7.0", "api": "7.1",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.0", "version": "1.0.0",
"description": "Upgrades configurations from Cura 4.1 to Cura 4.2.", "description": "Upgrades configurations from Cura 4.1 to Cura 4.2.",
"api": "7.0", "api": "7.1",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.0", "version": "1.0.0",
"description": "Upgrades configurations from Cura 4.2 to Cura 4.3.", "description": "Upgrades configurations from Cura 4.2 to Cura 4.3.",
"api": "7.0", "api": "7.1",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.0", "version": "1.0.0",
"description": "Upgrades configurations from Cura 4.3 to Cura 4.4.", "description": "Upgrades configurations from Cura 4.3 to Cura 4.4.",
"api": "7.0", "api": "7.1",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -1,6 +1,16 @@
# Copyright (c) 2020 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import configparser import configparser
from typing import Tuple, List from typing import Tuple, List
import fnmatch # To filter files that we need to delete.
import io import io
import os # To get the path to check for hidden stacks to delete.
import urllib.parse # To get the container IDs from file names.
import re # To filter directories to search for hidden stacks to delete.
from UM.Logger import Logger
from UM.Resources import Resources # To get the path to check for hidden stacks to delete.
from UM.Version import Version # To sort folders by version number.
from UM.VersionUpgrade import VersionUpgrade from UM.VersionUpgrade import VersionUpgrade
# Settings that were merged into one. Each one is a pair of settings. If both # Settings that were merged into one. Each one is a pair of settings. If both
@ -16,6 +26,102 @@ _removed_settings = {
} }
class VersionUpgrade44to45(VersionUpgrade): class VersionUpgrade44to45(VersionUpgrade):
def __init__(self) -> None:
"""
Creates the version upgrade plug-in from 4.4 to 4.5.
In this case the plug-in will also check for stacks that need to be
deleted.
"""
# Only delete hidden stacks when upgrading from version 4.4. Not 4.3 or 4.5, just when you're starting out from 4.4.
# If you're starting from an earlier version, you can't have had the bug that produces too many hidden stacks (https://github.com/Ultimaker/Cura/issues/6731).
# If you're starting from a later version, the bug was already fixed.
data_storage_root = os.path.dirname(Resources.getDataStoragePath())
folders = set(os.listdir(data_storage_root)) # All version folders.
folders = set(filter(lambda p: re.fullmatch(r"\d+\.\d+", p), folders)) # Only folders with a correct version number as name.
folders.difference_update({os.path.basename(Resources.getDataStoragePath())}) # Remove current version from candidates (since the folder was just copied).
if folders:
latest_version = max(folders, key = Version) # Sort them by semantic version numbering.
if latest_version == "4.4":
self.removeHiddenStacks()
def removeHiddenStacks(self) -> None:
"""
If starting the upgrade from 4.4, this will remove any hidden printer
stacks from the configuration folder as well as all of the user profiles
and definition changes profiles.
This will ONLY run when upgrading from 4.4, not when e.g. upgrading from
4.3 to 4.6 (through 4.4). This is because it's to fix a bug
(https://github.com/Ultimaker/Cura/issues/6731) that occurred in 4.4
only, so only there will it have hidden stacks that need to be deleted.
If people upgrade from 4.3 they don't need to be deleted. If people
upgrade from 4.5 they have already been deleted previously or never got
the broken hidden stacks.
"""
Logger.log("d", "Removing all hidden container stacks.")
hidden_global_stacks = set() # Which global stacks have been found? We'll delete anything referred to by these. Set of stack IDs.
hidden_extruder_stacks = set() # Which extruder stacks refer to the hidden global profiles?
hidden_instance_containers = set() # Which instance containers are referred to by the hidden stacks?
exclude_directories = {"plugins"}
# First find all of the hidden container stacks.
data_storage = Resources.getDataStoragePath()
for root, dirs, files in os.walk(data_storage):
dirs[:] = [dir for dir in dirs if dir not in exclude_directories]
for filename in fnmatch.filter(files, "*.global.cfg"):
parser = configparser.ConfigParser(interpolation = None)
try:
parser.read(os.path.join(root, filename))
except OSError: # File not found or insufficient rights.
continue
except configparser.Error: # Invalid file format.
continue
if "metadata" in parser and "hidden" in parser["metadata"] and parser["metadata"]["hidden"] == "True":
stack_id = urllib.parse.unquote_plus(os.path.basename(filename).split(".")[0])
hidden_global_stacks.add(stack_id)
# The user container and definition changes container are specific to this stack. We need to delete those too.
if "containers" in parser:
if "0" in parser["containers"]: # User container.
hidden_instance_containers.add(parser["containers"]["0"])
if "6" in parser["containers"]: # Definition changes container.
hidden_instance_containers.add(parser["containers"]["6"])
os.remove(os.path.join(root, filename))
# Walk a second time to find all extruder stacks referring to these hidden container stacks.
for root, dirs, files in os.walk(data_storage):
dirs[:] = [dir for dir in dirs if dir not in exclude_directories]
for filename in fnmatch.filter(files, "*.extruder.cfg"):
parser = configparser.ConfigParser(interpolation = None)
try:
parser.read(os.path.join(root, filename))
except OSError: # File not found or insufficient rights.
continue
except configparser.Error: # Invalid file format.
continue
if "metadata" in parser and "machine" in parser["metadata"] and parser["metadata"]["machine"] in hidden_global_stacks:
stack_id = urllib.parse.unquote_plus(os.path.basename(filename).split(".")[0])
hidden_extruder_stacks.add(stack_id)
# The user container and definition changes container are specific to this stack. We need to delete those too.
if "containers" in parser:
if "0" in parser["containers"]: # User container.
hidden_instance_containers.add(parser["containers"]["0"])
if "6" in parser["containers"]: # Definition changes container.
hidden_instance_containers.add(parser["containers"]["6"])
os.remove(os.path.join(root, filename))
# Walk a third time to remove all instance containers that are referred to by either of those.
for root, dirs, files in os.walk(data_storage):
dirs[:] = [dir for dir in dirs if dir not in exclude_directories]
for filename in fnmatch.filter(files, "*.inst.cfg"):
container_id = urllib.parse.unquote_plus(os.path.basename(filename).split(".")[0])
if container_id in hidden_instance_containers:
try:
os.remove(os.path.join(root, filename))
except OSError: # Is a directory, file not found, or insufficient rights.
continue
def getCfgVersion(self, serialised: str) -> int: def getCfgVersion(self, serialised: str) -> int:
parser = configparser.ConfigParser(interpolation = None) parser = configparser.ConfigParser(interpolation = None)
parser.read_string(serialised) parser.read_string(serialised)

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.0", "version": "1.0.0",
"description": "Upgrades configurations from Cura 4.4 to Cura 4.5.", "description": "Upgrades configurations from Cura 4.4 to Cura 4.5.",
"api": "7.0", "api": "7.1",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -1,8 +1,8 @@
{ {
"name": "X3D Reader", "name": "X3D Reader",
"author": "Seva Alekseyev", "author": "Seva Alekseyev, Ultimaker B.V.",
"version": "1.0.1", "version": "1.0.1",
"description": "Provides support for reading X3D files.", "description": "Provides support for reading X3D files.",
"api": "7.0", "api": "7.1",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.1", "version": "1.0.1",
"description": "Provides the X-Ray view.", "description": "Provides the X-Ray view.",
"api": "7.0", "api": "7.1",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

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